render existing openapi examples (#26405)
This commit is contained in:
123
components/lib/get-rest-code-samples.ts
Normal file
123
components/lib/get-rest-code-samples.ts
Normal 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)})`
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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
|
||||||
export interface Preview {
|
example: Record<string, string>
|
||||||
html: string
|
statusCode: string
|
||||||
required: boolean
|
}
|
||||||
name: string
|
request: {
|
||||||
|
contentType: string
|
||||||
|
acceptHeader: string
|
||||||
|
bodyParameters: Record<string, string>
|
||||||
|
parameters: Record<string, string>
|
||||||
|
description: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BodyParameter {
|
export interface BodyParameter {
|
||||||
|
|||||||
@@ -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
131
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)}`)
|
|
||||||
}
|
|
||||||
331
script/rest/utils/create-rest-examples.js
Normal file
331
script/rest/utils/create-rest-examples.js
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user