1
0
mirror of synced 2025-12-22 11:26:57 -05:00

render existing openapi examples (#26405)

This commit is contained in:
Rachael Sewell
2022-04-11 09:09:03 -07:00
committed by GitHub
parent dfdbfa9366
commit 02dbebbef3
32 changed files with 340459 additions and 264338 deletions

View File

@@ -0,0 +1,123 @@
import { parseTemplate } from 'url-template'
import { stringify } from 'javascript-stringify'
import type { CodeSample, Operation } from '../rest/types'
/*
Generates a curl example
For example:
curl \
-X POST \
-H "Accept: application/vnd.github.v3+json" \
https://{hostname}/api/v3/repos/OWNER/REPO/deployments \
-d '{"ref":"topic-branch","payload":"{ \"deploy\": \"migrate\" }","description":"Deploy request from hubot"}'
*/
export function getShellExample(operation: Operation, codeSample: CodeSample) {
// This allows us to display custom media types like application/sarif+json
const defaultAcceptHeader = codeSample?.response?.contentType?.includes('+json')
? codeSample.response.contentType
: 'application/vnd.github.v3+json'
const requestPath = codeSample?.request?.parameters
? parseTemplate(operation.requestPath).expand(codeSample.request.parameters)
: operation.requestPath
let requestBodyParams = ''
if (codeSample?.request?.bodyParameters) {
requestBodyParams = `-d '${JSON.stringify(codeSample.request.bodyParameters)}'`
// If the content type is application/x-www-form-urlencoded the format of
// the shell example is --data-urlencode param1=value1 --data-urlencode param2=value2
// For example, this operation:
// https://docs.github.com/en/enterprise/rest/reference/enterprise-admin#enable-or-disable-maintenance-mode
if (codeSample.request.contentType === 'application/x-www-form-urlencoded') {
requestBodyParams = ''
const paramNames = Object.keys(codeSample.request.bodyParameters)
paramNames.forEach((elem) => {
requestBodyParams = `${requestBodyParams} --data-urlencode ${elem}=${codeSample.request.bodyParameters[elem]}`
})
}
}
const args = [
operation.verb !== 'get' && `-X ${operation.verb.toUpperCase()}`,
`-H "Accept: ${defaultAcceptHeader}"`,
`${operation.serverUrl}${requestPath}`,
requestBodyParams,
].filter(Boolean)
return `curl \\\n ${args.join(' \\\n ')}`
}
/*
Generates a GitHub CLI example
For example:
gh api \
-X POST \
-H "Accept: application/vnd.github.v3+json" \
/repos/OWNER/REPO/deployments \
-fref,topic-branch=0,payload,{ "deploy": "migrate" }=1,description,Deploy request from hubot=2
*/
export function getGHExample(operation: Operation, codeSample: CodeSample) {
const defaultAcceptHeader = codeSample?.response?.contentType?.includes('+json')
? codeSample.response.contentType
: 'application/vnd.github.v3+json'
const hostname = operation.serverUrl !== 'https://api.github.com' ? '--hostname HOSTNAME' : ''
const requestPath = codeSample?.request?.parameters
? parseTemplate(operation.requestPath).expand(codeSample.request.parameters)
: operation.requestPath
let requestBodyParams = ''
if (codeSample?.request?.bodyParameters) {
const bodyParamValues = Object.values(codeSample.request.bodyParameters)
// GitHub CLI does not support sending Objects and arrays using the -F or
// -f flags. That support may be added in the future. It is possible to
// use gh api --input to take a JSON object from standard input
// constructed by jq and piped to gh api. However, we'll hold off on adding
// that complexity for now.
if (bodyParamValues.some((elem) => typeof elem === 'object')) {
return undefined
}
requestBodyParams = Object.keys(codeSample.request.bodyParameters)
.map((key) => {
if (typeof codeSample.request.bodyParameters[key] === 'string') {
return `-f ${key}='${codeSample.request.bodyParameters[key]}'`
} else {
return `-F ${key}=${codeSample.request.bodyParameters[key]}`
}
})
.join(' ')
}
const args = [
operation.verb !== 'get' && `--method ${operation.verb.toUpperCase()}`,
`-H "Accept: ${defaultAcceptHeader}"`,
hostname,
requestPath,
requestBodyParams,
].filter(Boolean)
return `gh api \\\n ${args.join(' \\\n ')}`
}
/*
Generates an octokit.js example
For example:
await octokit.request('POST /repos/{owner}/{repo}/deployments'{
"owner": "OWNER",
"repo": "REPO",
"ref": "topic-branch",
"payload": "{ \"deploy\": \"migrate\" }",
"description": "Deploy request from hubot"
})
*/
export function getJSExample(operation: Operation, codeSample: CodeSample) {
const parameters = codeSample.request
? { ...codeSample.request.parameters, ...codeSample.request.bodyParameters }
: {}
return `await octokit.request('${operation.verb.toUpperCase()} ${
operation.requestPath
}', ${stringify(parameters, null, 2)})`
}

View File

@@ -1,15 +1,13 @@
import cx from 'classnames' import cx from 'classnames'
import { CheckIcon, CopyIcon } from '@primer/octicons-react' import { CheckIcon, CopyIcon } from '@primer/octicons-react'
import { Tooltip } from '@primer/react' import { Tooltip } from '@primer/react'
import useClipboard from 'components/hooks/useClipboard' import useClipboard from 'components/hooks/useClipboard'
import styles from './CodeBlock.module.scss' import styles from './CodeBlock.module.scss'
import type { ReactNode } from 'react'
type Props = { type Props = {
verb?: string verb?: string
// Only Code samples should have a copy icon - if there's a headingLang it's a code sample headingLang?: ReactNode | string
headingLang?: string
codeBlock: string codeBlock: string
highlight?: string highlight?: string
} }
@@ -20,20 +18,12 @@ export function CodeBlock({ verb, headingLang, codeBlock, highlight }: Props) {
}) })
return ( return (
<div className={headingLang && 'code-extra'}> <div className={headingLang ? 'code-extra' : undefined}>
{/* Only Code samples should have a copy icon
If there's a headingLang it's a code sample */}
{headingLang && ( {headingLang && (
<header className="d-flex flex-justify-between flex-items-center p-2 text-small rounded-top-1 border"> <header className="d-flex flex-justify-between flex-items-center p-2 text-small rounded-top-1 border">
{headingLang === 'JavaScript' ? ( {headingLang}
<span>
{headingLang} (
<a className="text-underline" href="https://github.com/octokit/core.js#readme">
@octokit/core.js
</a>
)
</span>
) : (
`${headingLang}`
)}
<Tooltip direction="w" aria-label={isCopied ? 'Copied!' : 'Copy to clipboard'}> <Tooltip direction="w" aria-label={isCopied ? 'Copied!' : 'Copy to clipboard'}>
<button className="js-btn-copy btn-octicon" onClick={() => setCopied()}> <button className="js-btn-copy btn-octicon" onClick={() => setCopied()}>
{isCopied ? <CheckIcon /> : <CopyIcon />} {isCopied ? <CheckIcon /> : <CopyIcon />}
@@ -44,10 +34,13 @@ export function CodeBlock({ verb, headingLang, codeBlock, highlight }: Props) {
<pre className={cx(styles.codeBlock, 'rounded-1 border')} data-highlight={highlight}> <pre className={cx(styles.codeBlock, 'rounded-1 border')} data-highlight={highlight}>
<code> <code>
{verb && ( {verb && (
<>
<span className="color-bg-accent-emphasis color-fg-on-emphasis rounded-1 text-uppercase p-1"> <span className="color-bg-accent-emphasis color-fg-on-emphasis rounded-1 text-uppercase p-1">
{verb} {verb}
</span> </span>
)}{' '} <> </>
</>
)}
{codeBlock} {codeBlock}
</code> </code>
</pre> </pre>

View File

@@ -1,14 +1,12 @@
import { xGitHub } from './types'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
type Props = { type Props = {
slug: string slug: string
xGitHub: xGitHub numPreviews: number
} }
export function PreviewsRow({ slug, xGitHub }: Props) { export function PreviewsRow({ slug, numPreviews }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
const hasPreviews = xGitHub.previews && xGitHub.previews.length > 0
return ( return (
<tr> <tr>
@@ -21,9 +19,9 @@ export function PreviewsRow({ slug, xGitHub }: Props) {
<p className="m-0"> <p className="m-0">
Setting to Setting to
<code>application/vnd.github.v3+json</code> is recommended. <code>application/vnd.github.v3+json</code> is recommended.
{hasPreviews && ( {numPreviews > 0 && (
<a href={`#${slug}-preview-notices`} className="d-inline"> <a href={`#${slug}-preview-notices`} className="d-inline">
{xGitHub.previews.length > 1 {numPreviews > 1
? ` ${t('rest.reference.see_preview_notices')}` ? ` ${t('rest.reference.see_preview_notices')}`
: ` ${t('rest.reference.see_preview_notice')}`} : ` ${t('rest.reference.see_preview_notice')}`}
</a> </a>

View File

@@ -1,35 +1,92 @@
import type { xCodeSample } from './types' import type { Operation } from './types'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import { CodeBlock } from './CodeBlock' import { CodeBlock } from './CodeBlock'
import { Fragment } from 'react' import { getShellExample, getGHExample, getJSExample } from '../lib/get-rest-code-samples'
type Props = { type Props = {
slug: string slug: string
xCodeSamples: Array<xCodeSample> operation: Operation
} }
export function RestCodeSamples({ slug, xCodeSamples }: Props) { export function RestCodeSamples({ operation, slug }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
const JAVASCRIPT_HEADING = (
<span>
JavaScript{' '}
<a className="text-underline" href="https://github.com/octokit/core.js#readme">
@octokit/core.js
</a>
</span>
)
const GH_CLI_HEADING = (
<span>
GitHub CLI{' '}
<a className="text-underline" href="https://cli.github.com/manual/gh_api">
gh api
</a>
</span>
)
// Format the example properties into different language examples
const languageExamples = operation.codeExamples.map((sample) => {
const languageExamples = {
curl: getShellExample(operation, sample),
javascript: getJSExample(operation, sample),
ghcli: getGHExample(operation, sample),
}
return Object.assign({}, sample, languageExamples)
})
return ( return (
<Fragment key={xCodeSamples + slug}> <>
<h4 id={`${slug}--code-samples`}> <h4 id={`${slug}--code-samples`}>
<a href={`#${slug}--code-samples`}>{`${t('rest.reference.code_samples')}`}</a> <a href={`#${slug}--code-samples`}>{`${t('rest.reference.code_samples')}`}</a>
</h4> </h4>
{xCodeSamples.map((sample, index) => { {languageExamples.map((sample, index) => (
const sampleElements: JSX.Element[] = [] <div key={`${JSON.stringify(sample)}-${index}`}>
if (sample.lang !== 'Ruby') { {/* Example requests */}
sampleElements.push( {sample.request && (
<>
{/* Title of the code sample block */}
<h5 dangerouslySetInnerHTML={{ __html: sample.request.description }} />
{sample.curl && (
<CodeBlock headingLang="Shell" codeBlock={sample.curl} highlight="curl" />
)}
{sample.javascript && (
<CodeBlock <CodeBlock
key={sample.lang + index} headingLang={JAVASCRIPT_HEADING}
headingLang={sample.lang} codeBlock={sample.javascript}
codeBlock={sample.source} highlight="javascript"
highlight={sample.lang === 'JavaScript' ? 'javascript' : 'curl'} />
></CodeBlock> )}
) {sample.ghcli && (
} <CodeBlock headingLang={GH_CLI_HEADING} codeBlock={sample.ghcli} highlight="curl" />
return sampleElements )}
})} </>
</Fragment> )}
{/* Title of the response */}
{sample.response && (
<>
<h5 dangerouslySetInnerHTML={{ __html: sample.response.description }} />
{/* Status code */}
{sample.response.statusCode && (
<CodeBlock codeBlock={`Status: ${sample.response.statusCode}`} />
)}
{/* Example response */}
{sample.response.example && (
<CodeBlock
codeBlock={JSON.stringify(sample.response.example, null, 2)}
highlight="json"
/>
)}
</>
)}
</div>
))}
</>
) )
} }

View File

@@ -1,10 +0,0 @@
import { CodeBlock } from './CodeBlock'
type Props = {
verb: string
requestPath: string
}
export function RestHTTPMethod({ verb, requestPath }: Props) {
return <CodeBlock verb={verb} codeBlock={requestPath}></CodeBlock>
}

View File

@@ -1,25 +1,21 @@
import { useRouter } from 'next/router'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import { Link } from 'components/Link'
type Props = { export function RestNotes() {
notes: Array<string>
enabledForGitHubApps: boolean
}
export function RestNotes({ notes, enabledForGitHubApps }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
const router = useRouter()
return ( return (
<> <>
<h4 className="pt-4">{t('rest.reference.notes')}</h4> <h4 className="pt-4">{t('rest.reference.notes')}</h4>
<ul className="mt-2 pl-3 pb-2"> <ul className="mt-2 pl-3 pb-2">
{enabledForGitHubApps && (
<li> <li>
<a href="/developers/apps">Works with GitHub Apps</a> <Link href={`/${router.locale}/developers/apps`}>
{t('rest.reference.works_with_github_apps')}
</Link>
</li> </li>
)}
{notes.map((note: string) => {
return <li>{note}</li>
})}
</ul> </ul>
</> </>
) )

View File

@@ -1,56 +1,62 @@
import slugger from 'github-slugger'
import { RestOperationHeading } from './RestOperationHeading' import { RestOperationHeading } from './RestOperationHeading'
import { RestHTTPMethod } from './RestHTTPMethod' import { CodeBlock } from './CodeBlock'
import { RestParameterTable } from './RestParameterTable' import { RestParameterTable } from './RestParameterTable'
import { RestCodeSamples } from './RestCodeSamples' import { RestCodeSamples } from './RestCodeSamples'
import { RestResponse } from './RestResponse' import { RestStatusCodes } from './RestStatusCodes'
import { Operation } from './types' import { Operation } from './types'
import { RestNotes } from './RestNotes' import { RestNotes } from './RestNotes'
import { RestPreviewNotice } from './RestPreviewNotice' import { RestPreviewNotice } from './RestPreviewNotice'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import { RestStatusCodes } from './RestStatusCodes'
type Props = { type Props = {
operation: Operation operation: Operation
index: number
} }
export function RestOperation({ operation }: Props) { export function RestOperation({ operation }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
const previews = operation['x-github'].previews
const nonErrorResponses = operation.responses.filter( const slug = slugger.slug(operation.title)
(response) => parseInt(response.httpStatusCode) < 400
) const numPreviews = operation.previews.length
const hasStatusCodes = operation.statusCodes.length > 0
const hasCodeSamples = operation.codeExamples.length > 0
const hasParameters = operation.parameters.length > 0 || operation.bodyParameters.length > 0
return ( return (
<div> <div>
<RestOperationHeading <RestOperationHeading
slug={operation.slug} slug={slug}
summary={operation.summary} title={operation.title}
descriptionHTML={operation.descriptionHTML} descriptionHTML={operation.descriptionHTML}
/> />
<RestHTTPMethod verb={operation.verb} requestPath={operation.requestPath} />
{operation.parameters && ( {operation.requestPath && (
<CodeBlock verb={operation.verb} codeBlock={operation.requestPath}></CodeBlock>
)}
{hasParameters && (
<RestParameterTable <RestParameterTable
slug={operation.slug} slug={slug}
xGitHub={operation['x-github']} numPreviews={numPreviews}
parameters={operation.parameters} parameters={operation.parameters}
bodyParameters={operation.bodyParameters} bodyParameters={operation.bodyParameters}
/> />
)} )}
{operation['x-codeSamples'] && operation['x-codeSamples'].length > 0 && (
<RestCodeSamples slug={operation.slug} xCodeSamples={operation['x-codeSamples']} /> {hasCodeSamples && <RestCodeSamples operation={operation} slug={slug} />}
)}
<RestResponse responses={nonErrorResponses} /> {hasStatusCodes && (
{(operation.notes.length > 0 || operation['x-github'].enabledForGitHubApps) && ( <RestStatusCodes
<RestNotes heading={t('rest.reference.status_codes')}
notes={operation.notes} statusCodes={operation.statusCodes}
enabledForGitHubApps={operation['x-github'].enabledForGitHubApps}
/> />
)} )}
{previews && (
<RestPreviewNotice slug={operation.slug} previews={operation['x-github'].previews} /> {operation.enabledForGitHubApps && <RestNotes />}
)}
<RestStatusCodes heading={t('rest.reference.status_codes')} responses={operation.responses} /> {numPreviews > 0 && <RestPreviewNotice slug={slug} previews={operation.previews} />}
</div> </div>
) )
} }

View File

@@ -2,18 +2,18 @@ import { LinkIcon } from '@primer/octicons-react'
type Props = { type Props = {
slug: string slug: string
summary: string title: string
descriptionHTML: string descriptionHTML: string
} }
export function RestOperationHeading({ slug, summary, descriptionHTML }: Props) { export function RestOperationHeading({ slug, title, descriptionHTML }: Props) {
return ( return (
<> <>
<h3 id={slug}> <h3 id={slug}>
<a href={`#${slug}`}> <a href={`#${slug}`}>
<LinkIcon size={16} className="m-1" /> <LinkIcon size={16} className="m-1" />
</a> </a>
{summary} {title}
</h3> </h3>
<div dangerouslySetInnerHTML={{ __html: descriptionHTML }} /> <div dangerouslySetInnerHTML={{ __html: descriptionHTML }} />
</> </>

View File

@@ -1,6 +1,6 @@
import cx from 'classnames' import cx from 'classnames'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import { BodyParameter, Parameter, xGitHub } from './types' import { BodyParameter, Parameter } from './types'
import styles from './RestParameterTable.module.scss' import styles from './RestParameterTable.module.scss'
import { PreviewsRow } from './PreviewsRow' import { PreviewsRow } from './PreviewsRow'
import { ParameterRows } from './ParameterRows' import { ParameterRows } from './ParameterRows'
@@ -8,12 +8,12 @@ import { BodyParameterRows } from './BodyParametersRows'
type Props = { type Props = {
slug: string slug: string
xGitHub: xGitHub numPreviews: number
parameters: Array<Parameter> parameters: Array<Parameter>
bodyParameters: Array<BodyParameter> bodyParameters: Array<BodyParameter>
} }
export function RestParameterTable({ slug, xGitHub, parameters, bodyParameters }: Props) { export function RestParameterTable({ slug, numPreviews, parameters, bodyParameters }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
return ( return (
@@ -31,7 +31,7 @@ export function RestParameterTable({ slug, xGitHub, parameters, bodyParameters }
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<PreviewsRow slug={slug} xGitHub={xGitHub} /> <PreviewsRow slug={slug} numPreviews={numPreviews} />
<ParameterRows parameters={parameters} /> <ParameterRows parameters={parameters} />
<BodyParameterRows slug={slug} bodyParameters={bodyParameters} /> <BodyParameterRows slug={slug} bodyParameters={bodyParameters} />
</tbody> </tbody>

View File

@@ -1,34 +1,27 @@
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import { Preview } from './types'
type Props = { type Props = {
slug: string slug: string
previews: Array<Preview> | [] previews: Array<string>
} }
export function RestPreviewNotice({ slug, previews }: Props) { export function RestPreviewNotice({ slug, previews }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
const previewNotices = previews.map((preview, index) => {
return ( return (
<div
className="extended-markdown note border rounded-1 mb-6 p-3 color-border-accent-emphasis color-bg-accent f5"
dangerouslySetInnerHTML={{ __html: preview.html }}
key={`${preview.name}-${index}`}
>
{preview.required && t('preview_header_is_required')}
</div>
)
})
return previews.length > 0 ? (
<> <>
<h4 id={`${slug}-preview-notices`}> <h4 id={`${slug}-preview-notices`}>
{previews.length > 1 {previews.length > 1
? `${t('rest.reference.preview_notices')}` ? `${t('rest.reference.preview_notices')}`
: `${t('rest.reference.preview_notice')}`} : `${t('rest.reference.preview_notice')}`}
</h4> </h4>
{previewNotices} {previews.map((preview, index) => (
<div
className="extended-markdown note border rounded-1 mb-6 p-3 color-border-accent-emphasis color-bg-accent f5"
dangerouslySetInnerHTML={{ __html: preview }}
key={JSON.stringify(preview) + index}
/>
))}
</> </>
) : null )
} }

View File

@@ -194,10 +194,13 @@ export const RestReferencePage = ({
</div> </div>
<MarkdownContent> <MarkdownContent>
{subcategories.map((subcategory, index) => ( {subcategories.map((subcategory, index) => (
<div key={`restCategory-${index}`}> <div key={`${subcategory}-${index}`}>
<div dangerouslySetInnerHTML={{ __html: descriptions[subcategory] }} /> <div dangerouslySetInnerHTML={{ __html: descriptions[subcategory] }} />
{restOperations[subcategory].map((operation, index) => ( {restOperations[subcategory].map((operation, index) => (
<RestOperation key={`restOperation-${index}`} operation={operation} index={index} /> <RestOperation
key={`${subcategory}-${operation.title}-${index}`}
operation={operation}
/>
))} ))}
</div> </div>
))} ))}

View File

@@ -1,30 +0,0 @@
import { CodeResponse } from './types'
import { CodeBlock } from './CodeBlock'
type Props = {
responses: Array<CodeResponse>
}
export function RestResponse(props: Props) {
const { responses } = props
if (!responses || responses.length === 0) {
return null
}
return (
<>
{responses.map((response, index) => {
return (
<div key={`${response.httpStatusMessage}-${index}}`}>
<h4 dangerouslySetInnerHTML={{ __html: response.description }} />
<CodeBlock
codeBlock={`Status: ${response.httpStatusCode} ${response.httpStatusMessage}`}
/>
{response.payload ? <CodeBlock codeBlock={response.payload} highlight="json" /> : null}
</div>
)
})}
</>
)
}

View File

@@ -1,14 +1,14 @@
import cx from 'classnames' import cx from 'classnames'
import { CodeResponse } from './types' import { StatusCode } from './types'
import { useTranslation } from 'components/hooks/useTranslation' import { useTranslation } from 'components/hooks/useTranslation'
import styles from './RestResponseTable.module.scss' import styles from './RestResponseTable.module.scss'
type Props = { type Props = {
heading: string heading: string
responses: Array<CodeResponse> statusCodes: Array<StatusCode>
} }
export function RestStatusCodes({ heading, responses }: Props) { export function RestStatusCodes({ heading, statusCodes }: Props) {
const { t } = useTranslation('products') const { t } = useTranslation('products')
return ( return (
@@ -22,21 +22,22 @@ export function RestStatusCodes({ heading, responses }: Props) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{responses.map((response, index) => ( {statusCodes.map((statusCode, index) => {
<tr key={`${response.description}-${index}}`}> return (
<tr key={`${statusCode.description}-${index}}`}>
<td> <td>
<code>{response.httpStatusCode}</code> <code>{statusCode.httpStatusCode}</code>
</td> </td>
<td> <td>
{response.description && {statusCode.description ? (
response.description.toLowerCase() !== '<p>response</p>' ? ( <div dangerouslySetInnerHTML={{ __html: statusCode.description }} />
<div dangerouslySetInnerHTML={{ __html: response.description }} />
) : ( ) : (
response.httpStatusMessage statusCode.httpStatusMessage
)} )}
</td> </td>
</tr> </tr>
))} )
})}
</tbody> </tbody>
</table> </table>
</> </>

View File

@@ -1,15 +1,17 @@
export interface Operation { export interface Operation {
verb: string verb: string
summary: string title: string
slug: string
descriptionHTML: string descriptionHTML: string
notes: Array<string> previews: Array<string>
requestPath: string requestPath: string
responses: Array<CodeResponse> serverUrl: string
statusCodes: Array<StatusCode>
parameters: Array<Parameter> parameters: Array<Parameter>
bodyParameters: Array<BodyParameter> bodyParameters: Array<BodyParameter>
'x-github': xGitHub category: string
'x-codeSamples': Array<xCodeSample> subcategory: string
enabledForGitHubApps: boolean
codeExamples: Array<CodeSample>
} }
export interface Parameter { export interface Parameter {
@@ -23,28 +25,27 @@ export interface Parameter {
} }
} }
export interface xGitHub { export interface StatusCode {
category: string
enabledForGitHubApps: boolean
previews: Array<Preview> | []
}
export interface CodeResponse {
description: string description: string
httpStatusCode: string httpStatusCode: string
httpStatusMessage: string httpStatusMessage: string
payload: string
} }
export interface xCodeSample { export interface CodeSample {
lang: string key: string
source: string response: {
contentType: string
description: string
example: Record<string, string>
statusCode: string
}
request: {
contentType: string
acceptHeader: string
bodyParameters: Record<string, string>
parameters: Record<string, string>
description: string
} }
export interface Preview {
html: string
required: boolean
name: string
} }
export interface BodyParameter { export interface BodyParameter {

View File

@@ -87,7 +87,7 @@ export async function getRestOperationData(category, language, version, context)
// only a string with the raw HTML of each heading level 2 and 3 // only a string with the raw HTML of each heading level 2 and 3
// is needed to generate the toc // is needed to generate the toc
const titles = categoryOperations[subcategory] const titles = categoryOperations[subcategory]
.map((operation) => `### ${operation.summary}\n`) .map((operation) => `### ${operation.title}\n`)
.join('') .join('')
toc += renderedMarkdown + (await renderContent(titles, context)) toc += renderedMarkdown + (await renderContent(titles, context))
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

131
package-lock.json generated
View File

@@ -43,6 +43,7 @@
"hot-shots": "^9.0.0", "hot-shots": "^9.0.0",
"html-entities": "^2.3.2", "html-entities": "^2.3.2",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"javascript-stringify": "^2.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kleur": "4.1.4", "kleur": "4.1.4",
@@ -83,6 +84,7 @@
"ts-dedent": "^2.2.0", "ts-dedent": "^2.2.0",
"unified": "^10.1.0", "unified": "^10.1.0",
"unist-util-visit": "^4.1.0", "unist-util-visit": "^4.1.0",
"url-template": "^3.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"walk-sync": "^3.0.0" "walk-sync": "^3.0.0"
}, },
@@ -135,7 +137,6 @@
"http-status-code": "^2.1.0", "http-status-code": "^2.1.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"japanese-characters": "^1.1.0", "japanese-characters": "^1.1.0",
"javascript-stringify": "^2.1.0",
"jest": "^27.4.7", "jest": "^27.4.7",
"jest-environment-puppeteer": "5.0.4", "jest-environment-puppeteer": "5.0.4",
"jest-fail-on-console": "^2.2.3", "jest-fail-on-console": "^2.2.3",
@@ -160,8 +161,7 @@
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"strip-ansi": "^7.0.1", "strip-ansi": "^7.0.1",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"typescript": "^4.5.5", "typescript": "^4.5.5"
"url-template": "^3.0.0"
}, },
"engines": { "engines": {
"node": "16.x" "node": "16.x"
@@ -7281,13 +7281,13 @@
} }
}, },
"node_modules/crc-32": { "node_modules/crc-32": {
"version": "1.2.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
"integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"exit-on-epipe": "~1.0.1", "exit-on-epipe": "~1.0.1",
"printj": "~1.3.1" "printj": "~1.1.0"
}, },
"bin": { "bin": {
"crc32": "bin/crc32.njs" "crc32": "bin/crc32.njs"
@@ -7296,6 +7296,18 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/crc-32/node_modules/printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"optional": true,
"bin": {
"printj": "bin/printj.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-ecdh": { "node_modules/create-ecdh": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
@@ -8074,6 +8086,7 @@
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@@ -9243,7 +9256,6 @@
"version": "4.17.2", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==", "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.7", "accepts": "~1.3.7",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -9967,12 +9979,12 @@
"dev": true "dev": true
}, },
"node_modules/gifwrap": { "node_modules/gifwrap": {
"version": "0.9.4", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
"integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==", "integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"image-q": "^4.0.0", "image-q": "^1.1.1",
"omggif": "^1.0.10" "omggif": "^1.0.10"
} }
}, },
@@ -11013,20 +11025,14 @@
"dev": true "dev": true
}, },
"node_modules/image-q": { "node_modules/image-q": {
"version": "4.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", "resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", "integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=",
"optional": true, "optional": true,
"dependencies": { "engines": {
"@types/node": "16.9.1" "node": ">=0.9.0"
} }
}, },
"node_modules/image-q/node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"optional": true
},
"node_modules/image-size": { "node_modules/image-size": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
@@ -11807,8 +11813,7 @@
"node_modules/javascript-stringify": { "node_modules/javascript-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
"dev": true
}, },
"node_modules/jest": { "node_modules/jest": {
"version": "27.4.7", "version": "27.4.7",
@@ -16274,7 +16279,6 @@
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz",
"integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==", "integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==",
"deprecated": "Version no longer supported. Upgrade to @latest",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -16578,7 +16582,6 @@
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz",
"integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==", "integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==",
"deprecated": "Version no longer supported. Upgrade to @latest",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -16888,9 +16891,9 @@
} }
}, },
"node_modules/parse-headers": { "node_modules/parse-headers": {
"version": "2.0.5", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==",
"optional": true "optional": true
}, },
"node_modules/parse-json": { "node_modules/parse-json": {
@@ -17296,9 +17299,9 @@
} }
}, },
"node_modules/printj": { "node_modules/printj": {
"version": "1.3.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz",
"integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==", "integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==",
"optional": true, "optional": true,
"bin": { "bin": {
"printj": "bin/printj.njs" "printj": "bin/printj.njs"
@@ -17510,7 +17513,6 @@
"version": "9.1.1", "version": "9.1.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
"integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==", "integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==",
"deprecated": "Version no longer supported. Upgrade to @latest",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -17818,7 +17820,6 @@
"version": "15.5.0", "version": "15.5.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
"integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.3.1", "@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1", "highlight.js": "^10.4.1",
@@ -21442,7 +21443,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz",
"integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g==", "integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g==",
"dev": true,
"engines": { "engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
} }
@@ -28121,13 +28121,21 @@
} }
}, },
"crc-32": { "crc-32": {
"version": "1.2.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
"integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==", "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
"optional": true, "optional": true,
"requires": { "requires": {
"exit-on-epipe": "~1.0.1", "exit-on-epipe": "~1.0.1",
"printj": "~1.3.1" "printj": "~1.1.0"
},
"dependencies": {
"printj": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==",
"optional": true
}
} }
}, },
"create-ecdh": { "create-ecdh": {
@@ -30191,12 +30199,12 @@
"dev": true "dev": true
}, },
"gifwrap": { "gifwrap": {
"version": "0.9.4", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz", "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
"integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==", "integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
"optional": true, "optional": true,
"requires": { "requires": {
"image-q": "^4.0.0", "image-q": "^1.1.1",
"omggif": "^1.0.10" "omggif": "^1.0.10"
} }
}, },
@@ -30982,21 +30990,10 @@
"dev": true "dev": true
}, },
"image-q": { "image-q": {
"version": "4.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", "resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", "integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=",
"optional": true,
"requires": {
"@types/node": "16.9.1"
},
"dependencies": {
"@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"optional": true "optional": true
}
}
}, },
"image-size": { "image-size": {
"version": "1.0.0", "version": "1.0.0",
@@ -31533,8 +31530,7 @@
"javascript-stringify": { "javascript-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
"dev": true
}, },
"jest": { "jest": {
"version": "27.4.7", "version": "27.4.7",
@@ -35374,9 +35370,9 @@
} }
}, },
"parse-headers": { "parse-headers": {
"version": "2.0.5", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==",
"optional": true "optional": true
}, },
"parse-json": { "parse-json": {
@@ -35683,9 +35679,9 @@
} }
}, },
"printj": { "printj": {
"version": "1.3.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz", "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz",
"integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==", "integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==",
"optional": true "optional": true
}, },
"prismjs": { "prismjs": {
@@ -38839,8 +38835,7 @@
"url-template": { "url-template": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz",
"integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g==", "integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g=="
"dev": true
}, },
"use-subscription": { "use-subscription": {
"version": "1.5.1", "version": "1.5.1",

View File

@@ -45,6 +45,7 @@
"hot-shots": "^9.0.0", "hot-shots": "^9.0.0",
"html-entities": "^2.3.2", "html-entities": "^2.3.2",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"javascript-stringify": "^2.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kleur": "4.1.4", "kleur": "4.1.4",
@@ -85,6 +86,7 @@
"ts-dedent": "^2.2.0", "ts-dedent": "^2.2.0",
"unified": "^10.1.0", "unified": "^10.1.0",
"unist-util-visit": "^4.1.0", "unist-util-visit": "^4.1.0",
"url-template": "^3.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"walk-sync": "^3.0.0" "walk-sync": "^3.0.0"
}, },
@@ -137,7 +139,6 @@
"http-status-code": "^2.1.0", "http-status-code": "^2.1.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"japanese-characters": "^1.1.0", "japanese-characters": "^1.1.0",
"javascript-stringify": "^2.1.0",
"jest": "^27.4.7", "jest": "^27.4.7",
"jest-environment-puppeteer": "5.0.4", "jest-environment-puppeteer": "5.0.4",
"jest-fail-on-console": "^2.2.3", "jest-fail-on-console": "^2.2.3",
@@ -162,8 +163,7 @@
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"strip-ansi": "^7.0.1", "strip-ansi": "^7.0.1",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"typescript": "^4.5.5", "typescript": "^4.5.5"
"url-template": "^3.0.0"
}, },
"engines": { "engines": {
"node": "16.x" "node": "16.x"

View File

@@ -173,25 +173,7 @@ async function decorate() {
const operations = await getOperations(schema) const operations = await getOperations(schema)
// process each operation, asynchronously rendering markdown and stuff // process each operation, asynchronously rendering markdown and stuff
await Promise.all(operations.map((operation) => operation.process())) await Promise.all(operations.map((operation) => operation.process()))
const categories = [...new Set(operations.map((operation) => operation.category))].sort()
// Remove any keys not needed in the decorated files
const decoratedOperations = operations.map(
({
tags,
description,
serverUrl,
operationId,
categoryLabel,
subcategoryLabel,
contentType,
externalDocs,
...props
}) => props
)
const categories = [
...new Set(decoratedOperations.map((operation) => operation.category)),
].sort()
// Orders the operations by their category and subcategories. // Orders the operations by their category and subcategories.
// All operations must have a category, but operations don't need // All operations must have a category, but operations don't need
@@ -215,9 +197,7 @@ async function decorate() {
const operationsByCategory = {} const operationsByCategory = {}
categories.forEach((category) => { categories.forEach((category) => {
operationsByCategory[category] = {} operationsByCategory[category] = {}
const categoryOperations = decoratedOperations.filter( const categoryOperations = operations.filter((operation) => operation.category === category)
(operation) => operation.category === category
)
categoryOperations categoryOperations
.filter((operation) => !operation.subcategory) .filter((operation) => !operation.subcategory)
.map((operation) => (operation.subcategory = operation.category)) .map((operation) => (operation.subcategory = operation.category))
@@ -258,7 +238,7 @@ async function decorate() {
// This is a collection of operations that have `enabledForGitHubApps = true` // This is a collection of operations that have `enabledForGitHubApps = true`
// It's grouped by resource title to make rendering easier // It's grouped by resource title to make rendering easier
operationsEnabledForGitHubApps[schemaName][category] = categoryOperations operationsEnabledForGitHubApps[schemaName][category] = categoryOperations
.filter((operation) => operation['x-github'].enabledForGitHubApps) .filter((operation) => operation.enabledForGitHubApps)
.map((operation) => ({ .map((operation) => ({
slug: operation.slug, slug: operation.slug,
verb: operation.verb, verb: operation.verb,

View File

@@ -1,186 +0,0 @@
#!/usr/bin/env node
import { parseTemplate } from 'url-template'
import { stringify } from 'javascript-stringify'
import { get, mapValues, snakeCase } from 'lodash-es'
export default createCodeSamples
const PARAMETER_EXAMPLES = {
owner: 'octocat',
repo: 'hello-world',
email: 'octocat@github.com',
emails: ['octocat@github.com'],
}
function createCodeSamples(operation) {
const route = {
method: operation.verb.toUpperCase(),
path: operation.requestPath,
operation,
}
const serverUrl = operation.serverUrl
const codeSampleParams = { route, serverUrl }
if (
operation.operationId === 'repos/upload-release-asset' &&
Object.prototype.hasOwnProperty.call(operation, 'servers') &&
serverUrl === 'https://api.github.com'
) {
codeSampleParams.serverUrl = operation.servers[0].variables.origin.default
} else if (operation.subcategory && operation.subcategory === 'management-console') {
codeSampleParams.serverUrl = serverUrl.replace('/api/v3', '')
}
return [
{ lang: 'Shell', source: toShellExample(codeSampleParams) },
{ lang: 'JavaScript', source: toJsExample(codeSampleParams) },
]
}
function toShellExample({ route, serverUrl }) {
const pathParams = mapValues(getExamplePathParams(route), (value, paramName) =>
PARAMETER_EXAMPLES[paramName] ? value : snakeCase(value).toUpperCase()
)
const path = parseTemplate(route.path.replace(/:(\w+)/g, '{$1}')).expand(pathParams)
const params = getExampleBodyParams(route)
const { method } = route
const requiredPreview = get(route, 'operation.x-github.previews', []).find(
(preview) => preview.required
)
const defaultAcceptHeader = requiredPreview
? `application/vnd.github.${requiredPreview.name}-preview+json`
: 'application/vnd.github.v3+json'
let requestBodyParams = `-d '${JSON.stringify(params)}'`
// If the content type is application/x-www-form-urlencoded the format of
// the shell example is --data-urlencode param1=value1 --data-urlencode param2=value2
if (route.operation.contentType === 'application/x-www-form-urlencoded') {
requestBodyParams = ''
const paramNames = Object.keys(params)
paramNames.forEach((elem) => {
requestBodyParams = `${requestBodyParams} --data-urlencode ${elem}=${params[elem]}`
})
requestBodyParams = requestBodyParams.trim()
}
const args = [
method !== 'GET' && `-X ${method}`,
defaultAcceptHeader ? `-H "Accept: ${defaultAcceptHeader}"` : '',
`${serverUrl}${path}`,
Object.keys(params).length && requestBodyParams,
].filter(Boolean)
return `curl \\\n ${args.join(' \\\n ')}`
}
function toJsExample({ route }) {
const params = route.operation.parameters
.filter((param) => !param.deprecated)
.filter((param) => param.in !== 'header')
.filter((param) => param.required)
.reduce(
(_params, param) =>
Object.assign(_params, {
[param.name]: getExampleParamValue(param.name, param.schema),
}),
{}
)
Object.assign(params, getExampleBodyParams(route))
// add any required preview headers to the params object
const requiredPreviewNames = get(route.operation, 'x-github.previews', [])
.filter((preview) => preview.required)
.map((preview) => preview.name)
if (requiredPreviewNames.length) {
Object.assign(params, {
mediaType: { previews: requiredPreviewNames },
})
}
// add required content type header (presently only for `POST /markdown/raw`)
const contentTypeHeader = route.operation.parameters.find((param) => {
return param.name.toLowerCase() === 'content-type' && get(param, 'schema.enum')
})
if (contentTypeHeader) {
Object.assign(params, {
headers: { 'content-type': contentTypeHeader.schema.enum[0] },
})
}
const args = Object.keys(params).length ? ', ' + stringify(params, null, 2) : ''
return `await octokit.request('${route.method} ${route.path}'${args})`
}
function getExamplePathParams({ operation }) {
const pathParams = operation.parameters.filter((param) => param.in === 'path')
if (pathParams.length === 0) {
return {}
}
return pathParams.reduce((dict, param) => {
dict[param.name] = getExampleParamValue(param.name, param.schema)
return dict
}, {})
}
function getExampleBodyParams({ operation }) {
const contentType = Object.keys(get(operation, 'requestBody.content', []))[0]
let schema
try {
schema = operation.requestBody.content[contentType].schema
if (!schema.properties) return {}
} catch (noRequestBody) {
return {}
}
if (operation['x-github'].requestBodyParameterName) {
const paramName = operation['x-github'].requestBodyParameterName
return { [paramName]: getExampleParamValue(paramName, schema) }
}
if (schema.oneOf && schema.oneOf[0].type) {
schema = schema.oneOf[0]
} else if (schema.anyOf && schema.anyOf[0].type) {
schema = schema.anyOf[0]
}
const props =
schema.required && schema.required.length > 0
? schema.required
: Object.keys(schema.properties).slice(0, 1)
return props.reduce((dict, propName) => {
const propSchema = schema.properties[propName]
if (!propSchema.deprecated) {
dict[propName] = getExampleParamValue(propName, propSchema)
}
return dict
}, {})
}
function getExampleParamValue(name, schema) {
const value = PARAMETER_EXAMPLES[name]
if (value) {
return value
}
// TODO: figure out the right behavior here
if (schema.oneOf && schema.oneOf[0].type) return getExampleParamValue(name, schema.oneOf[0])
if (schema.anyOf && schema.anyOf[0].type) return getExampleParamValue(name, schema.anyOf[0])
if (!schema.type) return 'any'
if (schema.type.includes('string')) return name
else if (schema.type.includes('boolean')) return true
else if (schema.type.includes('integer')) return 42
else if (schema.type.includes('object')) {
return mapValues(schema.properties, (propSchema, propName) =>
getExampleParamValue(propName, propSchema)
)
} else if (schema.type.includes('array')) {
return [getExampleParamValue(name, schema.items)]
}
throw new Error(`Unknown data type in schema:, ${JSON.stringify(schema, null, 2)}`)
}

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env node
// In the case that there are more than one example requests, and
// no content responses, a request with an example key that matches the
// status code of a response will be matched.
const DEFAULT_EXAMPLE_DESCRIPTION = 'Example'
const DEFAULT_EXAMPLE_KEY = 'default'
const DEFAULT_ACCEPT_HEADER = 'application/vnd.github.v3+json'
// Retrieves request and response examples and attempts to
// merge them to create matching request/response examples
// The key used in the media type `examples` property is
// used to match requests to responses.
export default function getCodeSamples(operation) {
const responseExamples = getResponseExamples(operation)
const requestExamples = getRequestExamples(operation)
return mergeExamples(requestExamples, responseExamples)
}
// Iterates over the larger array or "target" (or if equal requests) to see
// if there are any matches in the smaller array or "source"
// (or if equal responses) that can be added to target array. If a request
// example and response example have matching keys they will be merged into
// an example. If there is more than one key match, the first match will
// be used.
function mergeExamples(requestExamples, responseExamples) {
// There is always at least one request example, but it won't create
// a meaningful example unless it has a response example.
if (requestExamples.length === 1 && responseExamples.length === 0) {
return []
}
// If there is one request and one response example, we don't
// need to merge the requests and responses, and we don't need
// to match keys directly. This allows falling back in the
// case that the existing OpenAPI schema has mismatched example keys.
if (requestExamples.length === 1 && responseExamples.length === 1) {
return [{ ...requestExamples[0], ...responseExamples[0] }]
}
// If there is a request with no request body parameters and all of
// the responses have no content, then we can create a docs
// example for just status codes below 300. All other status codes will
// be listed in the status code table in the docs.
if (
requestExamples.length === 1 &&
responseExamples.length > 1 &&
!responseExamples.find((ex) => ex.response.example)
) {
return responseExamples
.filter((resp) => parseInt(resp.response.statusCode, 10) < 300)
.map((ex) => ({ ...requestExamples[0], ...ex }))
}
// If there is exactly one request example and one or more response
// examples, we can make a docs example for the response examples that
// have content. All remaining status codes with no content
// will be listed in the status code table in the docs.
if (
requestExamples.length === 1 &&
responseExamples.length > 1 &&
responseExamples.filter((ex) => ex.response.example).length >= 1
) {
return responseExamples
.filter((ex) => ex.response.example)
.map((ex) => ({ ...requestExamples[0], ...ex }))
}
// Finally, we'll attempt to match examples with matching keys.
// This iterates through the longer array and compares key values to keys in
// the shorter array.
const requestsExamplesLarger = requestExamples.length >= responseExamples.length
const target = requestsExamplesLarger ? requestExamples : responseExamples
const source = requestsExamplesLarger ? responseExamples : requestExamples
return target.filter((targetEx) => {
const match = source.find((srcEx) => srcEx.key === targetEx.key)
if (match) return Object.assign(targetEx, match)
return false
})
}
/*
Create an example object for each example in the requestBody property
of the schema. Each requestBody can have more than one content type.
Each content type can have more than one example. We create an object
for each permutation of content type and example.
Returns an array of objects in the format:
{
key,
request: {
contentType,
description,
acceptHeader,
bodyParameters,
parameters,
}
}
*/
export function getRequestExamples(operation) {
const requestExamples = []
const parameterExamples = getParameterExamples(operation)
// When no request body or parameters are defined, we create a generic
// request example. Not all operations have request bodies or parameters,
// but we always want to show at least an example with the path.
if (!operation.requestBody && Object.keys(parameterExamples).length === 0) {
return [
{
key: DEFAULT_EXAMPLE_KEY,
request: {
description: DEFAULT_EXAMPLE_DESCRIPTION,
acceptHeader: DEFAULT_ACCEPT_HEADER,
},
},
]
}
// When no request body exists, we create an example from the parameters
if (!operation.requestBody) {
return Object.keys(parameterExamples).map((key) => {
return {
key,
request: {
description: DEFAULT_EXAMPLE_DESCRIPTION,
acceptHeader: DEFAULT_ACCEPT_HEADER,
parameters: parameterExamples[key] || parameterExamples.default,
},
}
})
}
// Requests can have multiple content types each with their own set of
// examples.
Object.keys(operation.requestBody.content).forEach((contentType) => {
let examples = {}
// This is a fallback to allow using the `example` property in
// the schema. If we start to enforce using examples vs. example using
// a linter, we can remove the check for `example`.
// For now, we'll use the key default, which is a common default
// example name in the OpenAPI schema.
if (operation.requestBody.content[contentType].example) {
examples = {
default: {
value: operation.requestBody.content[contentType].example,
},
}
} else if (operation.requestBody.content[contentType].examples) {
examples = operation.requestBody.content[contentType].examples
} else {
// Example for this content type doesn't exist so we'll try and create one
requestExamples.push({
key: DEFAULT_EXAMPLE_KEY,
request: {
contentType,
description: DEFAULT_EXAMPLE_DESCRIPTION,
acceptHeader: DEFAULT_ACCEPT_HEADER,
parameters: parameterExamples.default,
},
})
return
}
// There can be more than one example for a given content type. We need to
// iterate over the keys of the examples to create individual
// example objects
Object.keys(examples).forEach((key) => {
// A content type that includes `+json` is a custom media type
// The default accept header is application/vnd.github.v3+json
// Which would have a content type of `application/json`
const acceptHeader = contentType.includes('+json')
? contentType
: 'application/vnd.github.v3+json'
const example = {
key,
request: {
contentType,
description: examples[key].summary || DEFAULT_EXAMPLE_DESCRIPTION,
acceptHeader,
bodyParameters: examples[key].value,
parameters: parameterExamples[key] || parameterExamples.default,
},
}
requestExamples.push(example)
})
})
return requestExamples
}
/*
Create an example object for each example in the response property
of the schema. Each response can have more than one status code,
each with more than one content type. And each content type can
have more than one example. We create an object
for each permutation of status, content type, and example.
Returns an array of objects in the format:
{
key,
response: {
statusCode,
contentType,
description,
example,
}
}
*/
export function getResponseExamples(operation) {
const responseExamples = []
Object.keys(operation.responses).forEach((statusCode) => {
// We don't want to create examples for error codes
// Error codes are displayed in the status table in the docs
if (parseInt(statusCode, 10) >= 400) return
const content = operation.responses[statusCode].content
// A response doesn't always have content (ex:, status 304)
// In this case we create a generic example for the status code
// with a key that matches the status code.
if (!content) {
const example = {
key: statusCode,
response: {
statusCode,
description: operation.responses[statusCode].description,
},
}
responseExamples.push(example)
return
}
// Responses can have multiple content types each with their own set of
// examples.
Object.keys(content).forEach((contentType) => {
let examples = {}
// This is a fallback to allow using the `example` property in
// the schema. If we start to enforce using examples vs. example using
// a linter, we can remove the check for `example`.
// For now, we'll use the key default, which is a common default
// example name in the OpenAPI schema.
if (operation.responses[statusCode].content[contentType].example) {
examples = {
default: {
value: operation.responses[statusCode].content[contentType].example,
},
}
} else if (operation.responses[statusCode].content[contentType].examples) {
examples = operation.responses[statusCode].content[contentType].examples
} else if (parseInt(statusCode, 10) < 300) {
// Sometimes there are missing examples for say a 200 response and
// the operation also has a 304 no content status. If we don't add
// the 200 response example, even though it has not example response,
// the resulting responseExamples would only contain the 304 response.
// That would be confusing in the docs because it's expected to see the
// common or success responses by default.
const example = {
key: statusCode,
response: {
statusCode,
description: operation.responses[statusCode].description,
},
}
responseExamples.push(example)
return
} else {
// We could also check if there is a fully populated example
// directly in the response schema examples properties.
// Example for this content type doesn't exist
return
}
// There can be more than one example for a given content type. We need to
// iterate over the keys of the examples to create individual
// example objects
Object.keys(examples).forEach((key) => {
const example = {
key,
response: {
statusCode,
contentType,
description: examples[key].summary || operation.responses[statusCode].description,
example: examples[key].value,
// TODO adding the schema quadruples the JSON file size. Changing
// how we write the JSON file helps a lot, but we should revisit
// adding the response schema to ensure we have a way to view the
// prettified JSON before minimizing it.
// schema: operation.responses[statusCode].content[contentType].schema,
},
}
responseExamples.push(example)
})
})
})
return responseExamples
}
/*
Path parameters can have more than one example key. We need to create
an example for each and then choose the most appropriate example when
we merge requests with responses.
Parameter examples are in the format:
{
[parameter key]: {
[parameter name]: value
}
}
*/
export function getParameterExamples(operation) {
if (!operation.parameters) {
return {}
}
const parameters = operation.parameters.filter((param) => param.in === 'path')
const parameterExamples = {}
parameters.forEach((parameter) => {
const examples = parameter.examples
// If there are no examples, create an example from the uppercase parameter
// name, so that it is more visible that the value is fake data
// in the route path.
if (!examples) {
if (!parameterExamples.default) parameterExamples.default = {}
parameterExamples.default[parameter.name] = parameter.name.toUpperCase()
} else {
Object.keys(examples).forEach((key) => {
if (!parameterExamples[key]) parameterExamples[key] = {}
parameterExamples[key][parameter.name] = examples[key].value
})
}
})
return parameterExamples
}

View File

@@ -9,9 +9,8 @@ export default async function getOperations(schema) {
const operations = [] const operations = []
for (const [requestPath, operationsAtPath] of Object.entries(schema.paths)) { for (const [requestPath, operationsAtPath] of Object.entries(schema.paths)) {
for (const [verb, props] of Object.entries(operationsAtPath)) { for (const [verb, operationProps] of Object.entries(operationsAtPath)) {
const serverUrl = schema.servers[0].url.replace('{protocol}', 'http(s)') const operation = new Operation(verb, requestPath, operationProps, schema.servers)
const operation = new Operation(verb, requestPath, props, serverUrl)
operations.push(operation) operations.push(operation)
} }
} }

View File

@@ -3,45 +3,21 @@
export default { export default {
type: 'object', type: 'object',
// Every operation must have these props
required: [ required: [
'title',
'verb', 'verb',
'requestPath', 'requestPath',
'parameters',
'responses',
'slug',
'x-codeSamples',
'category', 'category',
'categoryLabel', 'parameters',
'statusCodes',
'codeExamples',
], ],
properties: { properties: {
// Properties from the source OpenAPI schema that this module depends on // Properties from the source OpenAPI schema that this module depends on
externalDocs: { title: {
description: 'The public documentation for the given operation', description: 'The title of the operation',
type: 'object',
required: ['description', 'url'],
properties: {
description: {
type: 'string', type: 'string',
}, },
url: {
type: 'string',
},
},
},
operationId: {
type: 'string',
minLength: 1,
},
parameters: {
description:
'Parameters to the operation that can be present in the URL path, the query, headers, or a POST body',
type: 'array',
},
// Additional derived properties not found in the source OpenAPI schema
verb: { verb: {
description: 'The HTTP method', description: 'The HTTP method',
type: 'string', type: 'string',
@@ -56,28 +32,37 @@ export default {
description: 'The rendered HTML version of the markdown `description` property', description: 'The rendered HTML version of the markdown `description` property',
type: 'string', type: 'string',
}, },
notes: {
type: 'array',
},
slug: {
description: 'GitHub.com-style param-case property for use as a unique DOM id',
type: 'string',
},
category: { category: {
description: 'the `issues` in `/v3/issues/events/`; supports legacy developer site URLs', description: 'the `issues` in `/v3/issues/events/`; supports legacy developer site URLs',
type: 'string', type: 'string',
}, },
categoryLabel: {
description: 'humanized form of category',
type: 'string',
},
subcategory: { subcategory: {
description: 'the `events` in `/v3/issues/events/`; supports legacy developer site URLs', description: 'the `events` in `/v3/issues/events/`; supports legacy developer site URLs',
type: 'string', type: 'string',
}, },
subcategoryLabel: { parameters: {
description: 'humanized form of subcategory', description: 'Parameters to the operation that can be present in the URL path or query',
type: 'string', type: 'array',
},
codeSamples: {
description: 'Code samples for the operation',
type: 'array',
},
statusCodes: {
description: 'The possible HTTP status codes for the operation',
type: 'array',
},
previews: {
description: 'The information about the preview headers',
type: 'array',
},
enabledForGitHubApps: {
description: 'Whether the operation is enabled for server-to-server GitHub Apps',
type: 'boolean',
},
bodyParameters: {
description: 'The request body parameters for the operation',
type: 'array',
}, },
}, },
} }

View File

@@ -1,206 +1,172 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import { get, flatten, isPlainObject } from 'lodash-es' import { get, flatten, isPlainObject } from 'lodash-es'
import { sentenceCase } from 'change-case'
import GitHubSlugger from 'github-slugger' import GitHubSlugger from 'github-slugger'
import httpStatusCodes from 'http-status-code' import httpStatusCodes from 'http-status-code'
import renderContent from '../../../lib/render-content/index.js' import renderContent from '../../../lib/render-content/index.js'
import createCodeSamples from './create-code-samples.js' import getCodeSamples from './create-rest-examples.js'
import Ajv from 'ajv' import Ajv from 'ajv'
import operationSchema from './operation-schema.js' import operationSchema from './operation-schema.js'
import { parseTemplate } from 'url-template'
const overrideOperations = JSON.parse( const overrideOperations = JSON.parse(
await readFile('script/rest/utils/rest-api-overrides.json', 'utf8') await readFile('script/rest/utils/rest-api-overrides.json', 'utf8')
) )
const slugger = new GitHubSlugger() const slugger = new GitHubSlugger()
// titles that can't be derived by sentence-casing the ID
const categoryTitles = { scim: 'SCIM' }
export default class Operation { export default class Operation {
constructor(verb, requestPath, props, serverUrl) { #operation
const defaultProps = { constructor(verb, requestPath, operation, globalServers) {
parameters: [], this.#operation = operation
'x-codeSamples': [], // The global server object sets metadata including the base url for
responses: {}, // all operations in a version. Individual operations can override
// the global server url at the operation level.
this.serverUrl = operation.servers ? operation.servers[0].url : globalServers[0].url
const serverVariables = operation.servers
? operation.servers[0].variables
: globalServers[0].variables
if (serverVariables) {
const templateVariables = {}
Object.keys(serverVariables).forEach(
(key) => (templateVariables[key] = serverVariables[key].default)
)
this.serverUrl = parseTemplate(this.serverUrl).expand(templateVariables)
} }
Object.assign(this, { verb, requestPath, serverUrl }, defaultProps, props) this.serverUrl = this.serverUrl.replace('http:', 'http(s):')
this.serverUrlOverride()
slugger.reset() // Attach some global properties to the operation object to use
this.slug = slugger.slug(this.summary) // during processing
this.#operation.serverUrl = this.serverUrl
// Add category this.#operation.requestPath = requestPath
this.#operation.verb = verb
// workaround for misnamed `code-scanning.` category bug
// https://github.com/github/rest-api-description/issues/38
this['x-github'].category = this['x-github'].category.replace('.', '')
// A temporary override file allows us to override the category defined in
// the openapi schema. Without it, we'd have to update several
// @documentation_urls in the github/github code every time we move
// an endpoint to a new page.
this.category = overrideOperations[this.operationId]
? overrideOperations[this.operationId].category
: this['x-github'].category
this.categoryLabel = categoryTitles[this.category] || sentenceCase(this.category)
// Removing since we don't need this in the decorated files
delete this['x-github'].githubCloudOnly
// Add subcategory
// A temporary override file allows us to override the subcategory
// defined in the openapi schema. Without it, we'd have to update several
// @documentation_urls in the github/github code every time we move
// an endpoint to a new page.
if (overrideOperations[this.operationId]) {
if (overrideOperations[this.operationId].subcategory) {
this.subcategory = overrideOperations[this.operationId].subcategory
this.subcategoryLabel = sentenceCase(this.subcategory)
}
} else if (this['x-github'].subcategory) {
this.subcategory = this['x-github'].subcategory
this.subcategoryLabel = sentenceCase(this.subcategory)
}
// Add content type. We only display one example and default
// to the first example defined.
const contentTypes = Object.keys(get(this, 'requestBody.content', []))
this.contentType = contentTypes[0]
this.verb = verb
this.requestPath = requestPath
this.title = operation.summary
this.setCategories()
this.parameters = operation.parameters || []
this.bodyParameters = []
this.enabledForGitHubApps = operation['x-github'].enabledForGitHubApps
this.codeExamples = getCodeSamples(this.#operation)
return this return this
} }
get schema() { setCategories() {
return operationSchema const operationId = this.#operation.operationId
const xGithub = this.#operation['x-github']
// Set category
// A temporary override file allows us to override the category defined in
// the openapi schema. Without it, we'd have to update several
// @documentation_urls in the api code every time we move
// an endpoint to a new page.
this.category = overrideOperations[operationId]
? overrideOperations[operationId].category
: xGithub.category
// Set subcategory
// A temporary override file allows us to override the subcategory
// defined in the openapi schema. Without it, we'd have to update several
// @documentation_urls in the api code every time we move
// an endpoint to a new page.
if (overrideOperations[operationId]) {
if (overrideOperations[operationId].subcategory) {
this.subcategory = overrideOperations[operationId].subcategory
}
} else if (xGithub.subcategory) {
this.subcategory = xGithub.subcategory
}
}
serverUrlOverride() {
// TODO - remove this once github pull #214649
// lands in this repo's lib/rest/static/dereferenced directory
if (
this.#operation['x-github'].subcategory &&
this.#operation['x-github'].subcategory === 'management-console'
) {
this.serverUrl = this.serverUrl.replace('/api/v3', '')
}
} }
async process() { async process() {
this['x-codeSamples'] = createCodeSamples(this)
await Promise.all([ await Promise.all([
this.renderDescription(), this.renderDescription(),
this.renderResponses(), this.renderStatusCodes(),
this.renderParameterDescriptions(), this.renderParameterDescriptions(),
this.renderBodyParameterDescriptions(), this.renderBodyParameterDescriptions(),
this.renderPreviewNotes(), this.renderPreviewNotes(),
this.renderNotes(),
]) ])
const ajv = new Ajv() const ajv = new Ajv()
const valid = ajv.validate(this.schema, this) const valid = ajv.validate(operationSchema, this)
if (!valid) { if (!valid) {
console.error(JSON.stringify(ajv.errors, null, 2)) console.error(JSON.stringify(ajv.errors, null, 2))
throw new Error('Invalid operation found') throw new Error('Invalid OpenAPI operation found')
} }
} }
async renderDescription() { async renderDescription() {
this.descriptionHTML = await renderContent(this.description) this.descriptionHTML = await renderContent(this.#operation.description)
return this return this
} }
async renderResponses() { async renderStatusCodes() {
// clone and delete this.responses so we can turn it into a clean array of objects const responses = this.#operation.responses
const rawResponses = JSON.parse(JSON.stringify(this.responses)) const responseKeys = Object.keys(responses)
delete this.responses if (responseKeys.length === 0) return []
this.responses = await Promise.all( this.statusCodes = await Promise.all(
Object.keys(rawResponses).map(async (responseCode) => { responseKeys.map(async (responseCode) => {
const rawResponse = rawResponses[responseCode] const response = responses[responseCode]
const httpStatusCode = responseCode const httpStatusCode = responseCode
const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode)) const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode), 'HTTP/2')
const responseDescription = await renderContent(rawResponse.description) // The OpenAPI should be updated to provide better descriptions, but
// until then, we can catch some known generic descriptions and replace
// them with the default http status message.
const responseDescription =
response.description.toLowerCase() === 'response'
? await renderContent(httpStatusMessage)
: await renderContent(response.description)
const cleanResponses = [] return {
/* Responses can have zero, one, or multiple examples. The `examples`
* property often only contains one example object. Both the `example`
* and `examples` properties can be used in the OpenAPI but `example`
* doesn't work with `$ref`.
* This works:
* schema:
* '$ref': '../../components/schemas/foo.yaml'
* example:
* id: 10
* description: This is a summary
* foo: bar
*
* This doesn't
* schema:
* '$ref': '../../components/schemas/foo.yaml'
* example:
* '$ref': '../../components/examples/bar.yaml'
*/
const examplesProperty = get(rawResponse, 'content.application/json.examples')
const exampleProperty = get(rawResponse, 'content.application/json.example')
// Return early if the response doesn't have an example payload
if (!exampleProperty && !examplesProperty) {
return [
{
httpStatusCode, httpStatusCode,
httpStatusMessage,
description: responseDescription, description: responseDescription,
},
]
} }
// Use the same format for `example` as `examples` property so that all
// examples can be handled the same way.
const normalizedExampleProperty = {
default: {
value: exampleProperty,
},
}
const rawExamples = examplesProperty || normalizedExampleProperty
const rawExampleKeys = Object.keys(rawExamples)
for (const exampleKey of rawExampleKeys) {
const exampleValue = rawExamples[exampleKey].value
const exampleSummary = rawExamples[exampleKey].summary
const cleanResponse = {
httpStatusCode,
httpStatusMessage,
}
// If there is only one example, use the response description
// property. For cases with more than one example, some don't have
// summary properties with a description, so we can sentence case
// the property name as a fallback
cleanResponse.description =
rawExampleKeys.length === 1
? exampleSummary || responseDescription
: exampleSummary || sentenceCase(exampleKey)
cleanResponse.payload = JSON.stringify(exampleValue, null, 2)
cleanResponses.push(cleanResponse)
}
return cleanResponses
}) })
) )
// flatten child arrays
this.responses = flatten(this.responses)
} }
async renderParameterDescriptions() { async renderParameterDescriptions() {
return Promise.all( return Promise.all(
this.parameters.map(async (param) => { this.parameters.map(async (param) => {
param.descriptionHTML = await renderContent(param.description) param.descriptionHtml = await renderContent(param.description)
delete param.description
return param return param
}) })
) )
} }
async renderBodyParameterDescriptions() { async renderBodyParameterDescriptions() {
if (!this.#operation.requestBody) return []
const contentType = Object.keys(this.#operation.requestBody.content)[0]
let bodyParamsObject = get( let bodyParamsObject = get(
this, this.#operation,
`requestBody.content.${this.contentType}.schema.properties`, `requestBody.content.${contentType}.schema.properties`,
{} {}
) )
let requiredParams = get(this, `requestBody.content.${this.contentType}.schema.required`, []) let requiredParams = get(
const oneOfObject = get(this, `requestBody.content.${this.contentType}.schema.oneOf`, undefined) this.#operation,
`requestBody.content.${contentType}.schema.required`,
[]
)
const oneOfObject = get(
this.#operation,
`requestBody.content.${contentType}.schema.oneOf`,
undefined
)
// oneOf is an array of input parameter options, so we need to either // oneOf is an array of input parameter options, so we need to either
// use the first option or munge the options together. // use the first option or munge the options together.
@@ -211,8 +177,8 @@ export default class Operation {
// TODO: Remove this check // TODO: Remove this check
// This operation shouldn't have a oneOf in this case, it needs to be // This operation shouldn't have a oneOf in this case, it needs to be
// removed from the schema in the github/github repo. // removed from the schema in the openapi schema repo.
if (this.operationId === 'checks/create') { if (this.#operation.operationId === 'checks/create') {
delete bodyParamsObject.oneOf delete bodyParamsObject.oneOf
} else if (allOneOfAreObjects) { } else if (allOneOfAreObjects) {
// When all of the oneOf objects have the `type: object` we // When all of the oneOf objects have the `type: object` we
@@ -233,14 +199,12 @@ export default class Operation {
requiredParams = firstOneOfObject.required requiredParams = firstOneOfObject.required
} }
} }
this.bodyParameters = await getBodyParams(bodyParamsObject, requiredParams) this.bodyParameters = await getBodyParams(bodyParamsObject, requiredParams)
} }
async renderPreviewNotes() { async renderPreviewNotes() {
const previews = get(this, 'x-github.previews', []).filter((preview) => preview.note) const previews = get(this.#operation, 'x-github.previews', [])
this.previews = await Promise.all(
return Promise.all(
previews.map(async (preview) => { previews.map(async (preview) => {
const note = preview.note const note = preview.note
// remove extra leading and trailing newlines // remove extra leading and trailing newlines
@@ -253,18 +217,10 @@ export default class Operation {
// example: This is the description.\n\n`application/vnd.github.machine-man-preview+json` // example: This is the description.\n\n`application/vnd.github.machine-man-preview+json`
.replace(/\n`application/, '\n```\napplication') .replace(/\n`application/, '\n```\napplication')
.replace(/json`$/, 'json\n```') .replace(/json`$/, 'json\n```')
preview.html = await renderContent(note) return await renderContent(note)
delete preview.note
}) })
) )
} }
// add additional notes to this array whenever we want
async renderNotes() {
this.notes = []
return Promise.all(this.notes.map(async (note) => renderContent(note)))
}
} }
// need to use this function recursively to get child and grandchild params // need to use this function recursively to get child and grandchild params

View File

@@ -7,6 +7,8 @@ import { allVersions } from '../../lib/all-versions.js'
describe('REST references docs', () => { describe('REST references docs', () => {
jest.setTimeout(3 * 60 * 1000) jest.setTimeout(3 * 60 * 1000)
// Checks that every version of the /rest/references/checks
// page has every operation defined in the openapi schema.
test('loads schema data for all versions', async () => { test('loads schema data for all versions', async () => {
for (const version in allVersions) { for (const version in allVersions) {
const checksRestOperations = getRest(version, 'checks') const checksRestOperations = getRest(version, 'checks')
@@ -21,6 +23,10 @@ describe('REST references docs', () => {
} }
}) })
// Checks every version of the
// /rest/overview/endpoints-available-for-github-apps page
// and ensures that all sections in the openapi schema
// are present in the page.
test('loads operations enabled for GitHub Apps', async () => { test('loads operations enabled for GitHub Apps', async () => {
const enableForApps = await getEnabledForApps() const enableForApps = await getEnabledForApps()

View File

@@ -2,10 +2,9 @@ import fs from 'fs/promises'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import path from 'path' import path from 'path'
import dedent from 'dedent'
import { describe } from '@jest/globals' import { describe } from '@jest/globals'
import walk from 'walk-sync' import walk from 'walk-sync'
import { get, isPlainObject, difference } from 'lodash-es' import { isPlainObject, difference } from 'lodash-es'
import { allVersions } from '../../lib/all-versions.js' import { allVersions } from '../../lib/all-versions.js'
import getRest from '../../lib/rest/index.js' import getRest from '../../lib/rest/index.js'
@@ -77,7 +76,6 @@ describe('OpenAPI schema validation', () => {
}) })
}) })
// remove?
test('operations object structure organized by version, category, and subcategory', async () => { test('operations object structure organized by version, category, and subcategory', async () => {
for (const version in allVersions) { for (const version in allVersions) {
const operations = await getFlatListOfOperations(version) const operations = await getFlatListOfOperations(version)
@@ -103,151 +101,26 @@ async function findOperation(version, method, path) {
}) })
} }
describe('x-codeSamples for curl', () => { describe('code examples are defined', () => {
test('GET', async () => { test('GET', async () => {
for (const version in allVersions) { for (const version in allVersions) {
if (version === 'enterprise-server@3.2' || version === 'enterprise-server@3.1') continue
let domain = 'https://api.github.com' let domain = 'https://api.github.com'
if (version.includes('enterprise-server')) { if (version.includes('enterprise-server')) {
domain = 'http(s)://{hostname}/api/v3' domain = 'http(s)://HOSTNAME/api/v3'
} else if (version === 'github-ae@latest') { } else if (version === 'github-ae@latest') {
domain = 'https://{hostname}/api/v3' domain = 'https://HOSTNAME/api/v3'
} }
const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}') const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}')
expect(operation.serverUrl).toBe(domain)
expect(isPlainObject(operation)).toBe(true) expect(isPlainObject(operation)).toBe(true)
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'Shell') expect(operation.codeExamples).toBeDefined()
const expected = operation.codeExamples.forEach((example) => {
'curl \\\n' + expect(isPlainObject(example.request)).toBe(true)
' -H "Accept: application/vnd.github.v3+json" \\\n' + expect(isPlainObject(example.response)).toBe(true)
` ${domain}/repos/octocat/hello-world`
expect(source).toEqual(expected)
}
}) })
test('operations with required preview headers match Shell examples', async () => {
for (const version in allVersions) {
const allOperations = await getFlatListOfOperations(version)
const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => {
const previews = get(operation, 'x-github.previews', [])
return previews.some((preview) => preview.required)
})
const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter(
(operation) => {
const { source: codeSample } = operation['x-codeSamples'].find(
(sample) => sample.lang === 'Shell'
)
return (
codeSample.includes('-H "Accept: application/vnd.github') &&
!codeSample.includes('application/vnd.github.v3+json')
)
}
)
expect(operationsWithRequiredPreviewHeaders.length).toEqual(
operationsWithHeadersInCodeSample.length
)
}
})
})
describe('x-codeSamples for @octokit/core.js', () => {
test('GET', async () => {
for (const version in allVersions) {
const operation = await findOperation(version, 'GET', '/repos/{owner}/{repo}')
expect(isPlainObject(operation)).toBe(true)
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript')
const plainText = source.replace(/<[^>]+>/g, '').trim()
const expected = dedent`await octokit.request('GET /repos/{owner}/{repo}', {
owner: 'octocat',
repo: 'hello-world'
})`
expect(plainText).toEqual(expected)
}
})
test('POST', async () => {
for (const version in allVersions) {
const operation = await findOperation(version, 'POST', '/repos/{owner}/{repo}/git/trees')
expect(isPlainObject(operation)).toBe(true)
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript')
const plainText = source.replace(/<[^>]+>/g, '').trim()
const expected = dedent`await octokit.request('POST /repos/{owner}/{repo}/git/trees', {
owner: 'octocat',
repo: 'hello-world',
tree: [
{
path: 'path',
mode: 'mode',
type: 'type',
sha: 'sha',
content: 'content'
}
]
})`
expect(plainText).toEqual(expected)
}
})
test('PUT', async () => {
const operation = await findOperation(
'free-pro-team@latest',
'PUT',
'/authorizations/clients/{client_id}/{fingerprint}'
)
expect(isPlainObject(operation)).toBe(true)
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript')
const plainText = source.replace(/<[^>]+>/g, '').trim()
const expected = dedent`await octokit.request('PUT /authorizations/clients/{client_id}/{fingerprint}', {
client_id: 'client_id',
fingerprint: 'fingerprint',
client_secret: 'client_secret'
})`
expect(plainText).toEqual(expected)
})
test('operations with required preview headers match JavaScript examples', async () => {
for (const version in allVersions) {
const allOperations = await getFlatListOfOperations(version)
const operationsWithRequiredPreviewHeaders = allOperations.filter((operation) => {
const previews = get(operation, 'x-github.previews', [])
return previews.some((preview) => preview.required)
})
// Find something that looks like the following in each code sample:
/*
mediaType: {
previews: [
'machine-man'
]
}
*/
const operationsWithHeadersInCodeSample = operationsWithRequiredPreviewHeaders.filter(
(operation) => {
const { source: codeSample } = operation['x-codeSamples'].find(
(sample) => sample.lang === 'JavaScript'
)
return codeSample.match(/mediaType: \{\s+previews: /g)
}
)
expect(operationsWithRequiredPreviewHeaders.length).toEqual(
operationsWithHeadersInCodeSample.length
)
}
})
// skipped because the definition is current missing the `content-type` parameter
// GitHub GitHub issue: 155943
test.skip('operation with content-type parameter', async () => {
for (const version in allVersions) {
const operation = await findOperation(version, 'POST', '/markdown/raw')
expect(isPlainObject(operation)).toBe(true)
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'JavaScript')
const expected = dedent`await octokit.request('POST /markdown/raw', {
data: 'data',
headers: {
'content-type': 'text/plain; charset=utf-8'
}
})`
expect(source).toEqual(expected)
} }
}) })
}) })