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 { CheckIcon, CopyIcon } from '@primer/octicons-react'
|
||||
import { Tooltip } from '@primer/react'
|
||||
|
||||
import useClipboard from 'components/hooks/useClipboard'
|
||||
|
||||
import styles from './CodeBlock.module.scss'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
verb?: string
|
||||
// Only Code samples should have a copy icon - if there's a headingLang it's a code sample
|
||||
headingLang?: string
|
||||
headingLang?: ReactNode | string
|
||||
codeBlock: string
|
||||
highlight?: string
|
||||
}
|
||||
@@ -20,20 +18,12 @@ export function CodeBlock({ verb, headingLang, codeBlock, highlight }: Props) {
|
||||
})
|
||||
|
||||
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 && (
|
||||
<header className="d-flex flex-justify-between flex-items-center p-2 text-small rounded-top-1 border">
|
||||
{headingLang === 'JavaScript' ? (
|
||||
<span>
|
||||
{headingLang} (
|
||||
<a className="text-underline" href="https://github.com/octokit/core.js#readme">
|
||||
@octokit/core.js
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
) : (
|
||||
`${headingLang}`
|
||||
)}
|
||||
{headingLang}
|
||||
<Tooltip direction="w" aria-label={isCopied ? 'Copied!' : 'Copy to clipboard'}>
|
||||
<button className="js-btn-copy btn-octicon" onClick={() => setCopied()}>
|
||||
{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}>
|
||||
<code>
|
||||
{verb && (
|
||||
<>
|
||||
<span className="color-bg-accent-emphasis color-fg-on-emphasis rounded-1 text-uppercase p-1">
|
||||
{verb}
|
||||
</span>
|
||||
)}{' '}
|
||||
<> </>
|
||||
</>
|
||||
)}
|
||||
{codeBlock}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { xGitHub } from './types'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
xGitHub: xGitHub
|
||||
numPreviews: number
|
||||
}
|
||||
|
||||
export function PreviewsRow({ slug, xGitHub }: Props) {
|
||||
export function PreviewsRow({ slug, numPreviews }: Props) {
|
||||
const { t } = useTranslation('products')
|
||||
const hasPreviews = xGitHub.previews && xGitHub.previews.length > 0
|
||||
|
||||
return (
|
||||
<tr>
|
||||
@@ -21,9 +19,9 @@ export function PreviewsRow({ slug, xGitHub }: Props) {
|
||||
<p className="m-0">
|
||||
Setting to
|
||||
<code>application/vnd.github.v3+json</code> is recommended.
|
||||
{hasPreviews && (
|
||||
{numPreviews > 0 && (
|
||||
<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_notice')}`}
|
||||
</a>
|
||||
|
||||
@@ -1,35 +1,92 @@
|
||||
import type { xCodeSample } from './types'
|
||||
import type { Operation } from './types'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { CodeBlock } from './CodeBlock'
|
||||
import { Fragment } from 'react'
|
||||
import { getShellExample, getGHExample, getJSExample } from '../lib/get-rest-code-samples'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
xCodeSamples: Array<xCodeSample>
|
||||
operation: Operation
|
||||
}
|
||||
|
||||
export function RestCodeSamples({ slug, xCodeSamples }: Props) {
|
||||
export function RestCodeSamples({ operation, slug }: Props) {
|
||||
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 (
|
||||
<Fragment key={xCodeSamples + slug}>
|
||||
<>
|
||||
<h4 id={`${slug}--code-samples`}>
|
||||
<a href={`#${slug}--code-samples`}>{`${t('rest.reference.code_samples')}`}</a>
|
||||
</h4>
|
||||
{xCodeSamples.map((sample, index) => {
|
||||
const sampleElements: JSX.Element[] = []
|
||||
if (sample.lang !== 'Ruby') {
|
||||
sampleElements.push(
|
||||
{languageExamples.map((sample, index) => (
|
||||
<div key={`${JSON.stringify(sample)}-${index}`}>
|
||||
{/* Example requests */}
|
||||
{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
|
||||
key={sample.lang + index}
|
||||
headingLang={sample.lang}
|
||||
codeBlock={sample.source}
|
||||
highlight={sample.lang === 'JavaScript' ? 'javascript' : 'curl'}
|
||||
></CodeBlock>
|
||||
)
|
||||
}
|
||||
return sampleElements
|
||||
})}
|
||||
</Fragment>
|
||||
headingLang={JAVASCRIPT_HEADING}
|
||||
codeBlock={sample.javascript}
|
||||
highlight="javascript"
|
||||
/>
|
||||
)}
|
||||
{sample.ghcli && (
|
||||
<CodeBlock headingLang={GH_CLI_HEADING} codeBlock={sample.ghcli} highlight="curl" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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 { Link } from 'components/Link'
|
||||
|
||||
type Props = {
|
||||
notes: Array<string>
|
||||
enabledForGitHubApps: boolean
|
||||
}
|
||||
|
||||
export function RestNotes({ notes, enabledForGitHubApps }: Props) {
|
||||
export function RestNotes() {
|
||||
const { t } = useTranslation('products')
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="pt-4">{t('rest.reference.notes')}</h4>
|
||||
<ul className="mt-2 pl-3 pb-2">
|
||||
{enabledForGitHubApps && (
|
||||
<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>
|
||||
)}
|
||||
{notes.map((note: string) => {
|
||||
return <li>{note}</li>
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,56 +1,62 @@
|
||||
import slugger from 'github-slugger'
|
||||
|
||||
import { RestOperationHeading } from './RestOperationHeading'
|
||||
import { RestHTTPMethod } from './RestHTTPMethod'
|
||||
import { CodeBlock } from './CodeBlock'
|
||||
import { RestParameterTable } from './RestParameterTable'
|
||||
import { RestCodeSamples } from './RestCodeSamples'
|
||||
import { RestResponse } from './RestResponse'
|
||||
import { RestStatusCodes } from './RestStatusCodes'
|
||||
import { Operation } from './types'
|
||||
import { RestNotes } from './RestNotes'
|
||||
import { RestPreviewNotice } from './RestPreviewNotice'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { RestStatusCodes } from './RestStatusCodes'
|
||||
|
||||
type Props = {
|
||||
operation: Operation
|
||||
index: number
|
||||
}
|
||||
|
||||
export function RestOperation({ operation }: Props) {
|
||||
const { t } = useTranslation('products')
|
||||
const previews = operation['x-github'].previews
|
||||
const nonErrorResponses = operation.responses.filter(
|
||||
(response) => parseInt(response.httpStatusCode) < 400
|
||||
)
|
||||
|
||||
const slug = slugger.slug(operation.title)
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<RestOperationHeading
|
||||
slug={operation.slug}
|
||||
summary={operation.summary}
|
||||
slug={slug}
|
||||
title={operation.title}
|
||||
descriptionHTML={operation.descriptionHTML}
|
||||
/>
|
||||
<RestHTTPMethod verb={operation.verb} requestPath={operation.requestPath} />
|
||||
{operation.parameters && (
|
||||
|
||||
{operation.requestPath && (
|
||||
<CodeBlock verb={operation.verb} codeBlock={operation.requestPath}></CodeBlock>
|
||||
)}
|
||||
|
||||
{hasParameters && (
|
||||
<RestParameterTable
|
||||
slug={operation.slug}
|
||||
xGitHub={operation['x-github']}
|
||||
slug={slug}
|
||||
numPreviews={numPreviews}
|
||||
parameters={operation.parameters}
|
||||
bodyParameters={operation.bodyParameters}
|
||||
/>
|
||||
)}
|
||||
{operation['x-codeSamples'] && operation['x-codeSamples'].length > 0 && (
|
||||
<RestCodeSamples slug={operation.slug} xCodeSamples={operation['x-codeSamples']} />
|
||||
)}
|
||||
<RestResponse responses={nonErrorResponses} />
|
||||
{(operation.notes.length > 0 || operation['x-github'].enabledForGitHubApps) && (
|
||||
<RestNotes
|
||||
notes={operation.notes}
|
||||
enabledForGitHubApps={operation['x-github'].enabledForGitHubApps}
|
||||
|
||||
{hasCodeSamples && <RestCodeSamples operation={operation} slug={slug} />}
|
||||
|
||||
{hasStatusCodes && (
|
||||
<RestStatusCodes
|
||||
heading={t('rest.reference.status_codes')}
|
||||
statusCodes={operation.statusCodes}
|
||||
/>
|
||||
)}
|
||||
{previews && (
|
||||
<RestPreviewNotice slug={operation.slug} previews={operation['x-github'].previews} />
|
||||
)}
|
||||
<RestStatusCodes heading={t('rest.reference.status_codes')} responses={operation.responses} />
|
||||
|
||||
{operation.enabledForGitHubApps && <RestNotes />}
|
||||
|
||||
{numPreviews > 0 && <RestPreviewNotice slug={slug} previews={operation.previews} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { LinkIcon } from '@primer/octicons-react'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
summary: string
|
||||
title: string
|
||||
descriptionHTML: string
|
||||
}
|
||||
|
||||
export function RestOperationHeading({ slug, summary, descriptionHTML }: Props) {
|
||||
export function RestOperationHeading({ slug, title, descriptionHTML }: Props) {
|
||||
return (
|
||||
<>
|
||||
<h3 id={slug}>
|
||||
<a href={`#${slug}`}>
|
||||
<LinkIcon size={16} className="m-1" />
|
||||
</a>
|
||||
{summary}
|
||||
{title}
|
||||
</h3>
|
||||
<div dangerouslySetInnerHTML={{ __html: descriptionHTML }} />
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import cx from 'classnames'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { BodyParameter, Parameter, xGitHub } from './types'
|
||||
import { BodyParameter, Parameter } from './types'
|
||||
import styles from './RestParameterTable.module.scss'
|
||||
import { PreviewsRow } from './PreviewsRow'
|
||||
import { ParameterRows } from './ParameterRows'
|
||||
@@ -8,12 +8,12 @@ import { BodyParameterRows } from './BodyParametersRows'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
xGitHub: xGitHub
|
||||
numPreviews: number
|
||||
parameters: Array<Parameter>
|
||||
bodyParameters: Array<BodyParameter>
|
||||
}
|
||||
|
||||
export function RestParameterTable({ slug, xGitHub, parameters, bodyParameters }: Props) {
|
||||
export function RestParameterTable({ slug, numPreviews, parameters, bodyParameters }: Props) {
|
||||
const { t } = useTranslation('products')
|
||||
|
||||
return (
|
||||
@@ -31,7 +31,7 @@ export function RestParameterTable({ slug, xGitHub, parameters, bodyParameters }
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<PreviewsRow slug={slug} xGitHub={xGitHub} />
|
||||
<PreviewsRow slug={slug} numPreviews={numPreviews} />
|
||||
<ParameterRows parameters={parameters} />
|
||||
<BodyParameterRows slug={slug} bodyParameters={bodyParameters} />
|
||||
</tbody>
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import { Preview } from './types'
|
||||
|
||||
type Props = {
|
||||
slug: string
|
||||
previews: Array<Preview> | []
|
||||
previews: Array<string>
|
||||
}
|
||||
|
||||
export function RestPreviewNotice({ slug, previews }: Props) {
|
||||
const { t } = useTranslation('products')
|
||||
|
||||
const previewNotices = previews.map((preview, index) => {
|
||||
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`}>
|
||||
{previews.length > 1
|
||||
? `${t('rest.reference.preview_notices')}`
|
||||
: `${t('rest.reference.preview_notice')}`}
|
||||
</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>
|
||||
<MarkdownContent>
|
||||
{subcategories.map((subcategory, index) => (
|
||||
<div key={`restCategory-${index}`}>
|
||||
<div key={`${subcategory}-${index}`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: descriptions[subcategory] }} />
|
||||
{restOperations[subcategory].map((operation, index) => (
|
||||
<RestOperation key={`restOperation-${index}`} operation={operation} index={index} />
|
||||
<RestOperation
|
||||
key={`${subcategory}-${operation.title}-${index}`}
|
||||
operation={operation}
|
||||
/>
|
||||
))}
|
||||
</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 { CodeResponse } from './types'
|
||||
import { StatusCode } from './types'
|
||||
import { useTranslation } from 'components/hooks/useTranslation'
|
||||
import styles from './RestResponseTable.module.scss'
|
||||
|
||||
type Props = {
|
||||
heading: string
|
||||
responses: Array<CodeResponse>
|
||||
statusCodes: Array<StatusCode>
|
||||
}
|
||||
|
||||
export function RestStatusCodes({ heading, responses }: Props) {
|
||||
export function RestStatusCodes({ heading, statusCodes }: Props) {
|
||||
const { t } = useTranslation('products')
|
||||
|
||||
return (
|
||||
@@ -22,21 +22,22 @@ export function RestStatusCodes({ heading, responses }: Props) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{responses.map((response, index) => (
|
||||
<tr key={`${response.description}-${index}}`}>
|
||||
{statusCodes.map((statusCode, index) => {
|
||||
return (
|
||||
<tr key={`${statusCode.description}-${index}}`}>
|
||||
<td>
|
||||
<code>{response.httpStatusCode}</code>
|
||||
<code>{statusCode.httpStatusCode}</code>
|
||||
</td>
|
||||
<td>
|
||||
{response.description &&
|
||||
response.description.toLowerCase() !== '<p>response</p>' ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: response.description }} />
|
||||
{statusCode.description ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: statusCode.description }} />
|
||||
) : (
|
||||
response.httpStatusMessage
|
||||
statusCode.httpStatusMessage
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export interface Operation {
|
||||
verb: string
|
||||
summary: string
|
||||
slug: string
|
||||
title: string
|
||||
descriptionHTML: string
|
||||
notes: Array<string>
|
||||
previews: Array<string>
|
||||
requestPath: string
|
||||
responses: Array<CodeResponse>
|
||||
serverUrl: string
|
||||
statusCodes: Array<StatusCode>
|
||||
parameters: Array<Parameter>
|
||||
bodyParameters: Array<BodyParameter>
|
||||
'x-github': xGitHub
|
||||
'x-codeSamples': Array<xCodeSample>
|
||||
category: string
|
||||
subcategory: string
|
||||
enabledForGitHubApps: boolean
|
||||
codeExamples: Array<CodeSample>
|
||||
}
|
||||
|
||||
export interface Parameter {
|
||||
@@ -23,28 +25,27 @@ export interface Parameter {
|
||||
}
|
||||
}
|
||||
|
||||
export interface xGitHub {
|
||||
category: string
|
||||
enabledForGitHubApps: boolean
|
||||
previews: Array<Preview> | []
|
||||
}
|
||||
|
||||
export interface CodeResponse {
|
||||
export interface StatusCode {
|
||||
description: string
|
||||
httpStatusCode: string
|
||||
httpStatusMessage: string
|
||||
payload: string
|
||||
}
|
||||
|
||||
export interface xCodeSample {
|
||||
lang: string
|
||||
source: string
|
||||
export interface CodeSample {
|
||||
key: string
|
||||
response: {
|
||||
contentType: string
|
||||
description: string
|
||||
example: Record<string, string>
|
||||
statusCode: string
|
||||
}
|
||||
request: {
|
||||
contentType: string
|
||||
acceptHeader: string
|
||||
bodyParameters: Record<string, string>
|
||||
parameters: Record<string, string>
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface Preview {
|
||||
html: string
|
||||
required: boolean
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface BodyParameter {
|
||||
|
||||
@@ -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
|
||||
// is needed to generate the toc
|
||||
const titles = categoryOperations[subcategory]
|
||||
.map((operation) => `### ${operation.summary}\n`)
|
||||
.map((operation) => `### ${operation.title}\n`)
|
||||
.join('')
|
||||
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",
|
||||
"html-entities": "^2.3.2",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"javascript-stringify": "^2.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kleur": "4.1.4",
|
||||
@@ -83,6 +84,7 @@
|
||||
"ts-dedent": "^2.2.0",
|
||||
"unified": "^10.1.0",
|
||||
"unist-util-visit": "^4.1.0",
|
||||
"url-template": "^3.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"walk-sync": "^3.0.0"
|
||||
},
|
||||
@@ -135,7 +137,6 @@
|
||||
"http-status-code": "^2.1.0",
|
||||
"husky": "^7.0.4",
|
||||
"japanese-characters": "^1.1.0",
|
||||
"javascript-stringify": "^2.1.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-environment-puppeteer": "5.0.4",
|
||||
"jest-fail-on-console": "^2.2.3",
|
||||
@@ -160,8 +161,7 @@
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"supertest": "^6.2.2",
|
||||
"typescript": "^4.5.5",
|
||||
"url-template": "^3.0.0"
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
@@ -7281,13 +7281,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz",
|
||||
"integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
|
||||
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"exit-on-epipe": "~1.0.1",
|
||||
"printj": "~1.3.1"
|
||||
"printj": "~1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
@@ -7296,6 +7296,18 @@
|
||||
"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": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
@@ -8074,6 +8086,7 @@
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -9243,7 +9256,6 @@
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
|
||||
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.7",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9967,12 +9979,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/gifwrap": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz",
|
||||
"integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
|
||||
"integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"image-q": "^4.0.0",
|
||||
"image-q": "^1.1.1",
|
||||
"omggif": "^1.0.10"
|
||||
}
|
||||
},
|
||||
@@ -11013,20 +11025,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/image-q": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
|
||||
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
|
||||
"integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "16.9.1"
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
|
||||
@@ -11807,8 +11813,7 @@
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
|
||||
},
|
||||
"node_modules/jest": {
|
||||
"version": "27.4.7",
|
||||
@@ -16274,7 +16279,6 @@
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz",
|
||||
"integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==",
|
||||
"deprecated": "Version no longer supported. Upgrade to @latest",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -16578,7 +16582,6 @@
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.19.0.tgz",
|
||||
"integrity": "sha512-2S6E6ygpoqcECaagDbBopoSOPDv0pAZvTbnBgUY+6hq0/XDFDOLEMNlHF/SKJlzcaZ9ckiKjKDuueWI3FN/WXw==",
|
||||
"deprecated": "Version no longer supported. Upgrade to @latest",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -16888,9 +16891,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/parse-headers": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz",
|
||||
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
|
||||
"integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
@@ -17296,9 +17299,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/printj": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz",
|
||||
"integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz",
|
||||
"integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"printj": "bin/printj.njs"
|
||||
@@ -17510,7 +17513,6 @@
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
|
||||
"integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==",
|
||||
"deprecated": "Version no longer supported. Upgrade to @latest",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -17818,7 +17820,6 @@
|
||||
"version": "15.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
|
||||
"integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
@@ -21442,7 +21443,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz",
|
||||
"integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
@@ -28121,13 +28121,21 @@
|
||||
}
|
||||
},
|
||||
"crc-32": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.1.tgz",
|
||||
"integrity": "sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
|
||||
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"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": {
|
||||
@@ -30191,12 +30199,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"gifwrap": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz",
|
||||
"integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz",
|
||||
"integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"image-q": "^4.0.0",
|
||||
"image-q": "^1.1.1",
|
||||
"omggif": "^1.0.10"
|
||||
}
|
||||
},
|
||||
@@ -30982,21 +30990,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"image-q": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
|
||||
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
|
||||
"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==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz",
|
||||
"integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"image-size": {
|
||||
"version": "1.0.0",
|
||||
@@ -31533,8 +31530,7 @@
|
||||
"javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="
|
||||
},
|
||||
"jest": {
|
||||
"version": "27.4.7",
|
||||
@@ -35374,9 +35370,9 @@
|
||||
}
|
||||
},
|
||||
"parse-headers": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz",
|
||||
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
|
||||
"integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==",
|
||||
"optional": true
|
||||
},
|
||||
"parse-json": {
|
||||
@@ -35683,9 +35679,9 @@
|
||||
}
|
||||
},
|
||||
"printj": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.1.tgz",
|
||||
"integrity": "sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz",
|
||||
"integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==",
|
||||
"optional": true
|
||||
},
|
||||
"prismjs": {
|
||||
@@ -38839,8 +38835,7 @@
|
||||
"url-template": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-3.0.0.tgz",
|
||||
"integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-S6P5TcJ8GrGG+yzMZ8ojdtiGtQmQG+UOMelhE3X5uQrEEoq69aDQ05eASPQGj+CjsPVfumWKbH2HrjME46sk0g=="
|
||||
},
|
||||
"use-subscription": {
|
||||
"version": "1.5.1",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"hot-shots": "^9.0.0",
|
||||
"html-entities": "^2.3.2",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"javascript-stringify": "^2.1.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kleur": "4.1.4",
|
||||
@@ -85,6 +86,7 @@
|
||||
"ts-dedent": "^2.2.0",
|
||||
"unified": "^10.1.0",
|
||||
"unist-util-visit": "^4.1.0",
|
||||
"url-template": "^3.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"walk-sync": "^3.0.0"
|
||||
},
|
||||
@@ -137,7 +139,6 @@
|
||||
"http-status-code": "^2.1.0",
|
||||
"husky": "^7.0.4",
|
||||
"japanese-characters": "^1.1.0",
|
||||
"javascript-stringify": "^2.1.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-environment-puppeteer": "5.0.4",
|
||||
"jest-fail-on-console": "^2.2.3",
|
||||
@@ -162,8 +163,7 @@
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"supertest": "^6.2.2",
|
||||
"typescript": "^4.5.5",
|
||||
"url-template": "^3.0.0"
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
|
||||
@@ -173,25 +173,7 @@ async function decorate() {
|
||||
const operations = await getOperations(schema)
|
||||
// process each operation, asynchronously rendering markdown and stuff
|
||||
await Promise.all(operations.map((operation) => operation.process()))
|
||||
|
||||
// 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()
|
||||
const categories = [...new Set(operations.map((operation) => operation.category))].sort()
|
||||
|
||||
// Orders the operations by their category and subcategories.
|
||||
// All operations must have a category, but operations don't need
|
||||
@@ -215,9 +197,7 @@ async function decorate() {
|
||||
const operationsByCategory = {}
|
||||
categories.forEach((category) => {
|
||||
operationsByCategory[category] = {}
|
||||
const categoryOperations = decoratedOperations.filter(
|
||||
(operation) => operation.category === category
|
||||
)
|
||||
const categoryOperations = operations.filter((operation) => operation.category === category)
|
||||
categoryOperations
|
||||
.filter((operation) => !operation.subcategory)
|
||||
.map((operation) => (operation.subcategory = operation.category))
|
||||
@@ -258,7 +238,7 @@ async function decorate() {
|
||||
// This is a collection of operations that have `enabledForGitHubApps = true`
|
||||
// It's grouped by resource title to make rendering easier
|
||||
operationsEnabledForGitHubApps[schemaName][category] = categoryOperations
|
||||
.filter((operation) => operation['x-github'].enabledForGitHubApps)
|
||||
.filter((operation) => operation.enabledForGitHubApps)
|
||||
.map((operation) => ({
|
||||
slug: operation.slug,
|
||||
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 = []
|
||||
|
||||
for (const [requestPath, operationsAtPath] of Object.entries(schema.paths)) {
|
||||
for (const [verb, props] of Object.entries(operationsAtPath)) {
|
||||
const serverUrl = schema.servers[0].url.replace('{protocol}', 'http(s)')
|
||||
const operation = new Operation(verb, requestPath, props, serverUrl)
|
||||
for (const [verb, operationProps] of Object.entries(operationsAtPath)) {
|
||||
const operation = new Operation(verb, requestPath, operationProps, schema.servers)
|
||||
operations.push(operation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,45 +3,21 @@
|
||||
|
||||
export default {
|
||||
type: 'object',
|
||||
|
||||
// Every operation must have these props
|
||||
required: [
|
||||
'title',
|
||||
'verb',
|
||||
'requestPath',
|
||||
'parameters',
|
||||
'responses',
|
||||
'slug',
|
||||
'x-codeSamples',
|
||||
'category',
|
||||
'categoryLabel',
|
||||
'parameters',
|
||||
'statusCodes',
|
||||
'codeExamples',
|
||||
],
|
||||
|
||||
properties: {
|
||||
// Properties from the source OpenAPI schema that this module depends on
|
||||
externalDocs: {
|
||||
description: 'The public documentation for the given operation',
|
||||
type: 'object',
|
||||
required: ['description', 'url'],
|
||||
properties: {
|
||||
description: {
|
||||
title: {
|
||||
description: 'The title of the operation',
|
||||
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: {
|
||||
description: 'The HTTP method',
|
||||
type: 'string',
|
||||
@@ -56,28 +32,37 @@ export default {
|
||||
description: 'The rendered HTML version of the markdown `description` property',
|
||||
type: 'string',
|
||||
},
|
||||
notes: {
|
||||
type: 'array',
|
||||
},
|
||||
slug: {
|
||||
description: 'GitHub.com-style param-case property for use as a unique DOM id',
|
||||
type: 'string',
|
||||
},
|
||||
category: {
|
||||
description: 'the `issues` in `/v3/issues/events/`; supports legacy developer site URLs',
|
||||
type: 'string',
|
||||
},
|
||||
categoryLabel: {
|
||||
description: 'humanized form of category',
|
||||
type: 'string',
|
||||
},
|
||||
subcategory: {
|
||||
description: 'the `events` in `/v3/issues/events/`; supports legacy developer site URLs',
|
||||
type: 'string',
|
||||
},
|
||||
subcategoryLabel: {
|
||||
description: 'humanized form of subcategory',
|
||||
type: 'string',
|
||||
parameters: {
|
||||
description: 'Parameters to the operation that can be present in the URL path or query',
|
||||
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
|
||||
import { readFile } from 'fs/promises'
|
||||
import { get, flatten, isPlainObject } from 'lodash-es'
|
||||
import { sentenceCase } from 'change-case'
|
||||
import GitHubSlugger from 'github-slugger'
|
||||
import httpStatusCodes from 'http-status-code'
|
||||
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 operationSchema from './operation-schema.js'
|
||||
import { parseTemplate } from 'url-template'
|
||||
|
||||
const overrideOperations = JSON.parse(
|
||||
await readFile('script/rest/utils/rest-api-overrides.json', 'utf8')
|
||||
)
|
||||
const slugger = new GitHubSlugger()
|
||||
|
||||
// titles that can't be derived by sentence-casing the ID
|
||||
const categoryTitles = { scim: 'SCIM' }
|
||||
|
||||
export default class Operation {
|
||||
constructor(verb, requestPath, props, serverUrl) {
|
||||
const defaultProps = {
|
||||
parameters: [],
|
||||
'x-codeSamples': [],
|
||||
responses: {},
|
||||
#operation
|
||||
constructor(verb, requestPath, operation, globalServers) {
|
||||
this.#operation = operation
|
||||
// The global server object sets metadata including the base url for
|
||||
// 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()
|
||||
this.slug = slugger.slug(this.summary)
|
||||
|
||||
// Add category
|
||||
|
||||
// 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]
|
||||
// Attach some global properties to the operation object to use
|
||||
// during processing
|
||||
this.#operation.serverUrl = this.serverUrl
|
||||
this.#operation.requestPath = requestPath
|
||||
this.#operation.verb = verb
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return operationSchema
|
||||
setCategories() {
|
||||
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() {
|
||||
this['x-codeSamples'] = createCodeSamples(this)
|
||||
|
||||
await Promise.all([
|
||||
this.renderDescription(),
|
||||
this.renderResponses(),
|
||||
this.renderStatusCodes(),
|
||||
this.renderParameterDescriptions(),
|
||||
this.renderBodyParameterDescriptions(),
|
||||
this.renderPreviewNotes(),
|
||||
this.renderNotes(),
|
||||
])
|
||||
|
||||
const ajv = new Ajv()
|
||||
const valid = ajv.validate(this.schema, this)
|
||||
const valid = ajv.validate(operationSchema, this)
|
||||
if (!valid) {
|
||||
console.error(JSON.stringify(ajv.errors, null, 2))
|
||||
throw new Error('Invalid operation found')
|
||||
throw new Error('Invalid OpenAPI operation found')
|
||||
}
|
||||
}
|
||||
|
||||
async renderDescription() {
|
||||
this.descriptionHTML = await renderContent(this.description)
|
||||
this.descriptionHTML = await renderContent(this.#operation.description)
|
||||
return this
|
||||
}
|
||||
|
||||
async renderResponses() {
|
||||
// clone and delete this.responses so we can turn it into a clean array of objects
|
||||
const rawResponses = JSON.parse(JSON.stringify(this.responses))
|
||||
delete this.responses
|
||||
async renderStatusCodes() {
|
||||
const responses = this.#operation.responses
|
||||
const responseKeys = Object.keys(responses)
|
||||
if (responseKeys.length === 0) return []
|
||||
|
||||
this.responses = await Promise.all(
|
||||
Object.keys(rawResponses).map(async (responseCode) => {
|
||||
const rawResponse = rawResponses[responseCode]
|
||||
this.statusCodes = await Promise.all(
|
||||
responseKeys.map(async (responseCode) => {
|
||||
const response = responses[responseCode]
|
||||
const httpStatusCode = responseCode
|
||||
const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode))
|
||||
const responseDescription = await renderContent(rawResponse.description)
|
||||
const httpStatusMessage = httpStatusCodes.getMessage(Number(responseCode), 'HTTP/2')
|
||||
// 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 = []
|
||||
|
||||
/* 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 [
|
||||
{
|
||||
return {
|
||||
httpStatusCode,
|
||||
httpStatusMessage,
|
||||
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() {
|
||||
return Promise.all(
|
||||
this.parameters.map(async (param) => {
|
||||
param.descriptionHTML = await renderContent(param.description)
|
||||
param.descriptionHtml = await renderContent(param.description)
|
||||
delete param.description
|
||||
return param
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async renderBodyParameterDescriptions() {
|
||||
if (!this.#operation.requestBody) return []
|
||||
const contentType = Object.keys(this.#operation.requestBody.content)[0]
|
||||
let bodyParamsObject = get(
|
||||
this,
|
||||
`requestBody.content.${this.contentType}.schema.properties`,
|
||||
this.#operation,
|
||||
`requestBody.content.${contentType}.schema.properties`,
|
||||
{}
|
||||
)
|
||||
let requiredParams = get(this, `requestBody.content.${this.contentType}.schema.required`, [])
|
||||
const oneOfObject = get(this, `requestBody.content.${this.contentType}.schema.oneOf`, undefined)
|
||||
let requiredParams = get(
|
||||
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
|
||||
// use the first option or munge the options together.
|
||||
@@ -211,8 +177,8 @@ export default class Operation {
|
||||
|
||||
// TODO: Remove this check
|
||||
// This operation shouldn't have a oneOf in this case, it needs to be
|
||||
// removed from the schema in the github/github repo.
|
||||
if (this.operationId === 'checks/create') {
|
||||
// removed from the schema in the openapi schema repo.
|
||||
if (this.#operation.operationId === 'checks/create') {
|
||||
delete bodyParamsObject.oneOf
|
||||
} else if (allOneOfAreObjects) {
|
||||
// When all of the oneOf objects have the `type: object` we
|
||||
@@ -233,14 +199,12 @@ export default class Operation {
|
||||
requiredParams = firstOneOfObject.required
|
||||
}
|
||||
}
|
||||
|
||||
this.bodyParameters = await getBodyParams(bodyParamsObject, requiredParams)
|
||||
}
|
||||
|
||||
async renderPreviewNotes() {
|
||||
const previews = get(this, 'x-github.previews', []).filter((preview) => preview.note)
|
||||
|
||||
return Promise.all(
|
||||
const previews = get(this.#operation, 'x-github.previews', [])
|
||||
this.previews = await Promise.all(
|
||||
previews.map(async (preview) => {
|
||||
const note = preview.note
|
||||
// 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`
|
||||
.replace(/\n`application/, '\n```\napplication')
|
||||
.replace(/json`$/, 'json\n```')
|
||||
preview.html = await renderContent(note)
|
||||
delete preview.note
|
||||
return await renderContent(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
|
||||
|
||||
@@ -7,6 +7,8 @@ import { allVersions } from '../../lib/all-versions.js'
|
||||
describe('REST references docs', () => {
|
||||
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 () => {
|
||||
for (const version in allVersions) {
|
||||
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 () => {
|
||||
const enableForApps = await getEnabledForApps()
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ import fs from 'fs/promises'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'path'
|
||||
|
||||
import dedent from 'dedent'
|
||||
import { describe } from '@jest/globals'
|
||||
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 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 () => {
|
||||
for (const version in allVersions) {
|
||||
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 () => {
|
||||
for (const version in allVersions) {
|
||||
if (version === 'enterprise-server@3.2' || version === 'enterprise-server@3.1') continue
|
||||
|
||||
let domain = 'https://api.github.com'
|
||||
if (version.includes('enterprise-server')) {
|
||||
domain = 'http(s)://{hostname}/api/v3'
|
||||
domain = 'http(s)://HOSTNAME/api/v3'
|
||||
} 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}')
|
||||
expect(operation.serverUrl).toBe(domain)
|
||||
expect(isPlainObject(operation)).toBe(true)
|
||||
const { source } = operation['x-codeSamples'].find((sample) => sample.lang === 'Shell')
|
||||
const expected =
|
||||
'curl \\\n' +
|
||||
' -H "Accept: application/vnd.github.v3+json" \\\n' +
|
||||
` ${domain}/repos/octocat/hello-world`
|
||||
expect(source).toEqual(expected)
|
||||
}
|
||||
expect(operation.codeExamples).toBeDefined()
|
||||
operation.codeExamples.forEach((example) => {
|
||||
expect(isPlainObject(example.request)).toBe(true)
|
||||
expect(isPlainObject(example.response)).toBe(true)
|
||||
})
|
||||
|
||||
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