| | import { parseTemplate } from 'url-template' |
| | import { stringify } from 'javascript-stringify' |
| |
|
| | import type { CodeSample, Operation } from '@/rest/components/types' |
| | import { type VersionItem } from '@/frame/components/context/MainContext' |
| |
|
| | |
| | function shouldOmitAuthentication(operation: Operation, currentVersion: string): boolean { |
| | |
| | if (!operation?.progAccess?.allowPermissionlessAccess) { |
| | return false |
| | } |
| |
|
| | |
| | |
| | const isDotcomVersion = |
| | currentVersion.startsWith('free-pro-team') || currentVersion.startsWith('enterprise-cloud') |
| |
|
| | return isDotcomVersion |
| | } |
| |
|
| | |
| | |
| | function escapeShellValue(value: string): string { |
| | |
| | return value.replace(/'/g, "'\\''") |
| | } |
| |
|
| | type CodeExamples = Record<string, any> |
| |
|
| | |
| | |
| | |
| | |
| | const CURL_CONTENT_TYPE_MAPPING: { [key: string]: string } = { |
| | 'application/x-www-form-urlencoded': '--data-urlencode', |
| | 'multipart/form-data': '--form', |
| | 'application/octet-stream': '--data-binary', |
| | } |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function getShellExample( |
| | operation: Operation, |
| | codeSample: CodeSample, |
| | currentVersion: string, |
| | allVersions: Record<string, VersionItem>, |
| | ) { |
| | let contentTypeHeader = '' |
| |
|
| | if (codeSample?.request?.contentType === 'application/octet-stream') { |
| | contentTypeHeader = '-H "Content-Type: application/octet-stream"' |
| | } else if (codeSample?.request?.contentType === 'multipart/form-data') { |
| | contentTypeHeader = '-H "Content-Type: multipart/form-data"' |
| | } |
| |
|
| | |
| | const omitAuth = shouldOmitAuthentication(operation, currentVersion) |
| |
|
| | |
| | if (operation.subcategory === 'manage-ghes') { |
| | |
| | if (operation.verb === 'get') { |
| | contentTypeHeader = '-H "Content-Type: application/json"' |
| | } else { |
| | contentTypeHeader = `-H "Content-Type: ${codeSample?.request?.contentType}"` |
| | } |
| | } |
| |
|
| | if (operation.subcategory === 'inference') { |
| | contentTypeHeader = '-H "Content-Type: application/json"' |
| | } |
| |
|
| | let requestPath = codeSample?.request?.parameters |
| | ? parseTemplate(operation.requestPath).expand(codeSample.request.parameters) |
| | : operation.requestPath |
| |
|
| | const requiredQueryParams = getRequiredQueryParamsPath(operation, codeSample) |
| | requestPath += requiredQueryParams ? `?${requiredQueryParams}` : '' |
| |
|
| | let requestBodyParams = '' |
| | if (codeSample?.request?.bodyParameters) { |
| | requestBodyParams = `-d '${JSON.stringify(codeSample.request.bodyParameters).replace( |
| | /'/g, |
| | "'\\''", |
| | )}'` |
| | |
| | const contentType = codeSample.request.contentType |
| | if (contentType in CURL_CONTENT_TYPE_MAPPING) { |
| | requestBodyParams = '' |
| | // Most of the time the example body parameters have a name and value |
| | // and are included in an object. But, some cases are a single value |
| | // and the type is a string. |
| | const { bodyParameters } = codeSample.request |
| | if (bodyParameters && typeof bodyParameters === 'object' && !Array.isArray(bodyParameters)) { |
| | const paramNames = Object.keys(bodyParameters) |
| | for (const elem of paramNames) { |
| | const escapedValue = escapeShellValue(String(bodyParameters[elem])) |
| | requestBodyParams = `${requestBodyParams} ${CURL_CONTENT_TYPE_MAPPING[contentType]} '${elem}=${escapedValue}'` |
| | } |
| | } else { |
| | const escapedValue = escapeShellValue(String(bodyParameters)) |
| | requestBodyParams = `${CURL_CONTENT_TYPE_MAPPING[contentType]} "${escapedValue}"` |
| | } |
| | } |
| | } |
| | |
| | let authHeader = omitAuth ? '' : '-H "Authorization: Bearer <YOUR-TOKEN>"' |
| | let apiVersionHeader = |
| | allVersions[currentVersion].apiVersions.length > 0 && |
| | allVersions[currentVersion].latestApiVersion |
| | ? `-H "X-GitHub-Api-Version: ${allVersions[currentVersion].latestApiVersion}"` |
| | : '' |
| | let acceptHeader = `-H "Accept: ${getAcceptHeader(codeSample)}"` |
| | let urlArg = `${operation.serverUrl}${requestPath}` |
| | // If the `requestPath` contains a `?` character, if you need to escape |
| | // the whole URL otherwise, when you paste it into your terminal, it |
| | // will fail because the `?` is a bash control character. |
| | if (requestPath.includes('?')) { |
| | urlArg = `"${urlArg}"` |
| | } |
| | |
| | // Overwrite curl examples since the github enterprise related apis are seperate from the dotcom api standards |
| | if (operation.subcategory === 'management-console' || operation.subcategory === 'manage-ghes') { |
| | authHeader = '-u "api_key:your-password"' |
| | apiVersionHeader = '' |
| | acceptHeader = acceptHeader === `-H "Accept: application/vnd.github+json"` ? '' : acceptHeader |
| | } |
| | |
| | // For unauthenticated endpoints, remove the auth header completely |
| | if ( |
| | omitAuth && |
| | operation.subcategory !== 'management-console' && |
| | operation.subcategory !== 'manage-ghes' |
| | ) { |
| | authHeader = '' |
| | } |
| | |
| | if (operation?.progAccess?.basicAuth) { |
| | authHeader = '-u "<YOUR_CLIENT_ID>:<YOUR_CLIENT_SECRET>"' |
| | } |
| | |
| | const args = [ |
| | operation.verb !== 'get' && `-X ${operation.verb.toUpperCase()}`, |
| | acceptHeader, |
| | authHeader, |
| | apiVersionHeader, |
| | contentTypeHeader, |
| | urlArg, |
| | requestBodyParams, |
| | ].filter(Boolean) |
| | return `curl -L \\\n ${args.join(' \\\n ')}` |
| | } |
| | |
| | /* |
| | Generates a GitHub CLI example |
| | |
| | For example: |
| | gh api \ |
| | -X POST \ |
| | -H "Accept: application/vnd.github+json" \ |
| | /repos/OWNER/REPO/deployments \ |
| | -f ref,topic-branch=0,payload,{ "deploy": "migrate" }=1,description,Deploy request from hubot=2 |
| | */ |
| | export function getGHExample( |
| | operation: Operation, |
| | codeSample: CodeSample, |
| | currentVersion: string, |
| | allVersions: Record<string, VersionItem>, |
| | ) { |
| | // Basic authentication is not supported by GH CLI |
| | if (operation?.progAccess?.basicAuth) return |
| | |
| | const defaultAcceptHeader = getAcceptHeader(codeSample) |
| | const hostname = operation.serverUrl !== 'https://api.github.com' ? '--hostname HOSTNAME' : '' |
| | |
| | let requestPath = codeSample?.request?.parameters |
| | ? parseTemplate(operation.requestPath).expand(codeSample.request.parameters) |
| | : operation.requestPath |
| | |
| | const apiVersionHeader = |
| | allVersions[currentVersion].apiVersions.length > 0 && |
| | allVersions[currentVersion].latestApiVersion |
| | ? `-H "X-GitHub-Api-Version: ${allVersions[currentVersion].latestApiVersion}"` |
| | : '' |
| | |
| | const requiredQueryParams = getRequiredQueryParamsPath(operation, codeSample) |
| | requestPath += requiredQueryParams ? `?${requiredQueryParams}` : '' |
| | |
| | let requestBodyParams = '' |
| | // Most of the time the example body parameters have a name and value |
| | // and are included in an object. But, some cases are a single value |
| | // and the type is a string. |
| | const { bodyParameters } = codeSample.request |
| | if (bodyParameters) { |
| | // There should not be a case in a REST API where only an array is sent |
| | if (Array.isArray(bodyParameters)) { |
| | throw new Error('Array of arrays found in body parameters') |
| | } |
| | |
| | if (typeof bodyParameters === 'object') { |
| | // Special handling for gist endpoints - use --input for nested file structures |
| | const isGistEndpoint = |
| | operation.requestPath.includes('/gists') && |
| | (operation.title === 'Create a gist' || operation.title === 'Update a gist') |
| | |
| | // For complex objects with arrays, use --input with JSON |
| | const hasArrays = hasNestedArrays(bodyParameters as NestedObjectParameter) |
| | if (hasArrays || isGistEndpoint) { |
| | const jsonBody = JSON.stringify( |
| | bodyParameters, |
| | (key: string, value: any) => { |
| | // Convert numeric strings back to numbers for API compatibility |
| | if (typeof value === 'string' && /^\d+$/.test(value)) { |
| | return parseInt(value, 10) |
| | } |
| | // Convert boolean strings to actual booleans |
| | if (value === 'true') return true |
| | if (value === 'false') return false |
| | return value |
| | }, |
| | 2, |
| | ).replace(/'/g, "'\\''") |
| | requestBodyParams = `--input - <<< '${jsonBody}'` |
| | } else { |
| | requestBodyParams += handleObjectParameter(bodyParameters as NestedObjectParameter) |
| | } |
| | } else { |
| | requestBodyParams += handleSingleParameter('', bodyParameters) |
| | } |
| | } |
| | |
| | const args = [ |
| | operation.verb !== 'get' && `--method ${operation.verb.toUpperCase()}`, |
| | `-H "Accept: ${defaultAcceptHeader}"`, |
| | apiVersionHeader, |
| | hostname, |
| | requestPath, |
| | requestBodyParams, |
| | ].filter(Boolean) |
| | return `# GitHub CLI api\n# https://cli.github.com/manual/gh_api\n\ngh api \\\n ${args.join( |
| | ' \\\n ', |
| | )}` |
| | } |
| | |
| | const startTransformKey = (currentKey: string): string => currentKey |
| | |
| | type TypedItem = 'string' | 'number' | 'boolean' |
| | type NestedObjectParameter = |
| | | TypedItem |
| | | { [key: string]: NestedObjectParameter } |
| | | NestedObjectParameter[] |
| | |
| | // Helper function to detect if an object has nested arrays |
| | function hasNestedArrays(obj: NestedObjectParameter): boolean { |
| | if (Array.isArray(obj)) { |
| | return true |
| | } |
| | if (typeof obj === 'object' && obj !== null) { |
| | for (const value of Object.values(obj)) { |
| | if (hasNestedArrays(value)) { |
| | return true |
| | } |
| | } |
| | } |
| | return false |
| | } |
| | |
| | function handleSingleParameter( |
| | key: string, |
| | value: NestedObjectParameter, |
| | transformKey = startTransformKey, |
| | ): string { |
| | let cliLine = '' |
| | const keyString = `${transformKey(key)}` |
| | // When only a value is passed to bodyParameters we don't show the '=' since there isn't a key |
| | let separator = '=' |
| | if (!key) { |
| | separator = '' |
| | } |
| | if (typeof value === 'string') { |
| | // Escape single quotes in string values to prevent shell command issues with contractions |
| | const escapedValue = escapeShellValue(value) |
| | cliLine += ` -f '${keyString}${separator}${escapedValue}'` |
| | } else if (typeof value === 'number' || typeof value === 'boolean' || value === null) { |
| | cliLine += ` -F "${keyString}${separator}${value}"` |
| | } else if (Array.isArray(value)) { |
| | // For simple arrays, use individual parameters with indices |
| | for (let i = 0; i < value.length; i++) { |
| | const param = value[i] |
| | if (Array.isArray(param)) { |
| | throw new Error('Nested arrays are not valid in the bodyParameters') |
| | } |
| | |
| | if (typeof param === 'object' && param !== null) { |
| | cliLine += handleObjectParameter( |
| | param, |
| | (nextKey: string): string => `${keyString}[${i}]${nextKey}`, |
| | ) |
| | } else { |
| | // Transform key in this case needs to account for the `key` being passed in and use array index |
| | const arrayTransform = () => `${transformKey(key)}[${i}]` |
| | cliLine += handleSingleParameter(key, param, arrayTransform) |
| | } |
| | } |
| | } else if (typeof value === 'object') { |
| | cliLine += handleObjectParameter(value, (nextKey) => `${keyString}[${nextKey}]`) |
| | } |
| | return cliLine |
| | } |
| | |
| | function handleObjectParameter( |
| | objectParams: NestedObjectParameter, |
| | transformKey = startTransformKey, |
| | ) { |
| | let cliLine = '' |
| | for (const [key, value] of Object.entries(objectParams)) { |
| | if (Array.isArray(value)) { |
| | for (let i = 0; i < value.length; i++) { |
| | const param = value[i] |
| | // This isn't valid in a REST context, our REST API should not be designed to take |
| | // something like { "letterSegments": [["a", "b", "c"], ["d", "e", "f"]] } |
| | // If this is a possibility, we can update the code to handle it |
| | if (Array.isArray(param)) { |
| | throw new Error('Nested arrays are not valid in the bodyParameters') |
| | } |
| | |
| | if (typeof param === 'object' && param !== null) { |
| | // When an array of objects, we want to display the key and value as two separate parameters |
| | // E.g. -F "properties[0][property_name]=repo" -F "properties[0][value]=docs-internal" |
| | for (const [nestedKey, nestedValue] of Object.entries(param)) { |
| | cliLine += handleSingleParameter( |
| | `${key}[${i}][${nestedKey}]`, |
| | nestedValue as NestedObjectParameter, |
| | transformKey, |
| | ) |
| | } |
| | } else { |
| | cliLine += handleSingleParameter( |
| | key, |
| | param, |
| | (nextKey: string) => `${transformKey(nextKey)}[${i}]`, |
| | ) |
| | } |
| | } |
| | } else if (typeof value === 'object' && value !== null) { |
| | cliLine += handleObjectParameter( |
| | value as NestedObjectParameter, |
| | (nextKey: string) => `${transformKey(key)}[${nextKey}]`, |
| | ) |
| | } else { |
| | cliLine += handleSingleParameter(key, value, transformKey) |
| | } |
| | } |
| | return cliLine |
| | } |
| | |
| | /* |
| | 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, |
| | currentVersion: string, |
| | allVersions: Record<string, VersionItem>, |
| | ) { |
| | // Check if we should omit authentication for this operation |
| | const omitAuth = shouldOmitAuthentication(operation, currentVersion) |
| | const parameters: { [key: string]: string | object } = {} |
| | |
| | if (codeSample.request) { |
| | Object.assign(parameters, codeSample.request.parameters) |
| | // Most of the time the example body parameters have a name and value |
| | // and are included in an object. But, some cases are a single value |
| | // and the type is a string. |
| | if ( |
| | codeSample.request.bodyParameters && |
| | typeof codeSample.request.bodyParameters !== 'object' |
| | ) { |
| | parameters.data = codeSample.request.bodyParameters |
| | } else { |
| | Object.assign(parameters, codeSample.request.bodyParameters) |
| | } |
| | } |
| | |
| | let queryParameters = '' |
| | |
| | // Query parameters are set automatically for GET and HEAD requests, we |
| | // otherwise have to handle it ourselves for other request methods by adding |
| | // the parameters to the request path in URL template format e.g.: |
| | // |
| | // 'POST /repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}' |
| | if ( |
| | operation.verb === 'delete' || |
| | operation.verb === 'patch' || |
| | operation.verb === 'post' || |
| | operation.verb === 'put' |
| | ) { |
| | const queryParams = operation.parameters |
| | .filter((param) => { |
| | return param.in === 'query' |
| | }) |
| | .map((param) => { |
| | return param.name |
| | }) |
| | if (queryParams.length > 0) { |
| | queryParameters = `{?${queryParams.join(',')}}` |
| | } |
| | } |
| | |
| | if ( |
| | allVersions[currentVersion].apiVersions.length > 0 && |
| | allVersions[currentVersion].latestApiVersion |
| | ) { |
| | parameters.headers = { |
| | 'X-GitHub-Api-Version': `${allVersions[currentVersion].latestApiVersion}`, |
| | } |
| | } |
| | |
| | const comment = ` |
| | const authOctokit = `const octokit = new Octokit(${stringify({ auth: 'YOUR-TOKEN' }, null, 2)})\n\n` |
| | const unauthenticatedOctokit = `const octokit = new Octokit()\n\n` |
| | const oauthOctokit = `import { createOAuthAppAuth } from "@octokit/auth-oauth-app"\n\nconst octokit = new Octokit({\n authStrategy: createOAuthAppAuth,\n auth:{\n clientType: 'oauth-app',\n clientId: '<YOUR_CLIENT ID>',\n clientSecret: '<YOUR_CLIENT SECRET>'\n }\n})\n\n` |
| | const isBasicAuth = operation?.progAccess?.basicAuth |
| | let authString = isBasicAuth ? oauthOctokit : authOctokit |
| |
|
| | |
| | if (omitAuth) { |
| | authString = unauthenticatedOctokit |
| | } |
| |
|
| | return `${comment}${authString}await octokit.request('${operation.verb.toUpperCase()} ${ |
| | operation.requestPath |
| | }${queryParameters}', ${stringify(parameters, null, 2)})` |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function findMatchingQueryKey(exampleObj: CodeExamples | CodeExamples[], matchKey: string) { |
| | let match: string | null = null |
| | JSON.stringify(exampleObj, (_, nestedValue) => { |
| | if (nestedValue && Object.prototype.hasOwnProperty.call(nestedValue, matchKey)) { |
| | match = nestedValue[matchKey] |
| | } |
| |
|
| | return nestedValue |
| | }) |
| |
|
| | return match |
| | } |
| |
|
| | function getRequiredQueryParamsPath(operation: Operation, codeSample: CodeSample) { |
| | const requiredQueryParams = new URLSearchParams() |
| | for (const param of operation.parameters) { |
| | if (param.in === 'query' && param.required === true) { |
| | const codeExamples = codeSample.response?.example |
| | const match = findMatchingQueryKey(codeExamples, param.name) |
| | requiredQueryParams.append(param.name, match || param.name.toUpperCase()) |
| | } |
| | } |
| |
|
| | return requiredQueryParams.toString() |
| | } |
| |
|
| | function getAcceptHeader(codeSample: CodeSample) { |
| | const contentType = codeSample?.response?.contentType |
| |
|
| | if (!contentType || contentType === 'application/json') { |
| | return 'application/vnd.github+json' |
| | } |
| |
|
| | return contentType |
| | } |
| |
|