diff --git a/script/rest/utils/get-body-params.js b/script/rest/utils/get-body-params.js new file mode 100644 index 0000000000..9595dbeb72 --- /dev/null +++ b/script/rest/utils/get-body-params.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node +import renderContent from '../../../lib/render-content/index.js' + +// If there is a oneOf at the top level, then we have to present just one +// in the docs. We don't currently have a convention for showing more than one +// set of input parameters in the docs. Having a top-level oneOf is also very +// uncommon. +// Currently there aren't very many operations that require this treatment. +// As an example, the 'Add status check contexts' and 'Set status check contexts' +// operations have a top-level oneOf. + +async function getTopLevelOneOfProperty(schema) { + if (!schema.oneOf) { + throw new Error('Schema does not have a requestBody oneOf property defined') + } + if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) { + throw new Error('Schema requestBody oneOf property is not an array') + } + // When a oneOf exists but the `type` differs, the case has historically + // been that the alternate option is an array, where the first option + // is the array as a property of the object. We need to ensure that the + // first option listed is the most comprehensive and preferred option. + const firstOneOfObject = schema.oneOf[0] + const allOneOfAreObjects = schema.oneOf.every((elem) => elem.type === 'object') + let required = firstOneOfObject.required || [] + let properties = firstOneOfObject.properties || {} + + // When all of the oneOf objects have the `type: object` we + // need to display all of the parameters. + // This merges all of the properties and required values. + if (allOneOfAreObjects) { + for (const each of schema.oneOf.slice(1)) { + Object.assign(firstOneOfObject.properties, each.properties) + required = firstOneOfObject.required.concat(each.required) + } + properties = firstOneOfObject.properties + } + return { properties, required } +} + +// Gets the body parameters for a given schema recursively. +export async function getBodyParams(schema, topLevel = false, summary = '', depth = 1) { + if (summary && depth > 3) console.log(depth, summary) + const bodyParametersParsed = [] + const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema + const properties = schemaObject.properties || {} + const required = schemaObject.required || [] + + for (const [paramKey, param] of Object.entries(properties)) { + const paramDecorated = {} + + // OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1 + // will either be a single value or an array of values. + // This makes type an array regardless of how many values the array + // includes. This allows us to support 3.1 while remaining backwards + // compatible with 3.0. + const paramType = Array.isArray(param.type) ? param.type : [param.type] + const additionalPropertiesType = param.additionalProperties + ? Array.isArray(param.additionalProperties.type) + ? param.additionalProperties.type + : [param.additionalProperties.type] + : [] + const childParamsGroups = [] + + // If the parameter is an array or object there may be child params + // If the parameter has oneOf or additionalProperties, they need to be + // recursively read too. + + // There are a couple operations with additionalProperties, which allows + // the api to define input parameters with the type dictionary. These are the only + // two operations (at the time of adding this code) that use additionalProperties + // Create a snapshot of dependencies for a repository + // Update a gist + if (param.additionalProperties && additionalPropertiesType.includes('object')) { + const keyParam = { + type: 'object', + name: 'key', + description: await renderContent( + `A user-defined key to represent an item in \`${paramKey}\`.` + ), + isRequired: param.required, + enum: param.enum, + default: param.default, + childParamsGroups: [], + } + keyParam.childParamsGroups.push( + ...(await getBodyParams(param.additionalProperties, false, summary, depth + 1)) + ) + childParamsGroups.push(keyParam) + } else if (paramType && paramType.includes('array')) { + const arrayType = param.items.type + if (arrayType) { + paramType.splice(paramType.indexOf('array'), 1, `array of ${arrayType}s`) + } + if (arrayType === 'object') { + childParamsGroups.push(...(await getBodyParams(param.items, false, summary, depth + 1))) + } + } else if (paramType && paramType.includes('object')) { + childParamsGroups.push(...(await getBodyParams(param, false, summary, depth + 1))) + } else if (param && param.oneOf) { + // get concatenated description and type + const descriptions = [] + for (const childParam of param.oneOf) { + paramType.push(childParam.type) + // If there is no parent description, create a description from + // each type + if (!param.description) { + if (childParam.type === 'array') { + if (childParam.items.description) { + descriptions.push({ + type: childParam.type, + description: childParam.items.description, + }) + } + } else { + if (childParam.description) { + descriptions.push({ type: childParam.type, description: childParam.description }) + } + } + } + } + // Occasionally, there is no parent description and the description + // is in the first child parameter. + const oneOfDescriptions = descriptions.length ? descriptions[0].description : '' + if (!param.description) param.description = oneOfDescriptions + } + + // Supports backwards compatibility for OpenAPI 3.0 + // In 3.1 a nullable type is part of the param.type array and + // the property param.nullable does not exist. + if (param.nullable) paramType.push('null') + paramDecorated.type = paramType.filter(Boolean).join(' or ') + paramDecorated.name = paramKey + if (topLevel) { + paramDecorated.in = 'body' + } + paramDecorated.description = await renderContent(param.description) + if (required.includes(paramKey)) { + paramDecorated.isRequired = true + } + if (childParamsGroups.length > 0) { + paramDecorated.childParamsGroups = childParamsGroups + } + if (param.enum) { + paramDecorated.enum = param.enum + } + if (param.default) { + paramDecorated.default = param.default + } + + bodyParametersParsed.push(paramDecorated) + } + + return bodyParametersParsed +} diff --git a/script/rest/utils/operation.js b/script/rest/utils/operation.js index a5941a7a85..88a087d6a4 100644 --- a/script/rest/utils/operation.js +++ b/script/rest/utils/operation.js @@ -8,6 +8,7 @@ import { parseTemplate } from 'url-template' import renderContent from '../../../lib/render-content/index.js' import getCodeSamples from './create-rest-examples.js' import operationSchema from './operation-schema.js' +import { getBodyParams } from './get-body-params.js' const { operationUrls } = JSON.parse( await readFile('script/rest/utils/rest-api-overrides.json', 'utf8') @@ -184,149 +185,3 @@ export default class Operation { ) } } - -// If there is a oneOf at the top level, then we have to present just one -// in the docs. We don't currently have a convention for showing more than one -// set of input parameters in the docs. Having a top-level oneOf is also very -// uncommon. -// Currently there aren't very many operations that require this treatment. -// As an example, the 'Add status check contexts' and 'Set status check contexts' -// operations have a top-level oneOf. -async function getTopLevelOneOfProperty(schema) { - if (!schema.oneOf) { - throw new Error('Schema does not have a requestBody oneOf property defined') - } - if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) { - throw new Error('Schema requestBody oneOf property is not an array') - } - // When a oneOf exists but the `type` differs, the case has historically - // been that the alternate option is an array, where the first option - // is the array as a property of the object. We need to ensure that the - // first option listed is the most comprehensive and preferred option. - const firstOneOfObject = schema.oneOf[0] - const allOneOfAreObjects = schema.oneOf.every((elem) => elem.type === 'object') - let required = firstOneOfObject.required || [] - let properties = firstOneOfObject.properties || {} - - // When all of the oneOf objects have the `type: object` we - // need to display all of the parameters. - // This merges all of the properties and required values. - if (allOneOfAreObjects) { - for (const each of schema.oneOf.slice(1)) { - Object.assign(firstOneOfObject.properties, each.properties) - required = firstOneOfObject.required.concat(each.required) - } - properties = firstOneOfObject.properties - } - return { properties, required } -} - -// Gets the body parameters for a given schema recursively. -async function getBodyParams(schema, topLevel = false) { - const bodyParametersParsed = [] - const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema - const properties = schemaObject.properties || {} - const required = schemaObject.required || [] - - for (const [paramKey, param] of Object.entries(properties)) { - const paramDecorated = {} - - // OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1 - // will either be a single value or an array of values. - // This makes type an array regardless of how many values the array - // includes. This allows us to support 3.1 while remaining backwards - // compatible with 3.0. - const paramType = Array.isArray(param.type) ? param.type : [param.type] - // Supports backwards compatibility for OpenAPI 3.0 - // In 3.1 a nullable type is part of the param.type array and - // the property param.nullable does not exist. - if (param.nullable) paramType.push('null') - - const additionalPropertiesType = param.additionalProperties - ? Array.isArray(param.additionalProperties.type) - ? param.additionalProperties.type - : [param.additionalProperties.type] - : [] - const childParamsGroups = [] - - // If the parameter is an array or object there may be child params - // If the parameter has oneOf or additionalProperties, they need to be - // recursively read too. - - // There are a couple operations with additionalProperties, which allows - // the api to define input parameters with the type dictionary. These are the only - // two operations (at the time of adding this code) that use additionalProperties - // Create a snapshot of dependencies for a repository - // Update a gist - if (param.additionalProperties && additionalPropertiesType.includes('object')) { - const keyParam = { - type: 'object', - name: 'key', - description: `
A user-defined key to represent an item in ${paramKey}.