You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
13 KiB
388 lines
13 KiB
3 years ago
|
'use strict'
|
||
|
|
||
|
const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
|
||
|
const { xResponseDescription, xConsume } = require('../../constants')
|
||
|
const { rawRequired } = require('../../symbols')
|
||
|
|
||
|
function prepareDefaultOptions (opts) {
|
||
|
const openapi = opts.openapi
|
||
|
const info = openapi.info || null
|
||
|
const servers = openapi.servers || null
|
||
|
const components = openapi.components || null
|
||
|
const security = openapi.security || null
|
||
|
const tags = openapi.tags || null
|
||
|
const externalDocs = openapi.externalDocs || null
|
||
|
const stripBasePath = opts.stripBasePath
|
||
|
const transform = opts.transform
|
||
|
const hiddenTag = opts.hiddenTag
|
||
|
const hideUntagged = opts.hideUntagged
|
||
|
const extensions = []
|
||
|
|
||
|
for (const [key, value] of Object.entries(opts.openapi)) {
|
||
|
if (key.startsWith('x-')) {
|
||
|
extensions.push([key, value])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
info,
|
||
|
servers,
|
||
|
components,
|
||
|
security,
|
||
|
tags,
|
||
|
externalDocs,
|
||
|
stripBasePath,
|
||
|
transform,
|
||
|
hiddenTag,
|
||
|
extensions,
|
||
|
hideUntagged
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function prepareOpenapiObject (opts) {
|
||
|
const pkg = readPackageJson()
|
||
|
const openapiObject = {
|
||
|
openapi: '3.0.3',
|
||
|
info: {
|
||
|
version: pkg.version || '1.0.0',
|
||
|
title: pkg.name || ''
|
||
|
},
|
||
|
components: { schemas: {} },
|
||
|
paths: {}
|
||
|
}
|
||
|
|
||
|
if (opts.info) openapiObject.info = opts.info
|
||
|
if (opts.servers) openapiObject.servers = opts.servers
|
||
|
if (opts.components) openapiObject.components = Object.assign({}, opts.components, { schemas: Object.assign({}, opts.components.schemas) })
|
||
|
if (opts.security) openapiObject.security = opts.security
|
||
|
if (opts.tags) openapiObject.tags = opts.tags
|
||
|
if (opts.externalDocs) openapiObject.externalDocs = opts.externalDocs
|
||
|
|
||
|
for (const [key, value] of opts.extensions) {
|
||
|
// "x-" extension can not be typed
|
||
|
openapiObject[key] = value
|
||
|
}
|
||
|
|
||
|
return openapiObject
|
||
|
}
|
||
|
|
||
|
function normalizeUrl (url, servers, stripBasePath) {
|
||
|
if (!stripBasePath) return formatParamUrl(url)
|
||
|
servers = Array.isArray(servers) ? servers : []
|
||
|
servers.forEach(function (server) {
|
||
|
const basePath = new URL(server.url).pathname
|
||
|
if (url.startsWith(basePath) && basePath !== '/') {
|
||
|
url = url.replace(basePath, '')
|
||
|
}
|
||
|
})
|
||
|
return formatParamUrl(url)
|
||
|
}
|
||
|
|
||
|
function transformDefsToComponents (jsonSchema) {
|
||
|
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
|
||
|
// Handle patternProperties, that is not part of OpenAPI definitions
|
||
|
if (jsonSchema.patternProperties) {
|
||
|
jsonSchema.additionalProperties = Object.values(jsonSchema.patternProperties)[0]
|
||
|
delete jsonSchema.patternProperties
|
||
|
} else if (jsonSchema.const) {
|
||
|
// OAS 3.1 supports `const` but it is not supported by `swagger-ui`
|
||
|
// https://swagger.io/docs/specification/data-models/keywords/
|
||
|
jsonSchema.enum = [jsonSchema.const]
|
||
|
delete jsonSchema.const
|
||
|
}
|
||
|
|
||
|
Object.keys(jsonSchema).forEach(function (key) {
|
||
|
if (key === 'properties') {
|
||
|
Object.keys(jsonSchema[key]).forEach(function (prop) {
|
||
|
jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop])
|
||
|
})
|
||
|
} else if (key === '$ref') {
|
||
|
jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas')
|
||
|
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) {
|
||
|
jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples)
|
||
|
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) {
|
||
|
jsonSchema.example = jsonSchema[key][0]
|
||
|
delete jsonSchema[key]
|
||
|
} else if (key === '$id' || key === '$schema') {
|
||
|
delete jsonSchema[key]
|
||
|
} else {
|
||
|
jsonSchema[key] = transformDefsToComponents(jsonSchema[key])
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
return jsonSchema
|
||
|
}
|
||
|
|
||
|
function convertExamplesArrayToObject (examples) {
|
||
|
return examples.reduce((examplesObject, example, index) => {
|
||
|
if (typeof example === 'object') {
|
||
|
examplesObject['example' + (index + 1)] = { value: example }
|
||
|
} else {
|
||
|
examplesObject[example] = { value: example }
|
||
|
}
|
||
|
|
||
|
return examplesObject
|
||
|
}, {})
|
||
|
}
|
||
|
|
||
|
// For supported keys read:
|
||
|
// https://swagger.io/docs/specification/describing-parameters/
|
||
|
function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, securityIgnores = []) {
|
||
|
const obj = transformDefsToComponents(resolveLocalRef(jsonSchema, externalSchemas))
|
||
|
let toOpenapiProp
|
||
|
switch (container) {
|
||
|
case 'cookie':
|
||
|
case 'query':
|
||
|
toOpenapiProp = function (propertyName, jsonSchemaElement) {
|
||
|
const result = {
|
||
|
in: container,
|
||
|
name: propertyName,
|
||
|
required: jsonSchemaElement.required
|
||
|
}
|
||
|
// complex serialization in query or cookie, eg. JSON
|
||
|
// https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
|
||
|
if (jsonSchemaElement[xConsume]) {
|
||
|
result.content = {
|
||
|
[jsonSchemaElement[xConsume]]: {
|
||
|
schema: {
|
||
|
...jsonSchemaElement,
|
||
|
required: jsonSchemaElement[rawRequired]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
|
||
|
} else {
|
||
|
result.schema = jsonSchemaElement
|
||
|
}
|
||
|
// description should be optional
|
||
|
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
|
||
|
// optionally add serialization format style
|
||
|
if (jsonSchema.style) result.style = jsonSchema.style
|
||
|
if (jsonSchema.explode != null) result.explode = jsonSchema.explode
|
||
|
return result
|
||
|
}
|
||
|
break
|
||
|
case 'path':
|
||
|
toOpenapiProp = function (propertyName, jsonSchemaElement) {
|
||
|
const result = {
|
||
|
in: container,
|
||
|
name: propertyName,
|
||
|
required: true,
|
||
|
schema: jsonSchemaElement
|
||
|
}
|
||
|
// description should be optional
|
||
|
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
|
||
|
return result
|
||
|
}
|
||
|
break
|
||
|
case 'header':
|
||
|
toOpenapiProp = function (propertyName, jsonSchemaElement) {
|
||
|
return {
|
||
|
in: 'header',
|
||
|
name: propertyName,
|
||
|
required: jsonSchemaElement.required,
|
||
|
description: jsonSchemaElement.description,
|
||
|
schema: {
|
||
|
type: jsonSchemaElement.type
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
return Object.keys(obj)
|
||
|
.filter((propKey) => (!securityIgnores.includes(propKey)))
|
||
|
.map((propKey) => {
|
||
|
const jsonSchema = toOpenapiProp(propKey, obj[propKey])
|
||
|
if (jsonSchema.schema) {
|
||
|
// it is needed as required in schema is invalid prop - delete only if needed
|
||
|
if (typeof jsonSchema.schema.required !== 'undefined') delete jsonSchema.schema.required
|
||
|
// it is needed as description in schema is invalid prop - delete only if needed
|
||
|
if (typeof jsonSchema.schema.description !== 'undefined') delete jsonSchema.schema.description
|
||
|
}
|
||
|
return jsonSchema
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function resolveBodyParams (body, schema, consumes, ref) {
|
||
|
const resolved = transformDefsToComponents(ref.resolve(schema))
|
||
|
if ((Array.isArray(consumes) && consumes.length === 0) || typeof consumes === 'undefined') {
|
||
|
consumes = ['application/json']
|
||
|
}
|
||
|
|
||
|
consumes.forEach((consume) => {
|
||
|
body.content[consume] = {
|
||
|
schema: resolved
|
||
|
}
|
||
|
})
|
||
|
if (resolved && resolved.required && resolved.required.length) {
|
||
|
body.required = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
|
||
|
const schemasPath = '#/components/schemas/'
|
||
|
let resolved = transformDefsToComponents(ref.resolve(schema))
|
||
|
|
||
|
// if the resolved definition is in global schema
|
||
|
if (resolved.$ref && resolved.$ref.startsWith(schemasPath)) {
|
||
|
const parts = resolved.$ref.split(schemasPath)
|
||
|
const pathParts = parts[1].split('/')
|
||
|
resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions)
|
||
|
}
|
||
|
|
||
|
const arr = plainJsonObjectToOpenapi3(container, resolved, sharedSchemas, securityIgnores)
|
||
|
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
|
||
|
}
|
||
|
|
||
|
// https://swagger.io/docs/specification/describing-responses/
|
||
|
function resolveResponse (fastifyResponseJson, produces, ref) {
|
||
|
// if the user does not provided an out schema
|
||
|
if (!fastifyResponseJson) {
|
||
|
return { 200: { description: 'Default Response' } }
|
||
|
}
|
||
|
|
||
|
const responsesContainer = {}
|
||
|
|
||
|
const statusCodes = Object.keys(fastifyResponseJson)
|
||
|
|
||
|
statusCodes.forEach(statusCode => {
|
||
|
const rawJsonSchema = fastifyResponseJson[statusCode]
|
||
|
const resolved = transformDefsToComponents(ref.resolve(rawJsonSchema))
|
||
|
|
||
|
/**
|
||
|
* 2xx require to be all upper-case
|
||
|
* converts statusCode to upper case only when it is not "default"
|
||
|
*/
|
||
|
if (statusCode !== 'default') {
|
||
|
statusCode = statusCode.toUpperCase()
|
||
|
}
|
||
|
|
||
|
const response = {
|
||
|
description: resolved[xResponseDescription] || rawJsonSchema.description || 'Default Response'
|
||
|
}
|
||
|
|
||
|
// add headers when there are any.
|
||
|
if (rawJsonSchema.headers) {
|
||
|
response.headers = {}
|
||
|
Object.keys(rawJsonSchema.headers).forEach(function (key) {
|
||
|
const header = {
|
||
|
schema: rawJsonSchema.headers[key]
|
||
|
}
|
||
|
|
||
|
if (rawJsonSchema.headers[key].description) {
|
||
|
header.description = rawJsonSchema.headers[key].description
|
||
|
// remove invalid field
|
||
|
delete header.schema.description
|
||
|
}
|
||
|
|
||
|
response.headers[key] = header
|
||
|
})
|
||
|
// remove invalid field
|
||
|
delete resolved.headers
|
||
|
}
|
||
|
|
||
|
// add schema when type is not 'null'
|
||
|
if (rawJsonSchema.type !== 'null') {
|
||
|
const content = {}
|
||
|
|
||
|
if ((Array.isArray(produces) && produces.length === 0) || typeof produces === 'undefined') {
|
||
|
produces = ['application/json']
|
||
|
}
|
||
|
|
||
|
delete resolved[xResponseDescription]
|
||
|
produces.forEach((produce) => {
|
||
|
content[produce] = {
|
||
|
schema: resolved
|
||
|
}
|
||
|
})
|
||
|
|
||
|
response.content = content
|
||
|
}
|
||
|
|
||
|
responsesContainer[statusCode] = response
|
||
|
})
|
||
|
|
||
|
return responsesContainer
|
||
|
}
|
||
|
|
||
|
function prepareOpenapiMethod (schema, ref, openapiObject) {
|
||
|
const openapiMethod = {}
|
||
|
const parameters = []
|
||
|
|
||
|
// Parse out the security prop keys to ignore
|
||
|
const securityIgnores = [
|
||
|
...(openapiObject && openapiObject.security ? openapiObject.security : []),
|
||
|
...(schema && schema.security ? schema.security : [])
|
||
|
]
|
||
|
.reduce((acc, securitySchemeGroup) => {
|
||
|
Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
|
||
|
const { name, in: category } = openapiObject.components.securitySchemes[securitySchemeLabel]
|
||
|
if (!acc[category]) {
|
||
|
acc[category] = []
|
||
|
}
|
||
|
acc[category].push(name)
|
||
|
})
|
||
|
return acc
|
||
|
}, {})
|
||
|
|
||
|
// All the data the user can give us, is via the schema object
|
||
|
if (schema) {
|
||
|
if (schema.operationId) openapiMethod.operationId = schema.operationId
|
||
|
if (schema.summary) openapiMethod.summary = schema.summary
|
||
|
if (schema.tags) openapiMethod.tags = schema.tags
|
||
|
if (schema.description) openapiMethod.description = schema.description
|
||
|
if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs
|
||
|
if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
|
||
|
if (schema.body) {
|
||
|
openapiMethod.requestBody = { content: {} }
|
||
|
resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref)
|
||
|
}
|
||
|
if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions)
|
||
|
if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
|
||
|
// TODO: need to documentation, we treat it same as the querystring
|
||
|
// fastify do not support cookies schema in first place
|
||
|
if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
|
||
|
if (parameters.length > 0) openapiMethod.parameters = parameters
|
||
|
if (schema.deprecated) openapiMethod.deprecated = schema.deprecated
|
||
|
if (schema.security) openapiMethod.security = schema.security
|
||
|
if (schema.servers) openapiMethod.servers = schema.servers
|
||
|
for (const key of Object.keys(schema)) {
|
||
|
if (key.startsWith('x-')) {
|
||
|
openapiMethod[key] = schema[key]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
openapiMethod.responses = resolveResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)
|
||
|
|
||
|
return openapiMethod
|
||
|
}
|
||
|
|
||
|
function prepareOpenapiSchemas (schemas, ref) {
|
||
|
return Object.entries(schemas)
|
||
|
.reduce((res, [name, schema]) => {
|
||
|
const _ = { ...schema }
|
||
|
const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] }))
|
||
|
|
||
|
// Swagger doesn't accept $id on /definitions schemas.
|
||
|
// The $ids are needed by Ref() to check the URI so we need
|
||
|
// to remove them at the end of the process
|
||
|
// definitions are added by resolve but they are replace by components.schemas
|
||
|
delete resolved.$id
|
||
|
delete resolved.definitions
|
||
|
|
||
|
res[name] = resolved
|
||
|
return res
|
||
|
}, {})
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
prepareDefaultOptions,
|
||
|
prepareOpenapiObject,
|
||
|
prepareOpenapiMethod,
|
||
|
prepareOpenapiSchemas,
|
||
|
normalizeUrl
|
||
|
}
|