render existing openapi examples (#26405)
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user