From e01cd7ba3f0972cf3ba75f87bd3f35b2d5c1683a Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Thu, 18 Apr 2024 22:33:08 -0700 Subject: [PATCH] refactor: json schema draft 7 to openapi schema converting (#797) --- lib/spec/openapi/utils.js | 129 +++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index b076e2b8..83d611fc 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -115,36 +115,6 @@ function resolveServerUrls (servers) { return resolvedUrls } -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 !== undefined) { - // 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 === '$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') { @@ -160,7 +130,7 @@ function convertExamplesArrayToObject (examples) { // For supported keys read: // https://swagger.io/docs/specification/describing-parameters/ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, securityIgnores = []) { - const obj = transformDefsToComponents(resolveLocalRef(jsonSchema, externalSchemas)) + const obj = convertJsonSchemaToOpenapi3(resolveLocalRef(jsonSchema, externalSchemas)) let toOpenapiProp switch (container) { case 'cookie': @@ -292,7 +262,7 @@ function schemaToMediaRecursive (schema) { } function resolveBodyParams (body, schema, consumes, ref) { - const resolved = transformDefsToComponents(ref.resolve(schema)) + const resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema)) if ((Array.isArray(consumes) && consumes.length === 0) || consumes === undefined) { consumes = ['application/json'] } @@ -313,7 +283,7 @@ function resolveBodyParams (body, schema, consumes, ref) { function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) { const schemasPath = '#/components/schemas/' - let resolved = transformDefsToComponents(ref.resolve(schema)) + let resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema)) // if the resolved definition is in global schema if (resolved.$ref && resolved.$ref.startsWith(schemasPath)) { @@ -339,7 +309,7 @@ function resolveResponse (fastifyResponseJson, produces, ref) { statusCodes.forEach(statusCode => { const rawJsonSchema = fastifyResponseJson[statusCode] - const resolved = transformDefsToComponents(ref.resolve(rawJsonSchema)) + const resolved = convertJsonSchemaToOpenapi3(ref.resolve(rawJsonSchema)) /** * 2xx require to be all upper-case @@ -465,23 +435,80 @@ function prepareOpenapiMethod (schema, ref, openapiObject, url) { return openapiMethod } -function prepareOpenapiSchemas (schemas, ref) { - return Object.entries(schemas) - .reduce((res, [name, schema]) => { - const _ = { ...schema } - const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) - resolveSchemaExamplesRecursive(resolved) - - // 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 - }, {}) +function convertJsonSchemaToOpenapi3 (jsonSchema) { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + return jsonSchema + } + + if (Array.isArray(jsonSchema)) { + return jsonSchema.map(convertJsonSchemaToOpenapi3) + } + + const openapiSchema = { ...jsonSchema } + + for (const key of Object.keys(openapiSchema)) { + const value = openapiSchema[key] + + if (key === '$id' || key === '$schema' || key === 'definitions') { + // TODO: this breaks references to the definition properties + delete openapiSchema[key] + continue + } + + if (key === '$ref') { + openapiSchema.$ref = value.replace('definitions', 'components/schemas') + continue + } + + if (key === 'const') { + // OAS 3.1 supports `const` but it is not supported by `swagger-ui` + // https://swagger.io/docs/specification/data-models/keywords/ + // TODO: check if enum property already exists + // TODO: this breaks references to the const property + openapiSchema.enum = [openapiSchema.const] + delete openapiSchema.const + continue + } + + if (key === 'patternProperties') { + // TODO: check if additionalProperties property already exists + // TODO: this breaks references to the additionalProperties properties + // TODO: patternProperties actually allowed in the openapi schema, but should + // always start with "x-" prefix + openapiSchema.additionalProperties = Object.values(openapiSchema.patternProperties)[0] + delete openapiSchema.patternProperties + continue + } + + if (key === 'properties') { + openapiSchema[key] = {} + for (const propertyName of Object.keys(value)) { + const propertyJsonSchema = value[propertyName] + const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(propertyJsonSchema) + openapiSchema[key][propertyName] = propertyOpenapiSchema + } + continue + } + + openapiSchema[key] = convertJsonSchemaToOpenapi3(value) + } + + return openapiSchema +} + +function prepareOpenapiSchemas (jsonSchemas, ref) { + const openapiSchemas = {} + + for (const schemaName of Object.keys(jsonSchemas)) { + const jsonSchema = { ...jsonSchemas[schemaName] } + + const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] }) + const openapiSchema = convertJsonSchemaToOpenapi3(resolvedJsonSchema) + resolveSchemaExamplesRecursive(openapiSchema) + + openapiSchemas[schemaName] = openapiSchema + } + return openapiSchemas } module.exports = {