UNPKG

api-schema-builder

Version:

build schema with validators for each endpoint

257 lines (218 loc) 9.62 kB
'use strict'; const get = require('lodash.get'); const Ajv = require('ajv'); const { URL } = require('url'); const { defaultFormatsValidators } = require('./validators/formatValidators.js'); const schemaPreprocessor = require('./utils/schema-preprocessor'); const oai3 = require('./parsers/open-api3'); const oai2 = require('./parsers/open-api2'); const ajvUtils = require('./utils/ajv-utils'); const schemaUtils = require('./utils/schemaUtils'); const sourceResolver = require('./utils/sourceResolver'); const Validators = require('./validators/index'); const createContentTypeHeaders = require('./utils/createContentTypeHeaders'); const { loadSchemaAsync, loadSchema } = require('./utils/schemaLoaders.js'); const DEFAULT_OPTIONS = { buildRequests: true, buildResponses: true }; async function buildSchema(swaggerPath, options) { const { jsonSchema, dereferencedSchema } = await loadSchemaAsync(swaggerPath, options); return buildValidations(jsonSchema, dereferencedSchema, options); } function buildSchemaSync(pathOrSchema, options) { const { jsonSchema, dereferencedSchema } = loadSchema(pathOrSchema, options); return buildValidations(jsonSchema, dereferencedSchema, options); } function buildValidations(referenced, dereferenced, receivedOptions) { const options = getOptions(receivedOptions); const schemas = {}; const basePaths = dereferenced.servers && dereferenced.servers.length ? dereferenced.servers.map(({ url }) => url.indexOf('://') > -1 ? new URL(url).pathname : url) : [dereferenced.basePath || '/']; Object.keys(dereferenced.paths).forEach(currentPath => { const operationSchemas = {}; Object.keys(dereferenced.paths[currentPath]) .filter(parameter => parameter !== 'parameters') .forEach(currentMethod => { const parsedMethod = currentMethod.toLowerCase(); let requestValidator; if (options.buildRequests) { requestValidator = buildRequestValidator( referenced, dereferenced, currentPath, currentMethod, options ); } let responseValidator; if (options.buildResponses) { responseValidator = buildResponseValidator( referenced, dereferenced, currentPath, currentMethod, options ); } operationSchemas[parsedMethod] = Object.assign({}, requestValidator, { responses: responseValidator }); }); basePaths.forEach(basePath => { const normalizedPath = basePath.replace(/\/$/, '') + currentPath.replace(/{(\w+)}/g, ':$1'); schemas[normalizedPath] = operationSchemas; }); }); return schemas; } function getOptions(opts = {}) { const formats = opts.formats ? defaultFormatsValidators.concat(opts.formats) : defaultFormatsValidators; return Object.assign( {}, DEFAULT_OPTIONS, opts, { formats } ); } function getContentTypes(isOpenApi3, dereferenced, currentPath, currentMethod) { let contentType; if (isOpenApi3) { const requestBody = dereferenced.paths[currentPath][currentMethod].requestBody; contentType = requestBody && Object.keys(requestBody.content); } else { contentType = dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes; } return contentType; } function buildRequestValidator(referenced, dereferenced, currentPath, currentMethod, options) { const requestSchema = {}; let localParameters = []; const pathParameters = dereferenced.paths[currentPath].parameters || []; const isOpenApi3 = schemaUtils.getOAIVersion(dereferenced) === 3; const parameters = dereferenced.paths[currentPath][currentMethod].parameters || []; if (isOpenApi3) { requestSchema.body = oai3.buildRequestBodyValidation(dereferenced, referenced, currentPath, currentMethod, options); localParameters = oai3.buildPathParameters(parameters, pathParameters); } else { const bodySchema = options.expectFormFieldsInBody ? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')); }) : parameters.filter(function (parameter) { return parameter.in === 'body' }); options.makeOptionalAttributesNullable && schemaPreprocessor.makeOptionalAttributesNullable(bodySchema); if (bodySchema.length > 0) { const validatedBodySchema = oai2.getValidatedBodySchema(bodySchema); requestSchema.body = oai2.buildRequestBodyValidation(validatedBodySchema, dereferenced.definitions, referenced, currentPath, currentMethod, options); } localParameters = oai2.buildPathParameters(parameters, pathParameters); } const contentTypes = getContentTypes(isOpenApi3, dereferenced, currentPath, currentMethod); requestSchema.parameters = buildParametersValidation(localParameters, contentTypes, options); return requestSchema; } function buildResponseValidator(referenced, dereferenced, currentPath, currentMethod, options) { const responsesSchema = {}; const isOpenApi3 = schemaUtils.getOAIVersion(dereferenced) === 3; const responses = get(dereferenced, `paths[${currentPath}][${currentMethod}].responses`); if (responses) { Object .keys(responses) .forEach(statusCode => { let headersValidator, bodyValidator; if (isOpenApi3) { headersValidator = oai3.buildHeadersValidation(responses, statusCode, options); bodyValidator = oai3.buildResponseBodyValidation(dereferenced, referenced, currentPath, currentMethod, statusCode, options); } else { const contentTypes = dereferenced.paths[currentPath][currentMethod].produces || dereferenced.paths[currentPath].produces || dereferenced.produces; headersValidator = oai2.buildHeadersValidation(responses, contentTypes, statusCode, options); bodyValidator = oai2.buildResponseBodyValidation(responses, dereferenced.definitions, referenced, currentPath, currentMethod, statusCode, options); } if (headersValidator || bodyValidator) { responsesSchema[statusCode] = new Validators.ResponseValidator({ body: bodyValidator, headers: headersValidator }); } }); } return responsesSchema; } function buildParametersValidation(parameters, contentTypes, options) { const defaultAjvOptions = { allErrors: true, coerceTypes: 'array' // unknownFormats: 'ignore' }; const ajvOptions = Object.assign({}, defaultAjvOptions, options.ajvConfigParams); const ajv = new Ajv(ajvOptions); ajvUtils.addCustomKeyword(ajv, options.formats, options.keywords); var ajvParametersSchema = { title: 'HTTP parameters', type: 'object', additionalProperties: false, properties: { headers: { title: 'HTTP headers', type: 'object', properties: {}, additionalProperties: true // plural: 'headers' }, path: { title: 'HTTP path', type: 'object', properties: {}, additionalProperties: false }, query: { title: 'HTTP query', type: 'object', properties: {}, additionalProperties: false }, files: { title: 'HTTP form files', files: { required: [], optional: [] } } } }; parameters.forEach(parameter => { var data = Object.assign({}, parameter); const required = parameter.required; const source = sourceResolver.resolveParameterSource(parameter); const key = parameter.in === 'header' ? parameter.name.toLowerCase() : parameter.name; var destination = ajvParametersSchema.properties[source]; delete data.name; delete data.in; delete data.required; if (data.type === 'file') { if (required) { destination.files.required.push(key); } else { destination.files.optional.push(key); } } else if (source !== 'fields') { if (required) { destination.required = destination.required || []; destination.required.push(key); } destination.properties[key] = data; } }); ajvParametersSchema.properties.headers.content = createContentTypeHeaders(options.contentTypeValidation, contentTypes); return new Validators.SimpleValidator(ajv.compile(ajvParametersSchema)); } module.exports = { buildSchemaSync, buildSchema, buildValidations };