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.
		
		
		
		
		
			
		
			
				
					339 lines
				
				11 KiB
			
		
		
			
		
	
	
					339 lines
				
				11 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common') | ||
|  | const { xResponseDescription, xConsume } = require('../../constants') | ||
|  | 
 | ||
|  | function prepareDefaultOptions (opts) { | ||
|  |   const swagger = opts.swagger | ||
|  |   const info = swagger.info || null | ||
|  |   const host = swagger.host || null | ||
|  |   const schemes = swagger.schemes || null | ||
|  |   const consumes = swagger.consumes || null | ||
|  |   const produces = swagger.produces || null | ||
|  |   const definitions = swagger.definitions || null | ||
|  |   const basePath = swagger.basePath || null | ||
|  |   const securityDefinitions = swagger.securityDefinitions || null | ||
|  |   const security = swagger.security || null | ||
|  |   const tags = swagger.tags || null | ||
|  |   const externalDocs = swagger.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.swagger)) { | ||
|  |     if (key.startsWith('x-')) { | ||
|  |       extensions.push([key, value]) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   return { | ||
|  |     info, | ||
|  |     host, | ||
|  |     schemes, | ||
|  |     consumes, | ||
|  |     produces, | ||
|  |     definitions, | ||
|  |     basePath, | ||
|  |     securityDefinitions, | ||
|  |     security, | ||
|  |     tags, | ||
|  |     externalDocs, | ||
|  |     stripBasePath, | ||
|  |     transform, | ||
|  |     hiddenTag, | ||
|  |     extensions, | ||
|  |     hideUntagged | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function prepareSwaggerObject (opts) { | ||
|  |   const pkg = readPackageJson() | ||
|  |   const swaggerObject = { | ||
|  |     swagger: '2.0', | ||
|  |     info: { | ||
|  |       version: pkg.version || '1.0.0', | ||
|  |       title: pkg.name || '' | ||
|  |     }, | ||
|  |     definitions: {}, | ||
|  |     paths: {} | ||
|  |   } | ||
|  | 
 | ||
|  |   if (opts.info) swaggerObject.info = opts.info | ||
|  |   if (opts.host) swaggerObject.host = opts.host | ||
|  |   if (opts.schemes) swaggerObject.schemes = opts.schemes | ||
|  |   if (opts.basePath) swaggerObject.basePath = opts.basePath | ||
|  |   if (opts.consumes) swaggerObject.consumes = opts.consumes | ||
|  |   if (opts.produces) swaggerObject.produces = opts.produces | ||
|  |   if (opts.definitions) swaggerObject.definitions = opts.definitions | ||
|  |   if (opts.securityDefinitions) swaggerObject.securityDefinitions = opts.securityDefinitions | ||
|  |   if (opts.security) swaggerObject.security = opts.security | ||
|  |   if (opts.tags) swaggerObject.tags = opts.tags | ||
|  |   if (opts.externalDocs) swaggerObject.externalDocs = opts.externalDocs | ||
|  | 
 | ||
|  |   for (const [key, value] of opts.extensions) { | ||
|  |     // "x-" extension can not be typed
 | ||
|  |     swaggerObject[key] = value | ||
|  |   } | ||
|  | 
 | ||
|  |   return swaggerObject | ||
|  | } | ||
|  | 
 | ||
|  | function normalizeUrl (url, basePath, stripBasePath) { | ||
|  |   let path | ||
|  |   if (stripBasePath && url.startsWith(basePath)) { | ||
|  |     path = url.replace(basePath, '') | ||
|  |   } else { | ||
|  |     path = url | ||
|  |   } | ||
|  |   if (!path.startsWith('/')) { | ||
|  |     path = '/' + String(path) | ||
|  |   } | ||
|  |   return formatParamUrl(path) | ||
|  | } | ||
|  | 
 | ||
|  | // For supported keys read:
 | ||
|  | // https://swagger.io/docs/specification/2-0/describing-parameters/
 | ||
|  | function plainJsonObjectToSwagger2 (container, jsonSchema, externalSchemas, securityIgnores = []) { | ||
|  |   const obj = resolveLocalRef(jsonSchema, externalSchemas) | ||
|  |   let toSwaggerProp | ||
|  |   switch (container) { | ||
|  |     case 'query': | ||
|  |       toSwaggerProp = function (propertyName, jsonSchemaElement) { | ||
|  |         // complex serialization is not supported by swagger
 | ||
|  |         if (jsonSchemaElement[xConsume]) { | ||
|  |           throw new Error('Complex serialization is not supported by Swagger. ' + | ||
|  |             'Remove "' + xConsume + '" for "' + propertyName + '" querystring schema or ' + | ||
|  |             'change specification to OpenAPI') | ||
|  |         } | ||
|  |         jsonSchemaElement.in = container | ||
|  |         jsonSchemaElement.name = propertyName | ||
|  |         return jsonSchemaElement | ||
|  |       } | ||
|  |       break | ||
|  |     case 'formData': | ||
|  |       toSwaggerProp = function (propertyName, jsonSchemaElement) { | ||
|  |         delete jsonSchemaElement.$id | ||
|  |         jsonSchemaElement.in = container | ||
|  |         jsonSchemaElement.name = propertyName | ||
|  | 
 | ||
|  |         // https://json-schema.org/understanding-json-schema/reference/non_json_data.html#contentencoding
 | ||
|  |         if (jsonSchemaElement.contentEncoding === 'binary') { | ||
|  |           delete jsonSchemaElement.contentEncoding // Must be removed
 | ||
|  |           jsonSchemaElement.type = 'file' | ||
|  |         } | ||
|  | 
 | ||
|  |         return jsonSchemaElement | ||
|  |       } | ||
|  |       break | ||
|  |     case 'path': | ||
|  |       toSwaggerProp = function (propertyName, jsonSchemaElement) { | ||
|  |         jsonSchemaElement.in = container | ||
|  |         jsonSchemaElement.name = propertyName | ||
|  |         jsonSchemaElement.required = true | ||
|  |         return jsonSchemaElement | ||
|  |       } | ||
|  |       break | ||
|  |     case 'header': | ||
|  |       toSwaggerProp = function (propertyName, jsonSchemaElement) { | ||
|  |         return { | ||
|  |           in: 'header', | ||
|  |           name: propertyName, | ||
|  |           required: jsonSchemaElement.required, | ||
|  |           description: jsonSchemaElement.description, | ||
|  |           type: jsonSchemaElement.type | ||
|  |         } | ||
|  |       } | ||
|  |       break | ||
|  |   } | ||
|  | 
 | ||
|  |   return Object.keys(obj) | ||
|  |     .filter((propKey) => (!securityIgnores.includes(propKey))) | ||
|  |     .map((propKey) => { | ||
|  |       return toSwaggerProp(propKey, obj[propKey]) | ||
|  |     }) | ||
|  | } | ||
|  | 
 | ||
|  | /* | ||
|  | * Map  unsupported JSON schema definitions to Swagger definitions | ||
|  | */ | ||
|  | function replaceUnsupported (jsonSchema) { | ||
|  |   if (typeof jsonSchema === 'object' && jsonSchema !== null) { | ||
|  |     // Handle patternProperties, that is not part of OpenAPI definitions
 | ||
|  |     if (jsonSchema.patternProperties) { | ||
|  |       jsonSchema.additionalProperties = { type: 'string' } | ||
|  |       delete jsonSchema.patternProperties | ||
|  |     } else if (jsonSchema.const) { | ||
|  |       // Handle const, that is not part of OpenAPI definitions
 | ||
|  |       jsonSchema.enum = [jsonSchema.const] | ||
|  |       delete jsonSchema.const | ||
|  |     } | ||
|  | 
 | ||
|  |     Object.keys(jsonSchema).forEach(function (key) { | ||
|  |       jsonSchema[key] = replaceUnsupported(jsonSchema[key]) | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   return jsonSchema | ||
|  | } | ||
|  | 
 | ||
|  | function isConsumesFormOnly (schema) { | ||
|  |   const consumes = schema.consumes | ||
|  |   return ( | ||
|  |     consumes && | ||
|  |       consumes.length === 1 && | ||
|  |       (consumes[0] === 'application/x-www-form-urlencoded' || | ||
|  |         consumes[0] === 'multipart/form-data') | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function resolveBodyParams (parameters, schema, ref) { | ||
|  |   const resolved = ref.resolve(schema) | ||
|  |   replaceUnsupported(resolved) | ||
|  | 
 | ||
|  |   parameters.push({ | ||
|  |     name: 'body', | ||
|  |     in: 'body', | ||
|  |     schema: resolved | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) { | ||
|  |   const resolved = ref.resolve(schema) | ||
|  |   const arr = plainJsonObjectToSwagger2(container, resolved, sharedSchemas, securityIgnores) | ||
|  |   arr.forEach(swaggerSchema => parameters.push(swaggerSchema)) | ||
|  | } | ||
|  | 
 | ||
|  | // https://swagger.io/docs/specification/2-0/describing-responses/
 | ||
|  | function resolveResponse (fastifyResponseJson, 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 = ref.resolve(rawJsonSchema) | ||
|  | 
 | ||
|  |     delete resolved.$schema | ||
|  | 
 | ||
|  |     // 2xx is not supported by swagger
 | ||
|  |     const deXXStatusCode = statusCode.toUpperCase().replace('XX', '00') | ||
|  |     // conflict when we have both 2xx and 200
 | ||
|  |     if (statusCode.toUpperCase().includes('XX') && statusCodes.includes(deXXStatusCode)) { | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     // converts statusCode to upper case only when it is not "default"
 | ||
|  |     if (statusCode !== 'default') { | ||
|  |       statusCode = deXXStatusCode | ||
|  |     } | ||
|  | 
 | ||
|  |     const response = { | ||
|  |       description: rawJsonSchema[xResponseDescription] || rawJsonSchema.description || 'Default Response' | ||
|  |     } | ||
|  | 
 | ||
|  |     // add headers when there are any.
 | ||
|  |     if (rawJsonSchema.headers) { | ||
|  |       response.headers = rawJsonSchema.headers | ||
|  |       // remove invalid field
 | ||
|  |       delete resolved.headers | ||
|  |     } | ||
|  | 
 | ||
|  |     // add schema when type is not 'null'
 | ||
|  |     if (rawJsonSchema.type !== 'null') { | ||
|  |       const schema = { ...resolved } | ||
|  |       replaceUnsupported(schema) | ||
|  |       delete schema[xResponseDescription] | ||
|  |       response.schema = schema | ||
|  |     } | ||
|  | 
 | ||
|  |     responsesContainer[statusCode] = response | ||
|  |   }) | ||
|  | 
 | ||
|  |   return responsesContainer | ||
|  | } | ||
|  | 
 | ||
|  | function prepareSwaggerMethod (schema, ref, swaggerObject) { | ||
|  |   const swaggerMethod = {} | ||
|  |   const parameters = [] | ||
|  | 
 | ||
|  |   // Parse out the security prop keys to ignore
 | ||
|  |   const securityIgnores = [ | ||
|  |     ...(swaggerObject && swaggerObject.security ? swaggerObject.security : []), | ||
|  |     ...(schema && schema.security ? schema.security : []) | ||
|  |   ] | ||
|  |     .reduce((acc, securitySchemeGroup) => { | ||
|  |       Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => { | ||
|  |         const { name, in: category } = swaggerObject.securityDefinitions[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) swaggerMethod.operationId = schema.operationId | ||
|  |     if (schema.summary) swaggerMethod.summary = schema.summary | ||
|  |     if (schema.description) swaggerMethod.description = schema.description | ||
|  |     if (schema.externalDocs) swaggerMethod.externalDocs = schema.externalDocs | ||
|  |     if (schema.tags) swaggerMethod.tags = schema.tags | ||
|  |     if (schema.produces) swaggerMethod.produces = schema.produces | ||
|  |     if (schema.consumes) swaggerMethod.consumes = schema.consumes | ||
|  |     if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, swaggerObject.definitions, securityIgnores.query) | ||
|  |     if (schema.body) { | ||
|  |       const isConsumesAllFormOnly = isConsumesFormOnly(schema) || isConsumesFormOnly(swaggerObject) | ||
|  |       isConsumesAllFormOnly | ||
|  |         ? resolveCommonParams('formData', parameters, schema.body, ref, swaggerObject.definitions) | ||
|  |         : resolveBodyParams(parameters, schema.body, ref) | ||
|  |     } | ||
|  |     if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, swaggerObject.definitions) | ||
|  |     if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, swaggerObject.definitions, securityIgnores.header) | ||
|  |     if (parameters.length > 0) swaggerMethod.parameters = parameters | ||
|  |     if (schema.deprecated) swaggerMethod.deprecated = schema.deprecated | ||
|  |     if (schema.security) swaggerMethod.security = schema.security | ||
|  |     for (const key of Object.keys(schema)) { | ||
|  |       if (key.startsWith('x-')) { | ||
|  |         swaggerMethod[key] = schema[key] | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   swaggerMethod.responses = resolveResponse(schema ? schema.response : null, ref) | ||
|  | 
 | ||
|  |   return swaggerMethod | ||
|  | } | ||
|  | 
 | ||
|  | function prepareSwaggerDefinitions (definitions, ref) { | ||
|  |   return Object.entries(definitions) | ||
|  |     .reduce((res, [name, definition]) => { | ||
|  |       const _ = { ...definition } | ||
|  |       const resolved = ref.resolve(_, { externalSchemas: [definitions] }) | ||
|  | 
 | ||
|  |       // 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
 | ||
|  |       delete resolved.$id | ||
|  |       delete resolved.definitions | ||
|  | 
 | ||
|  |       res[name] = resolved | ||
|  |       return res | ||
|  |     }, {}) | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |   prepareDefaultOptions, | ||
|  |   prepareSwaggerObject, | ||
|  |   prepareSwaggerMethod, | ||
|  |   normalizeUrl, | ||
|  |   prepareSwaggerDefinitions | ||
|  | } |