-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathget-body-params.ts
More file actions
327 lines (308 loc) · 12.3 KB
/
get-body-params.ts
File metadata and controls
327 lines (308 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import { renderContent } from './render-content'
interface Schema {
oneOf?: any[]
type?: string
items?: any
properties?: Record<string, any>
required?: string[]
additionalProperties?: any
description?: string
enum?: string[]
nullable?: boolean
allOf?: any[]
anyOf?: any[]
[key: string]: any
}
export interface TransformedParam {
type: string
name: string
description: string
isRequired?: boolean
in?: string
childParamsGroups?: TransformedParam[]
enum?: string[]
oneOfObject?: boolean
default?: any
}
interface BodyParamProps {
paramKey?: string
required?: string[]
childParamsGroups?: TransformedParam[]
topLevel?: boolean
}
// 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: Schema,
): Promise<{ properties: Record<string, any>; required: string[] }> {
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.
// Helper function to handle oneOf fields where all items are objects
async function handleObjectOnlyOneOf(
param: Schema,
paramType: string[],
): Promise<TransformedParam[]> {
if (param.oneOf && param.oneOf.every((object: TransformedParam) => object.type === 'object')) {
paramType.push('object')
param.oneOfObject = true
return await getOneOfChildParams(param)
}
return []
}
export async function getBodyParams(schema: Schema, topLevel = false): Promise<TransformedParam[]> {
const bodyParametersParsed: TransformedParam[] = []
const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema
const properties = 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: TransformedParam[] = []
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: TransformedParam[] = []
// 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: TransformedParam = {
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: [],
}
if (keyParam.childParamsGroups) {
keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false)))
}
childParamsGroups.push(keyParam)
} else if (paramType.includes('array') && param.items) {
if (param.items.oneOf) {
if (param.items.oneOf.every((object: TransformedParam) => object.type === 'object')) {
paramType.splice(paramType.indexOf('array'), 1, `array of objects`)
param.oneOfObject = true
childParamsGroups.push(...(await getOneOfChildParams(param.items)))
}
} else {
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)))
}
if (arrayType === 'string' && param.items.enum) {
param.description += `${
param.description ? '\n' : ''
}Supported values are: ${param.items.enum.map((lang: string) => `<code>${lang}</code>`).join(', ')}`
}
}
} else if (paramType.includes('object')) {
if (param.oneOf) {
const oneOfChildren = await handleObjectOnlyOneOf(param, paramType)
if (oneOfChildren.length > 0) {
childParamsGroups.push(...oneOfChildren)
}
} else {
childParamsGroups.push(...(await getBodyParams(param, false)))
}
} else if (param.oneOf) {
// Check if all oneOf items are objects - if so, treat this as a oneOfObject case
const oneOfChildren = await handleObjectOnlyOneOf(param, paramType)
if (oneOfChildren.length > 0) {
childParamsGroups.push(...oneOfChildren)
} else {
// Handle mixed types or non-object oneOf cases
const descriptions: { type: string; description: string }[] = []
for (const childParam of param.oneOf) {
paramType.push(childParam.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 })
}
}
} else {
descriptions.push({ type: param.type, description: param.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 anyOf
// for a body parameter. We use the first object in the list of the anyOf array.
// 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.anyOf && Object.keys(param).length === 1) {
const firstObject = Object.values(param.anyOf).find(
(item) => (item as Schema).type === 'object',
) as Schema
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
}
// Used only for webhooks handling allOf
} else if (param.allOf) {
for (const prop of param.allOf) {
paramType.push('object')
childParamsGroups.push(...(await getBodyParams(prop, false)))
}
}
const paramDecorated = await getTransformedParam(param, paramType, {
paramKey,
required,
childParamsGroups,
topLevel,
})
bodyParametersParsed.push(paramDecorated)
}
return bodyParametersParsed
}
async function getTransformedParam(
param: Schema,
paramType: string[],
props: BodyParamProps,
): Promise<TransformedParam> {
const { paramKey, required, childParamsGroups, topLevel } = props
const paramDecorated: TransformedParam = {} as TransformedParam
// 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 = Array.from(new Set(paramType.filter(Boolean))).join(' or ')
paramDecorated.name = paramKey || ''
if (topLevel) {
paramDecorated.in = 'body'
}
paramDecorated.description = await renderContent(param.description || '')
if (required && required.includes(paramKey || '')) {
paramDecorated.isRequired = true
}
if (childParamsGroups && childParamsGroups.length > 0 && !param.oneOfObject) {
// Since the allOf properties can have multiple duplicate properties we want to get rid of the duplicates with the same name, but keep the
// the one that has isRequired set to true.
const mergedChildParamsGroups = Array.from(
childParamsGroups
.reduce((childParam, obj) => {
const curr = childParam.get(obj.name)
return childParam.set(
obj.name,
curr ? (!Object.hasOwn(curr, 'isRequired') ? obj : curr) : obj,
)
}, new Map<string, TransformedParam>())
.values(),
)
paramDecorated.childParamsGroups = mergedChildParamsGroups
} else if (childParamsGroups && childParamsGroups.length > 0) {
paramDecorated.childParamsGroups = childParamsGroups
}
if (param.enum) {
paramDecorated.enum = param.enum
}
if (param.oneOfObject) {
paramDecorated.oneOfObject = true
}
if (param.default !== undefined) {
paramDecorated.default = param.default
}
return paramDecorated
}
async function getOneOfChildParams(param: Schema): Promise<TransformedParam[]> {
const childParamsGroups: TransformedParam[] = []
if (!param.oneOf) {
return childParamsGroups
}
for (const oneOfParam of param.oneOf) {
const objParam: TransformedParam = {
type: 'object',
name: oneOfParam.title,
description: await renderContent(oneOfParam.description),
isRequired: oneOfParam.required,
childParamsGroups: [],
}
if (objParam.childParamsGroups) {
objParam.childParamsGroups.push(...(await getBodyParams(oneOfParam, false)))
}
childParamsGroups.push(objParam)
}
return childParamsGroups
}