Skip to content

Commit 02dbebb

Browse files
authored
render existing openapi examples (github#26405)
1 parent dfdbfa9 commit 02dbebb

32 files changed

+317426
-241305
lines changed
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { parseTemplate } from 'url-template'
2+
import { stringify } from 'javascript-stringify'
3+
4+
import type { CodeSample, Operation } from '../rest/types'
5+
6+
/*
7+
Generates a curl example
8+
9+
For example:
10+
curl \
11+
-X POST \
12+
-H "Accept: application/vnd.github.v3+json" \
13+
https://{hostname}/api/v3/repos/OWNER/REPO/deployments \
14+
-d '{"ref":"topic-branch","payload":"{ \"deploy\": \"migrate\" }","description":"Deploy request from hubot"}'
15+
*/
16+
export function getShellExample(operation: Operation, codeSample: CodeSample) {
17+
// This allows us to display custom media types like application/sarif+json
18+
const defaultAcceptHeader = codeSample?.response?.contentType?.includes('+json')
19+
? codeSample.response.contentType
20+
: 'application/vnd.github.v3+json'
21+
22+
const requestPath = codeSample?.request?.parameters
23+
? parseTemplate(operation.requestPath).expand(codeSample.request.parameters)
24+
: operation.requestPath
25+
26+
let requestBodyParams = ''
27+
if (codeSample?.request?.bodyParameters) {
28+
requestBodyParams = `-d '${JSON.stringify(codeSample.request.bodyParameters)}'`
29+
30+
// If the content type is application/x-www-form-urlencoded the format of
31+
// the shell example is --data-urlencode param1=value1 --data-urlencode param2=value2
32+
// For example, this operation:
33+
// https://docs.github.com/en/enterprise/rest/reference/enterprise-admin#enable-or-disable-maintenance-mode
34+
if (codeSample.request.contentType === 'application/x-www-form-urlencoded') {
35+
requestBodyParams = ''
36+
const paramNames = Object.keys(codeSample.request.bodyParameters)
37+
paramNames.forEach((elem) => {
38+
requestBodyParams = `${requestBodyParams} --data-urlencode ${elem}=${codeSample.request.bodyParameters[elem]}`
39+
})
40+
}
41+
}
42+
43+
const args = [
44+
operation.verb !== 'get' && `-X ${operation.verb.toUpperCase()}`,
45+
`-H "Accept: ${defaultAcceptHeader}"`,
46+
`${operation.serverUrl}${requestPath}`,
47+
requestBodyParams,
48+
].filter(Boolean)
49+
return `curl \\\n ${args.join(' \\\n ')}`
50+
}
51+
52+
/*
53+
Generates a GitHub CLI example
54+
55+
For example:
56+
gh api \
57+
-X POST \
58+
-H "Accept: application/vnd.github.v3+json" \
59+
/repos/OWNER/REPO/deployments \
60+
-fref,topic-branch=0,payload,{ "deploy": "migrate" }=1,description,Deploy request from hubot=2
61+
*/
62+
export function getGHExample(operation: Operation, codeSample: CodeSample) {
63+
const defaultAcceptHeader = codeSample?.response?.contentType?.includes('+json')
64+
? codeSample.response.contentType
65+
: 'application/vnd.github.v3+json'
66+
const hostname = operation.serverUrl !== 'https://api.github.com' ? '--hostname HOSTNAME' : ''
67+
68+
const requestPath = codeSample?.request?.parameters
69+
? parseTemplate(operation.requestPath).expand(codeSample.request.parameters)
70+
: operation.requestPath
71+
72+
let requestBodyParams = ''
73+
if (codeSample?.request?.bodyParameters) {
74+
const bodyParamValues = Object.values(codeSample.request.bodyParameters)
75+
// GitHub CLI does not support sending Objects and arrays using the -F or
76+
// -f flags. That support may be added in the future. It is possible to
77+
// use gh api --input to take a JSON object from standard input
78+
// constructed by jq and piped to gh api. However, we'll hold off on adding
79+
// that complexity for now.
80+
if (bodyParamValues.some((elem) => typeof elem === 'object')) {
81+
return undefined
82+
}
83+
requestBodyParams = Object.keys(codeSample.request.bodyParameters)
84+
.map((key) => {
85+
if (typeof codeSample.request.bodyParameters[key] === 'string') {
86+
return `-f ${key}='${codeSample.request.bodyParameters[key]}'`
87+
} else {
88+
return `-F ${key}=${codeSample.request.bodyParameters[key]}`
89+
}
90+
})
91+
.join(' ')
92+
}
93+
const args = [
94+
operation.verb !== 'get' && `--method ${operation.verb.toUpperCase()}`,
95+
`-H "Accept: ${defaultAcceptHeader}"`,
96+
hostname,
97+
requestPath,
98+
requestBodyParams,
99+
].filter(Boolean)
100+
return `gh api \\\n ${args.join(' \\\n ')}`
101+
}
102+
103+
/*
104+
Generates an octokit.js example
105+
106+
For example:
107+
await octokit.request('POST /repos/{owner}/{repo}/deployments'{
108+
"owner": "OWNER",
109+
"repo": "REPO",
110+
"ref": "topic-branch",
111+
"payload": "{ \"deploy\": \"migrate\" }",
112+
"description": "Deploy request from hubot"
113+
})
114+
115+
*/
116+
export function getJSExample(operation: Operation, codeSample: CodeSample) {
117+
const parameters = codeSample.request
118+
? { ...codeSample.request.parameters, ...codeSample.request.bodyParameters }
119+
: {}
120+
return `await octokit.request('${operation.verb.toUpperCase()} ${
121+
operation.requestPath
122+
}', ${stringify(parameters, null, 2)})`
123+
}

components/rest/CodeBlock.tsx

+13-20
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import cx from 'classnames'
22
import { CheckIcon, CopyIcon } from '@primer/octicons-react'
33
import { Tooltip } from '@primer/react'
4-
54
import useClipboard from 'components/hooks/useClipboard'
6-
75
import styles from './CodeBlock.module.scss'
6+
import type { ReactNode } from 'react'
87

98
type Props = {
109
verb?: string
11-
// Only Code samples should have a copy icon - if there's a headingLang it's a code sample
12-
headingLang?: string
10+
headingLang?: ReactNode | string
1311
codeBlock: string
1412
highlight?: string
1513
}
@@ -20,20 +18,12 @@ export function CodeBlock({ verb, headingLang, codeBlock, highlight }: Props) {
2018
})
2119

2220
return (
23-
<div className={headingLang && 'code-extra'}>
21+
<div className={headingLang ? 'code-extra' : undefined}>
22+
{/* Only Code samples should have a copy icon
23+
If there's a headingLang it's a code sample */}
2424
{headingLang && (
2525
<header className="d-flex flex-justify-between flex-items-center p-2 text-small rounded-top-1 border">
26-
{headingLang === 'JavaScript' ? (
27-
<span>
28-
{headingLang} (
29-
<a className="text-underline" href="https://github.com/octokit/core.js#readme">
30-
@octokit/core.js
31-
</a>
32-
)
33-
</span>
34-
) : (
35-
`${headingLang}`
36-
)}
26+
{headingLang}
3727
<Tooltip direction="w" aria-label={isCopied ? 'Copied!' : 'Copy to clipboard'}>
3828
<button className="js-btn-copy btn-octicon" onClick={() => setCopied()}>
3929
{isCopied ? <CheckIcon /> : <CopyIcon />}
@@ -44,10 +34,13 @@ export function CodeBlock({ verb, headingLang, codeBlock, highlight }: Props) {
4434
<pre className={cx(styles.codeBlock, 'rounded-1 border')} data-highlight={highlight}>
4535
<code>
4636
{verb && (
47-
<span className="color-bg-accent-emphasis color-fg-on-emphasis rounded-1 text-uppercase p-1">
48-
{verb}
49-
</span>
50-
)}{' '}
37+
<>
38+
<span className="color-bg-accent-emphasis color-fg-on-emphasis rounded-1 text-uppercase p-1">
39+
{verb}
40+
</span>
41+
<> </>
42+
</>
43+
)}
5144
{codeBlock}
5245
</code>
5346
</pre>

components/rest/PreviewsRow.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { xGitHub } from './types'
21
import { useTranslation } from 'components/hooks/useTranslation'
32

43
type Props = {
54
slug: string
6-
xGitHub: xGitHub
5+
numPreviews: number
76
}
87

9-
export function PreviewsRow({ slug, xGitHub }: Props) {
8+
export function PreviewsRow({ slug, numPreviews }: Props) {
109
const { t } = useTranslation('products')
11-
const hasPreviews = xGitHub.previews && xGitHub.previews.length > 0
1210

1311
return (
1412
<tr>
@@ -21,9 +19,9 @@ export function PreviewsRow({ slug, xGitHub }: Props) {
2119
<p className="m-0">
2220
Setting to
2321
<code>application/vnd.github.v3+json</code> is recommended.
24-
{hasPreviews && (
22+
{numPreviews > 0 && (
2523
<a href={`#${slug}-preview-notices`} className="d-inline">
26-
{xGitHub.previews.length > 1
24+
{numPreviews > 1
2725
? ` ${t('rest.reference.see_preview_notices')}`
2826
: ` ${t('rest.reference.see_preview_notice')}`}
2927
</a>

components/rest/RestCodeSamples.tsx

+77-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,92 @@
1-
import type { xCodeSample } from './types'
1+
import type { Operation } from './types'
22
import { useTranslation } from 'components/hooks/useTranslation'
33
import { CodeBlock } from './CodeBlock'
4-
import { Fragment } from 'react'
4+
import { getShellExample, getGHExample, getJSExample } from '../lib/get-rest-code-samples'
55

66
type Props = {
77
slug: string
8-
xCodeSamples: Array<xCodeSample>
8+
operation: Operation
99
}
1010

11-
export function RestCodeSamples({ slug, xCodeSamples }: Props) {
11+
export function RestCodeSamples({ operation, slug }: Props) {
1212
const { t } = useTranslation('products')
1313

14+
const JAVASCRIPT_HEADING = (
15+
<span>
16+
JavaScript{' '}
17+
<a className="text-underline" href="https://github.com/octokit/core.js#readme">
18+
@octokit/core.js
19+
</a>
20+
</span>
21+
)
22+
23+
const GH_CLI_HEADING = (
24+
<span>
25+
GitHub CLI{' '}
26+
<a className="text-underline" href="https://cli.github.com/manual/gh_api">
27+
gh api
28+
</a>
29+
</span>
30+
)
31+
32+
// Format the example properties into different language examples
33+
const languageExamples = operation.codeExamples.map((sample) => {
34+
const languageExamples = {
35+
curl: getShellExample(operation, sample),
36+
javascript: getJSExample(operation, sample),
37+
ghcli: getGHExample(operation, sample),
38+
}
39+
return Object.assign({}, sample, languageExamples)
40+
})
41+
1442
return (
15-
<Fragment key={xCodeSamples + slug}>
43+
<>
1644
<h4 id={`${slug}--code-samples`}>
1745
<a href={`#${slug}--code-samples`}>{`${t('rest.reference.code_samples')}`}</a>
1846
</h4>
19-
{xCodeSamples.map((sample, index) => {
20-
const sampleElements: JSX.Element[] = []
21-
if (sample.lang !== 'Ruby') {
22-
sampleElements.push(
23-
<CodeBlock
24-
key={sample.lang + index}
25-
headingLang={sample.lang}
26-
codeBlock={sample.source}
27-
highlight={sample.lang === 'JavaScript' ? 'javascript' : 'curl'}
28-
></CodeBlock>
29-
)
30-
}
31-
return sampleElements
32-
})}
33-
</Fragment>
47+
{languageExamples.map((sample, index) => (
48+
<div key={`${JSON.stringify(sample)}-${index}`}>
49+
{/* Example requests */}
50+
{sample.request && (
51+
<>
52+
{/* Title of the code sample block */}
53+
<h5 dangerouslySetInnerHTML={{ __html: sample.request.description }} />
54+
{sample.curl && (
55+
<CodeBlock headingLang="Shell" codeBlock={sample.curl} highlight="curl" />
56+
)}
57+
{sample.javascript && (
58+
<CodeBlock
59+
headingLang={JAVASCRIPT_HEADING}
60+
codeBlock={sample.javascript}
61+
highlight="javascript"
62+
/>
63+
)}
64+
{sample.ghcli && (
65+
<CodeBlock headingLang={GH_CLI_HEADING} codeBlock={sample.ghcli} highlight="curl" />
66+
)}
67+
</>
68+
)}
69+
70+
{/* Title of the response */}
71+
{sample.response && (
72+
<>
73+
<h5 dangerouslySetInnerHTML={{ __html: sample.response.description }} />
74+
{/* Status code */}
75+
{sample.response.statusCode && (
76+
<CodeBlock codeBlock={`Status: ${sample.response.statusCode}`} />
77+
)}
78+
79+
{/* Example response */}
80+
{sample.response.example && (
81+
<CodeBlock
82+
codeBlock={JSON.stringify(sample.response.example, null, 2)}
83+
highlight="json"
84+
/>
85+
)}
86+
</>
87+
)}
88+
</div>
89+
))}
90+
</>
3491
)
3592
}

components/rest/RestHTTPMethod.tsx

-10
This file was deleted.

components/rest/RestNotes.tsx

+10-14
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
import { useTranslation } from 'components/hooks/useTranslation'
1+
import { useRouter } from 'next/router'
22

3-
type Props = {
4-
notes: Array<string>
5-
enabledForGitHubApps: boolean
6-
}
3+
import { useTranslation } from 'components/hooks/useTranslation'
4+
import { Link } from 'components/Link'
75

8-
export function RestNotes({ notes, enabledForGitHubApps }: Props) {
6+
export function RestNotes() {
97
const { t } = useTranslation('products')
8+
const router = useRouter()
109

1110
return (
1211
<>
1312
<h4 className="pt-4">{t('rest.reference.notes')}</h4>
1413
<ul className="mt-2 pl-3 pb-2">
15-
{enabledForGitHubApps && (
16-
<li>
17-
<a href="/developers/apps">Works with GitHub Apps</a>
18-
</li>
19-
)}
20-
{notes.map((note: string) => {
21-
return <li>{note}</li>
22-
})}
14+
<li>
15+
<Link href={`/${router.locale}/developers/apps`}>
16+
{t('rest.reference.works_with_github_apps')}
17+
</Link>
18+
</li>
2319
</ul>
2420
</>
2521
)

0 commit comments

Comments
 (0)