UNPKG

@tiemma/sonic-core

Version:

Core package for the sonic project on swagger documentation

533 lines (470 loc) 15.4 kB
const matchAll = require('match-all'); const { writeFileSync } = require('fs'); const { debugLogger } = require('./logger'); const { routeParametersRegex, requestBodyDependencyRegex, getDependency, routeDependencyRegex, } = require('./regex-utils'); const logger = debugLogger(__filename); const getType = (obj) => ({}.toString .call(obj) .match(/\s([a-zA-Z]+)/)[1] .toLowerCase()); const NonPrimitiveTypes = { ARRAY: 'array', OBJECT: 'object', NULL: 'null', UNDEFINED: 'undefined', }; const swaggerRef = (contentType, responseRef, prefix = '#/definitions') => ({ content: { [contentType]: { schema: { $ref: `${prefix}/${responseRef}`, }, }, }, }); const generateResponseRef = () => Math.random().toString(36).substring(7); const generateResponse = (op, obj) => { if (!obj) { return op; } for (const key of Object.keys(obj)) { if (Object.keys(obj[key]).includes('properties')) { op[key] = {}; op[key] = generateResponse(op[key], obj[key].properties); } else { switch (obj[key].type) { case 'object': op[key] = generateResponse(op, obj); break; default: op[key] = obj[key].example; } } } return op; }; const buildSwaggerJSON = (data) => { if (!Object.values(NonPrimitiveTypes).includes(getType(data))) { return { type: getType(data), example: data, }; } if (getType(data) === NonPrimitiveTypes.ARRAY) { return { type: NonPrimitiveTypes.ARRAY, items: { type: getType(data[0]), properties: buildSwaggerJSON(data[0]).properties, }, example: data, }; } const keys = Object.keys(data || {}); const op = { required: keys, properties: {}, }; for (const key of keys) { const value = data[key]; switch (getType(value)) { case NonPrimitiveTypes.ARRAY: // eslint-disable-next-line no-case-declarations const typeData = getType(value[0]); if (typeData === NonPrimitiveTypes.ARRAY) { throw new Error(`Complex object (array of array etc...)', ${value[0]}`); } else if (typeData === NonPrimitiveTypes.OBJECT) { op.properties[key] = { type: NonPrimitiveTypes.ARRAY, items: { type: typeData, properties: buildSwaggerJSON(value[0]).properties, }, example: value, }; } else { op.properties[key] = { type: NonPrimitiveTypes.ARRAY, items: { type: typeData, }, }; op.properties[key].example = value; } break; case NonPrimitiveTypes.OBJECT: op.properties[key] = buildSwaggerJSON(value); op.properties[key].type = NonPrimitiveTypes.OBJECT; break; default: op.properties[key] = { type: getType(value), example: value, }; break; } } return op; }; const findBodyParameterIndexV2 = (parameterList) => { if (getType(parameterList) === NonPrimitiveTypes.ARRAY) { for (let idx = 0; idx < parameterList.length; idx += 1) { if (parameterList[idx].in === 'body') { return idx; } } } return false; }; const findPathParameterIndex = (parameterList, key) => { if (getType(parameterList) === NonPrimitiveTypes.ARRAY) { // eslint-disable-next-line no-restricted-syntax for (let idx = 0; idx < parameterList.length; idx += 1) { if (parameterList[idx].in === 'path' && parameterList[idx].name === key) { return idx; } } } return false; }; const findQueryParameterIndex = (parameterList, key) => { if (getType(parameterList) === NonPrimitiveTypes.ARRAY) { // eslint-disable-next-line no-restricted-syntax for (let idx = 0; idx < parameterList.length; idx += 1) { if (parameterList[idx].in === 'query' && parameterList[idx].name === key) { return idx; } } } return false; }; const trimString = (path) => (!path.includes('?') ? path.substr(1) : path.substring(1, path.length - 1)); const replaceRoutes = (route, regex) => route.replace(regex, (x) => `{${trimString(x)}}`); const initSwaggerSchemaSpecV3 = (swaggerSpec) => { if (!swaggerSpec.components) { swaggerSpec.components = { schemas: {}, }; } if (!swaggerSpec.components.schemas) { swaggerSpec.components.schemas = {}; } }; const initSwaggerSchemaSpecV2 = (swaggerSpec) => { if (!swaggerSpec.definitions) { swaggerSpec.definitions = {}; } }; const initSwaggerPathForRouteAndMethod = (swaggerSpec, route, method) => { if (!swaggerSpec.paths) { swaggerSpec.paths = { [route]: { [method]: { }, }, }; } if (!swaggerSpec.paths[route]) { swaggerSpec.paths[route] = {}; } if (!swaggerSpec.paths[route][method]) { swaggerSpec.paths[route][method] = {}; } if (!swaggerSpec.paths[route][method].parameters) { swaggerSpec.paths[route][method].parameters = []; } // @TODO: remove default init of definitions after fixing issue with using components/schemas if (!swaggerSpec.definitions) { swaggerSpec.definitions = []; } if (swaggerSpec.openapi) { initSwaggerSchemaSpecV3(swaggerSpec); } else if (swaggerSpec.swagger) { initSwaggerSchemaSpecV2(swaggerSpec); } else { throw new Error('Unknown swagger specification'); } }; const initSwaggerSchemaParameters = (swaggerSpec, originalRoute, parameterRegex, method) => { const route = replaceRoutes(originalRoute, parameterRegex); initSwaggerPathForRouteAndMethod(swaggerSpec, route, method); const parameterList = swaggerSpec.paths[route][method].parameters; const parameterPathList = originalRoute.match(parameterRegex); if (getType(parameterPathList) !== NonPrimitiveTypes.ARRAY) { return; } for (const path of parameterPathList) { if (findPathParameterIndex(parameterList, path) === false) { swaggerSpec.paths[route][method].parameters.push({ name: trimString(path), in: 'path', required: !path.includes('?'), }); } } }; const generateQueryParameterSpec = (swaggerSpec, route, method, queries) => { const parameterList = swaggerSpec.paths[route][method].parameters; for (const key of Object.keys(queries)) { const pIdx = findQueryParameterIndex(parameterList, key); if (pIdx === false) { swaggerSpec.paths[route][method].parameters.push({ name: key, in: 'query', type: getType(queries[key]), }); } } }; const generateRequestBodySpec = (swaggerSpec, route, method, requestBody, contentType, definitionName) => { if (!Object.keys(requestBody).length) { return; } if (!definitionName) { definitionName = generateResponseRef(); } if (swaggerSpec.openapi) { swaggerSpec.paths[route][method].requestBody = swaggerRef(contentType, definitionName); // Deferring from using component schemas cause WTF is the complexity in making this work // I'd revisit at a later time in a more calmer state of mind // swaggerSpec.components.schemas[definitionName] = buildSwaggerJSON(requestBody); swaggerSpec.definitions[definitionName] = buildSwaggerJSON(requestBody); } else if (swaggerSpec.swagger) { const parameterList = swaggerSpec[route][method].parameters; const bodyIndex = findBodyParameterIndexV2(parameterList); if (bodyIndex === false) { swaggerSpec.paths[route][method].parameters.push({ schema: {} }); } swaggerSpec.paths[route][method].parameters[bodyIndex].schema.$ref = `#/definitions/${definitionName}`; swaggerSpec.definitions[definitionName] = buildSwaggerJSON(requestBody); } else { throw new Error('Unknown swagger specification'); } }; const generateResponseBodySpec = (swaggerSpec, route, method, responseBody, contentType, statusCode) => { if (!Object.keys(responseBody).length) { return; } const definitionName = generateResponseRef(); if (!swaggerSpec.paths[route][method].responses) { swaggerSpec.paths[route][method].responses = {}; } if (swaggerSpec.openapi) { const responseSpec = swaggerRef(contentType, definitionName); swaggerSpec.paths[route][method].responses[statusCode] = responseSpec; } else if (swaggerSpec.swagger) { if (!swaggerSpec.paths[route][method].responses[statusCode].schema) { swaggerSpec.paths[route][method].responses[statusCode].schema = {}; } swaggerSpec.paths[route][method].responses[statusCode].schema.$ref = `#/definitions/${definitionName}`; } else { throw new Error('Unknown swagger specification'); } swaggerSpec.definitions[definitionName] = buildSwaggerJSON(responseBody); }; const writeAsSwaggerDocToFile = (swaggerSpec, method, originalRoute, parameterRegex, responseBody, requestBody, queries, statusCode, contentType, requestDefinitionName, swaggerFilePath) => { try { responseBody = JSON.parse(responseBody); } catch (e) { logger("Response isn't a JSON object, ignoring parse"); } initSwaggerSchemaParameters(swaggerSpec, originalRoute, parameterRegex, method); const route = replaceRoutes(originalRoute, parameterRegex); if (statusCode < 400) { // eslint-disable-next-line max-len generateRequestBodySpec(swaggerSpec, route, method, requestBody, contentType, requestDefinitionName); generateQueryParameterSpec(swaggerSpec, route, method, queries); } if (statusCode !== 204) { generateResponseBodySpec(swaggerSpec, route, method, responseBody, contentType, statusCode); } writeFileSync(swaggerFilePath, JSON.stringify(swaggerSpec, null, 4)); }; const getBodyDependencies = (routes, method, swaggerSpec) => { const allowedBodyRoutes = ['post', 'put']; let definitionName; const defaultRef = { dependencies: [], body: {}, definitionName }; let definitionRef = ''; if (allowedBodyRoutes.includes(method)) { if (swaggerSpec.openapi) { if (!routes[method].requestBody) { return defaultRef; } const contentTypes = routes[method].requestBody.content; const type = Object.keys(contentTypes)[0]; definitionRef = contentTypes[type].schema.$ref; } else if (swaggerSpec.swagger) { const bodyIdx = findBodyParameterIndexV2(routes[method].parameters); if (!routes[method].parameters || !bodyIdx) { return defaultRef; } definitionRef = routes[method].parameters[bodyIdx].schema.$ref; } else { throw new Error('Unknown swagger specification'); } // Not necessarily definitions // This fixes the need to manage both swagger 2 or 3 definitions // by traversing the rote definition path in the swagger spec const definitionPaths = definitionRef.split('/').slice(1); [definitionName] = definitionPaths.slice(-1); let body = ''; if (definitionName) { body = swaggerSpec; for (const path of definitionPaths) { body = body[path]; } } if (body) { const rawDeps = matchAll(JSON.stringify(body), requestBodyDependencyRegex).toArray(); return { dependencies: new Set(rawDeps.map(getDependency)), body, definitionName }; } } return defaultRef; }; const getParameterDependencies = (route, method, parameters, name, strictMode = false) => { let dependencies = []; const templateKey = 'defaultTemplate'; if (!parameters) { return { route, dependencies: [] }; } if (getType(parameters) === NonPrimitiveTypes.ARRAY) { const routeParameters = matchAll(route, routeParametersRegex).toArray(); parameters.forEach((params) => { const template = params[templateKey]; if (template) { if (routeParameters.includes(params.name) && params.in === 'path') { route = route.replace(`{${params.name}}`, template); } dependencies = [...dependencies, getDependency(template)]; } }); if (!name && dependencies && strictMode) { throw Error(`All routes with dependencies must have a name: ${method} ${route}`); } } else { throw Error(`Parameters must be an array ${JSON.stringify(parameters)}`); } return { dependencies, route }; }; const evaluateRoute = (route, context) => { const routeDeps = matchAll(route, routeDependencyRegex).toArray(); logger(`Evaluating route with context: ${JSON.stringify(context, null, 4)}`); for (const dependency of routeDeps) { // eslint-disable-next-line no-eval const value = eval(`context.${dependency.slice(1)}`); route = route.replace(dependency, value); } return route; }; const getDefinitions = (swaggerSpec) => generateResponse({}, swaggerSpec.definitions); const addDefinitions = (bodyDefinitions, swaggerSpec = {}) => { for (const name of Object.keys(bodyDefinitions)) { swaggerSpec.definitions[name] = buildSwaggerJSON( bodyDefinitions[name], ); } return swaggerSpec; }; const parseSwaggerRouteData = (swaggerSpec, bodyDefinitions, strictMode = false) => { logger('Generating JSON object representing decomposed swagger definitions'); swaggerSpec.definitions = { ...getDefinitions(swaggerSpec), ...bodyDefinitions }; const { paths } = swaggerSpec; const dependencyGraph = {}; const definitionMap = {}; for (const path of Object.keys(paths)) { const routes = paths[path]; for (const method of Object.keys(routes)) { logger(`Parsing documentation under ${method.toUpperCase()} ${path}`); const { name } = routes[method]; if (!name) { if (!strictMode) { // eslint-disable-next-line no-continue continue; } else { throw Error(`Define name for route: ${method.toUpperCase()} ${path}`); } } if (dependencyGraph[name] && strictMode) { throw Error(`Duplicate dependency name: ${name}`); } logger('Obtaining parameter dependencies'); const { route, dependencies: parameterDependencies, } = getParameterDependencies(path, method, routes[method].parameters, name); logger('Obtaining request body dependencies'); const { body, definitionName, dependencies: bodyDependencies, } = getBodyDependencies(routes, method, swaggerSpec); if (definitionName) { definitionMap[path] = { [method.toUpperCase()]: definitionName }; } const dependencies = Array.from( new Set([...parameterDependencies, ...bodyDependencies]).values(), ); dependencyGraph[name] = { dependencies }; const reqObj = { requestBody: body, apiRoute: route, originalRoute: path, method, }; if (Object.keys(body).length > 0) { reqObj.definitionName = definitionName; } if (dependencyGraph[name]) { dependencyGraph[name].requestData = reqObj; } logger(`Successfully obtained dependencies for node ${name}`); logger('-'); } } return { dependencyGraph, definitionMap }; }; module.exports = { parseSwaggerRouteData, evaluateRoute, buildSwaggerJSON, addDefinitions, swaggerRef, generateResponseRef, generateResponse, getType, findBodyParameterIndexV2, findPathParameterIndex, findQueryParameterIndex, writeAsSwaggerDocToFile, replaceRoutes, trimString, NonPrimitiveTypes, };