// Based on https://github.com/github/docs/blob/9be5ebb4855be066ac22a3ae0dafb9d615c7dfb4/src/rest/scripts/utils/get-body-params.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: any) { 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: any) => 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: any, topLevel = false): Promise { const bodyParametersParsed = []; const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema; const properties: Record = schemaObject.properties || {}; const required = schemaObject.required || []; // Most operation requestBody schemas are objects. When the type is an array, // there will not be properties on the `schema` object. if (topLevel && schema.type === "array") { const childParamsGroups = []; const arrayType = schema.items.type; const paramType = [schema.type]; if (arrayType === "object") { childParamsGroups.push(...(await getBodyParams(schema.items, false))); } else { paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`); } const paramDecorated = await getTransformedParam(schema, paramType, { required, topLevel, childParamsGroups }); return [paramDecorated]; } for (const [paramKey, param] of Object.entries(properties)) { // 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: `A user-defined key to represent an item in \`${paramKey}\`.`, isRequired: param.required, enum: param.enum, default: param.default, childParamsGroups: [] as any[] }; keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false))); 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))); } } else if (paramType && paramType.includes("object")) { childParamsGroups.push(...(await getBodyParams(param, false))); } 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; // This is a workaround for an operation that incorrectly defines allOf for a // body parameter. As a workaround, we will use the first object in the list of // the allOf array. Otherwise, fallback to the first item in the array. // This isn't ideal, and in the case of an actual allOf occurrence, we should // handle it differently by merging all of the properties. There is currently // only one occurrence for the operation id repos/update-information-about-pages-site // See Ecosystem API issue number #3332 for future plans to fix this in the OpenAPI } else if (param && param.anyOf && Object.keys(param).length === 1) { const firstObject = Object.values(param.anyOf).find((item: any) => item.type === "object") as { description: string; required: boolean; }; if (firstObject) { paramType.push("object"); param.description = firstObject.description; param.isRequired = firstObject.required; childParamsGroups.push(...(await getBodyParams(firstObject, false))); } else { paramType.push(param.anyOf[0].type); param.description = param.anyOf[0].description; param.isRequired = param.anyOf[0].required; } } else if (param && param.allOf && param.allOf.length > 0) { paramType.push("object"); const firstObject = param.allOf[0]; param.description = firstObject.description; param.isRequired = firstObject.required; for (const a of param.allOf) { childParamsGroups.push(...(await getBodyParams(a, false))); } } const paramDecorated = await getTransformedParam(param, paramType, { paramKey, required, childParamsGroups, topLevel }); bodyParametersParsed.push(paramDecorated); } return bodyParametersParsed; } type Param = { type: "string" | "string or null" | "number" | "boolean" | "array" | "object" | "object or null" | "integer or null"; name: string; in: string; isRequired: boolean; description: string; childParamsGroups?: Param[]; enum?: string[]; default: any; }; async function getTransformedParam(param: any, paramType: any, props: any) { const {paramKey, required, childParamsGroups, topLevel} = props; const paramDecorated = {} as Param; // 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 = param.description; if (required && required.includes(paramKey)) { paramDecorated.isRequired = true; } if (childParamsGroups && childParamsGroups.length > 0) { paramDecorated.childParamsGroups = childParamsGroups; } if (param.enum) { paramDecorated.enum = param.enum; } // we also want to catch default values of `false` for booleans if (param.default !== undefined) { paramDecorated.default = param.default; } return paramDecorated; }