UNPKG

openapi-format

Version:

Format an OpenAPI document by ordering, formatting and filtering fields.

1,202 lines (1,086 loc) 47.3 kB
#!/usr/bin/env node 'use strict'; const traverse = require('neotraverse/legacy'); const {isString, isArray, isObject} = require('./utils/types'); const { prioritySort, isMatchOperationItem, arraySort, sortPathsByTags, sortPathsByAlphabet } = require('./utils/sorting'); const { changeComponentParametersCasingEnabled, changeParametersCasingEnabled, changeCase, changeArrayObjKeysCase, changeObjKeysCase } = require('./utils/casing'); const {valueReplace, get, isUsedComp} = require('./utils/filtering'); const { convertNullable, convertExample, convertImageBase64, convertMultiPartBinary, convertConst, convertExclusiveMinimum, convertExclusiveMaximum, setInObject } = require('./utils/convert'); const {parseFile, writeFile, stringify, detectFormat, parseString, analyzeOpenApi, readFile} = require('./utils/file'); const {parseTpl, getOperation} = require('./utils/parseTpl'); const {writePaths, writeComponents, writeSplitOpenAPISpec} = require('./utils/split'); const {dirname, extname} = require('path'); const {openapiOverlay, resolveJsonPath, resolveJsonPathValue} = require('./utils/overlay'); /** * OpenAPI sort function * Traverse through all keys and based on the key name, sort the props according the preferred order. * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format sort options * @returns {object} Sorted OpenAPI document */ async function openapiSort(oaObj, options) { // Skip sorting, when the option "no-sort" is set if (options.sort === false) { return oaObj; } // Sort by options const sortPathsBy = options.sortSet?.sortPathsBy || 'original'; // Cleanup sortSet if (options.sortSet) { delete options.sortSet.sortPathsBy; options.sortSet = Object.keys(options.sortSet).length ? options.sortSet : null; } let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object let sortSet = options.sortSet || (await parseFile(__dirname + '/defaultSort.json')); let sortComponentsSet = options.sortComponentsSet || (await parseFile(__dirname + '/defaultSortComponents.json')); let debugStep = ''; // uncomment // debugStep below to see which sort part is triggered // Recursive traverse through OpenAPI document traverse(jsonObj).forEach(function (node) { if (typeof node === 'object' && node !== null) { // Components sorting by alphabet if ( this.parent && this.parent.key && this.path[0] === 'components' && this.parent.key === 'components' && sortComponentsSet.length > 0 && sortComponentsSet.includes(this.key) ) { // debugStep = 'Component sorting by alphabet' let sortedObj = JSON.parse(JSON.stringify(node)); // Deep copy of the schema object node = prioritySort(sortedObj, []); this.update(node); } // Inline parameters sorting by alphabet if ( this.path[0] !== 'components' && Array.isArray(node) && sortComponentsSet.length > 0 && sortComponentsSet.includes(this.key) ) { // debugStep = 'Sorting inline parameters' let sortedObj = JSON.parse(JSON.stringify(node)); // Deep copy of the node node = arraySort(sortedObj, 'name'); this.update(node); } // Path sorting if (this.key === 'paths' && sortPathsBy !== 'original') { let sortedObj = JSON.parse(JSON.stringify(node)); if (sortPathsBy === 'path') { // debugStep = 'Path sorting by alphabet' node = sortPathsByAlphabet(sortedObj); this.update(node); } if (sortPathsBy === 'tags') { // debugStep = 'Path sorting by first method, first tag' node = sortPathsByTags(sortedObj); this.update(node); } } // Generic sorting if (sortSet.hasOwnProperty(this.key) && Array.isArray(sortSet[this.key])) { if (Array.isArray(node)) { if ( this.parent && this.parent.key === 'example' && (this.path[0] === 'components' || this.path[3] === 'requestBody') ) { // debugStep = 'Generic sorting - skip nested components>example array' // Skip nested components>example or requestBody>example values } else { // debugStep = 'Generic sorting - array' // Deep sort array of properties let sortedObj = JSON.parse(JSON.stringify(node)); // Deep copy of the schema object for (let i = 0; i < sortedObj.length; i++) { sortedObj[i] = prioritySort(sortedObj[i], sortSet[this.key]); } this.update(sortedObj); } } else if ( (this.key === 'responses' || this.key === 'schemas' || this.key === 'properties') && this.parent && this.parent.key !== 'properties' && this.parent.key !== 'value' && this.path[1] !== 'examples' ) { // debugStep = 'Generic sorting - responses/schemas/properties' // Deep sort list of properties let sortedObj = JSON.parse(JSON.stringify(node)); // Deep copy of the object for (let keyRes in sortedObj) { sortedObj[keyRes] = prioritySort(sortedObj[keyRes], sortSet[this.key]); } this.update(sortedObj); } else { if (this.path[0] === 'components' && this.path[1] === 'examples' && this.path[3] === 'value') { // debugStep = 'Generic sorting - skip nested components>examples' // Skip nested components>examples values } else { // debugStep = 'Generic sorting - properties' // Sort list of properties this.update(prioritySort(node, sortSet[this.key])); } } } } }); // Process root level if (jsonObj.openapi) { jsonObj = prioritySort(jsonObj, sortSet['root']); } // Return result object return {data: jsonObj, resultData: {}}; } /** * OpenAPI filter function * Traverse through all keys and based on the key name, filter the props according to the filter configuration. * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format filter options * @returns {object} Filtered OpenAPI document */ async function openapiFilter(oaObj, options) { let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object let defaultFilter = options.defaultFilter || (await parseFile(__dirname + '/defaultFilter.json')); let filterSet = Object.assign({}, defaultFilter, options.filterSet); const httpVerbs = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; const fixedFlags = ['x-openapi-format-filter']; options.unusedDepth = options.unusedDepth || 0; // Merge object filters const filterKeys = (filterSet.methods ?? []).filter(method => method != null).map(method => method.toLowerCase()); const filterArray = [...(filterSet.tags ?? [])]; const filterOperations = [...(filterSet.operations ?? [])]; const filterProps = [...(filterSet.operationIds ?? []), ...(filterSet.flags ?? []), ...(fixedFlags ?? [])]; const filterResponseContent = [...(filterSet.responseContent ?? [])]; const filterRequestContent = [...(filterSet.requestContent ?? [])]; // Inverse object filters const inverseFilterKeys = [...(filterSet.inverseMethods ?? [])]; const inverseFilterProps = [...(filterSet.inverseOperationIds ?? [])]; const inverseFilterArray = [...(filterSet.inverseTags ?? [])]; const inverseFilterFlags = [...(filterSet.inverseFlags ?? [])]; const inverseFilterResponseContent = [...(filterSet.inverseResponseContent ?? [])]; const inverseFilterRequestContent = [...(filterSet.inverseRequestContent ?? [])]; const stripFlags = [...(filterSet.stripFlags ?? [])]; const stripUnused = [...(filterSet.unusedComponents ?? [])]; const textReplace = filterSet.textReplace || []; let doRecurse = false; // Convert flag values to flags const filterFlagValuesKeys = Object.keys(Object.assign({}, ...(filterSet.flagValues ?? []))); const filterFlagValues = [...(filterSet.flagValues ?? [])]; const filterFlagHash = filterFlagValues.map(o => JSON.stringify(o)); // Convert invert flag values to flags const inverseFilterFlagValuesKeys = Object.keys(Object.assign({}, ...(filterSet.inverseFlagValues ?? []))); const inverseFilterFlagValues = [...(filterSet.inverseFlagValues ?? [])]; const inverseFilterFlagHash = inverseFilterFlagValues.map(o => JSON.stringify(o)); // Initiate components tracking const comps = { schemas: {}, responses: {}, parameters: {}, examples: {}, requestBodies: {}, headers: {}, meta: {total: 0} }; // Prepare unused components let unusedComp = { schemas: [], responses: [], parameters: [], examples: [], requestBodies: [], headers: [], meta: {total: 0} }; // Use options.unusedComp to collect unused components during multiple recursion if (!options.unusedComp) options.unusedComp = JSON.parse(JSON.stringify(unusedComp)); let debugFilterStep = ''; // uncomment // debugFilterStep below to see which sort part is triggered traverse(jsonObj).forEach(function (node) { // Register components presence if (get(this, 'parent.parent.key') && this.parent.parent.key === 'components') { if (get(this, 'parent.key') && this.parent.key && comps[this.parent.key]) { comps[this.parent.key][this.key] = {...comps[this.parent.key][this.key], present: true}; comps.meta.total++; } } // Register components usage if (this.key === '$ref' && typeof node === 'string') { for (let type of ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers']) { const prefix = `#/components/${type}/`; if (node.startsWith(prefix)) { const name = node.slice(prefix.length); comps[type][name] = Object.assign(comps[type][name] || {}, {used: true}); } } } // Filter out object matching the "response content" if ( filterResponseContent.length > 0 && filterResponseContent.includes(this.key) && this.parent && this.parent.key === 'content' && this.parent.parent && this.parent.parent.parent && this.parent.parent.parent.key === 'responses' ) { // debugFilterStep = 'Filter - response content' this.remove(); } // Filter out object matching the inverse "response content" if ( inverseFilterResponseContent.length > 0 && !inverseFilterResponseContent.includes(this.key) && this.parent && this.parent.key === 'content' && this.parent.parent && this.parent.parent.parent && this.parent.parent.parent.key === 'responses' ) { // debugFilterStep = 'Filter - inverse response content' this.remove(); } // Filter out object matching the "request content" if ( filterRequestContent.length > 0 && filterRequestContent.includes(this.key) && this.parent && this.parent.key === 'content' && this.parent.parent && this.parent.parent.key === 'requestBody' ) { // debugFilterStep = 'Filter - request content' this.remove(); } // Filter out object matching the inverse "request content" if ( inverseFilterRequestContent.length > 0 && !inverseFilterRequestContent.includes(this.key) && this.parent && this.parent.key === 'content' && this.parent.parent && this.parent.parent.key === 'requestBody' ) { // debugFilterStep = 'Filter - inverse request content' this.remove(); } // Filter out object matching the inverse "methods" if ( inverseFilterKeys.length > 0 && !inverseFilterKeys.includes(this.key) && this.parent && this.parent.parent && this.parent.parent.key === 'paths' ) { // debugFilterStep = 'Filter - inverse methods' this.remove(); } // Filter out object matching the "methods" if ( filterKeys.length > 0 && filterKeys.includes(this.key) && this.parent && this.parent.parent && this.parent.parent.key === 'paths' ) { // debugFilterStep = 'Filter - methods' this.remove(); } // Filter out fields without operationIds, when Inverse operationIds is set if ( node !== null && inverseFilterProps.length > 0 && this.path[0] === 'paths' && node.operationId === undefined && httpVerbs.includes(this.key) ) { // debugFilterStep = 'Filter - Single field - Inverse operationIds without operationIds' this.remove(); } // Array field matching if (Array.isArray(node)) { // Filter out object matching the inverse "tags" if ( inverseFilterArray.length > 0 && this.key === 'tags' && !inverseFilterArray.some(i => node.includes(i)) && this.parent.parent !== undefined ) { // debugFilterStep = 'Filter - inverse tags' this.parent.delete(); } // Filter out the top level tags matching the inverse "tags" if (inverseFilterArray.length > 0 && this.key === 'tags' && this.parent.parent === undefined) { // debugFilterStep = 'Filter - inverse top tags' node = node.filter(value => inverseFilterArray.includes(value.name)); this.update(node); } // Filter out object matching the "tags" if (filterArray.length > 0 && this.key === 'tags' && filterArray.some(i => node.includes(i))) { // debugFilterStep = 'Filter - tags' this.parent.delete(); } // Filter out the top OpenAPI.tags matching the "tags" if (filterArray.length > 0 && this.key === 'tags' && this.path[0] === 'tags') { // debugFilterStep = 'Filter - top tags' node = node.filter(value => !filterArray.includes(value.name)); this.update(node); } // Filter out fields matching the flagValues if (filterFlagValuesKeys.length > 0 && filterFlagValuesKeys.includes(this.key)) { for (let i = 0; i < node.length; i++) { const itmObj = {[this.key]: node[i]}; const itmObjHash = JSON.stringify(itmObj); if (filterFlagHash.some(filterFlag => filterFlag === itmObjHash)) { // ======================================================================== // HACK to overcome the issue with removing items from an array if (get(this, 'parent.parent.key') && this.parent.parent.key === 'x-tagGroups') { // debugFilterStep = 'Filter -x-tagGroups - flagValues - array value' const tagGroup = this.parent.node; tagGroup['x-openapi-format-filter'] = true; this.parent.update(tagGroup); // ======================================================================== } else if (isArray(this?.parent?.parent.node)) { const group = this.parent.node; group['x-openapi-format-filter'] = true; } else { // debugFilterStep = 'Filter - Single field - flagValues - array value' this.parent.remove(); } } } } // Keep fields matching the inverseFlags array if ( inverseFilterFlags.length > 0 && (this.path[0] === 'tags' || this.path[0] === 'x-tagGroups') && this.level === 1 ) { let oaTags = JSON.parse(JSON.stringify(node)); oaTags = oaTags.filter(itmObj => { return inverseFilterFlags.some(flagKey => { return itmObj.hasOwnProperty(flagKey); }); }); // Update the node with the filtered array node = oaTags; this.update(node); } // Keep the fields matching the inverseFlagValues array if ( inverseFilterFlagValuesKeys.length > 0 && (this.path[0] === 'tags' || this.path[0] === 'x-tagGroups') && this.level === 1 ) { let oaTags = JSON.parse(JSON.stringify(node)); oaTags = oaTags.filter(itmObj => { // keep the item in the array if any of the inverseFilterFlags is a property of itmObj with a matching value return inverseFilterFlagValues.some(flagObj => { const flagKey = Object.keys(flagObj)[0]; // Get the key of the flagObj const flagValue = flagObj[flagKey]; // Get the value of the flagObj // Check if the key exists in itmObj and if its value matches the value in flagObj return itmObj.hasOwnProperty(flagKey) && itmObj[flagKey] === flagValue; }); }); // Update the node with the filtered array node = oaTags; this.update(node); } // Filter out fields matching the inverseFlagValues if (inverseFilterFlagValuesKeys.length > 0 && inverseFilterFlagValuesKeys.includes(this.key)) { for (let i = 0; i < node.length; i++) { const itmObj = {[this.key]: node[i]}; const itmObjHash = JSON.stringify(itmObj); if (inverseFilterFlagHash.some(filterFlag => filterFlag !== itmObjHash)) { if (isArray(this?.parent?.parent.node)) { // debugFilterStep = 'Filter - inverse flagValues - array value' const group = this.parent.node; group['x-openapi-format-filter'] = true; } } } } } // Single field matching if (typeof node !== 'object' && !Array.isArray(node)) { // Filter out fields matching the flags if (filterProps.length > 0 && filterProps.includes(this.key)) { if (this.parent && this.parent.parent && Array.isArray(this.parent.parent.node)) { // debugFilterStep = 'Filter - Array - flags' const arrayItem = this.parent.parent.node; const filteredArray = arrayItem.filter(item => !item[this.key]); this.parent.parent.update(filteredArray); } else { // debugFilterStep = 'Filter - Single field - flags' this.parent.remove(); } } // Filter out fields matching the flagValues if (filterFlagValuesKeys.length > 0 && filterFlagValuesKeys.includes(this.key)) { const itmObj = {[this.key]: node}; const itmObjHash = JSON.stringify(itmObj); if (filterFlagHash.some(filterFlagHash => filterFlagHash === itmObjHash)) { // ======================================================================== // HACK to overcome the issue with removing items from an array if (get(this, 'parent.parent.key') && this.parent.parent.key === 'x-tagGroups') { // debugFilterStep = 'Filter -x-tagGroups - flagValues - single value' const tagGroup = this.parent.node; tagGroup['x-openapi-format-filter'] = true; this.parent.update(tagGroup); // ======================================================================== } else { // debugFilterStep = 'Filter - Single field - flagValues - single value' this.parent.remove(); } } } // Filter out fields matching the inverse operationIds if (inverseFilterProps.length > 0 && this.key === 'operationId' && !inverseFilterProps.includes(node)) { // debugFilterStep = 'Filter - Single field - Inverse operationIds' this.parent.remove(); } // Filter out fields matching the Tags/operationIds if (filterProps.length > 0 && filterProps.includes(node)) { // debugFilterStep = 'Filter - Single field - Tags/operationIds' this.parent.remove(); } } // Filter out fields matching the operations if (filterOperations.length > 0 && this.parent && this.parent.parent && this.parent.parent.key === 'paths') { // debugFilterStep = 'Filter - fields - operations' for (let i = 0; i < filterOperations.length; i++) { if (isMatchOperationItem(this.parent.key, this.key, filterOperations[i])) { this.delete(); } } } // Filter out operations not matching inverseFilterArray if (inverseFilterArray.length > 0 && this.parent && this.parent.parent && this.parent.parent.key === 'paths') { if (node.tags === undefined || !inverseFilterArray.some(i => node.tags.includes(i))) { this.delete(); } } // Keep fields matching the inverseFlags if ( inverseFilterFlags.length > 0 && ((this.path[0] === 'paths' && this.level === 3) || (this.path[0] === 'components' && this.level === 3)) ) { const itmObj = node; const matchesInverseFlag = inverseFilterFlags.some(flagKey => { return itmObj.hasOwnProperty(flagKey); }); if (!matchesInverseFlag) { // debugFilterStep = 'Filter - Single field - inverseFlags' this.remove(); } } // Keep fields matching the inverseFlagValues if ( inverseFilterFlagValuesKeys.length > 0 && ((this.path[0] === 'paths' && this.level === 3) || (this.path[0] === 'components' && this.level === 3)) ) { const itmObj = node; const matchesInverseFlag = inverseFilterFlagValues.some(flagObj => { const flagKey = Object.keys(flagObj)[0]; // Get the key of the flagObj const flagValue = flagObj[flagKey]; // Get the value of the flagObj // Check if the key exists in itmObj and if its value matches the value in flagObj return itmObj.hasOwnProperty(flagKey) && itmObj[flagKey] === flagValue; }); if (!matchesInverseFlag) { // debugFilterStep = 'Filter - Single field - inverseFlagValues - single value' this.remove(); } } // Filter out OpenAPI.tags & OpenAPI.x-tagGroups matching the flags if ((this.key === 'tags' || this.key === 'x-tagGroups') && this.parent.key === undefined && Array.isArray(node)) { let oaTags = JSON.parse(JSON.stringify(node)); // Deep copy of the object if (filterProps.length > 0) { // debugFilterStep = 'Filter - tag/x-tagGroup - flags' // Deep filter array of tag/x-tagGroup oaTags = oaTags.filter(item => !filterProps.some(i => Object.keys(item || {}).includes(i))); this.update(oaTags); // const oaFilteredTags = oaTags.filter(item => !filterProps.some(i => (Object.keys(item || {}).includes(i)))); // this.update(oaFilteredTags); } if (filterFlagValues.length > 0) { // debugFilterStep = 'Filter - tag - flagValues' // Deep filter array of tag/x-tagGroup for (let i = 0; i < filterFlagValues.length; i++) { let [key, value] = Object.entries(filterFlagValues[i])[0]; oaTags = oaTags.filter(item => item[key] !== value); } this.update(oaTags); } } // Filter out markdown comments in description fields if (this.key === 'description' && isString(node)) { const lines = node.split('\n'); if (lines.length > 1) { const filtered = lines.filter(line => !line.startsWith('[comment]: <>')); const cleanDescription = filtered.join('\n'); this.update(cleanDescription); node = cleanDescription; } } // Replace words in text with new value if ( isString(node) && textReplace.length > 0 && (this.key === 'description' || this.key === 'summary' || this.key === 'url') ) { const replaceRes = valueReplace(node, textReplace); this.update(replaceRes); node = replaceRes; } }); // Calculate comps.meta.total at the end // comps.meta.total = Object.keys(comps.schemas).length + // Object.keys(comps.responses).length + // Object.keys(comps.parameters).length + // Object.keys(comps.examples).length + // Object.keys(comps.requestBodies).length + // Object.keys(comps.headers).length; // Collect unused components const optFs = get(options, 'filterSet.unusedComponents', []) || []; // Identify components that are directly unused (not referenced anywhere) unusedComp.schemas = Object.keys(comps.schemas || {}).filter(key => !comps.schemas[key].used); unusedComp.responses = Object.keys(comps.responses || {}).filter(key => !comps.responses[key].used); unusedComp.parameters = Object.keys(comps.parameters || {}).filter(key => !comps.parameters[key].used); unusedComp.examples = Object.keys(comps.examples || {}).filter(key => !comps.examples[key].used); unusedComp.requestBodies = Object.keys(comps.requestBodies || {}).filter(key => !comps.requestBodies[key].used); unusedComp.headers = Object.keys(comps.headers || {}).filter(key => !comps.headers[key].used); const refGraph = {schemas: {}, responses: {}, parameters: {}, examples: {}, requestBodies: {}, headers: {}}; const rootRefs = new Set(); // Traverse $ref in components traverse(jsonObj).forEach(function (node) { if (this.key !== '$ref' || typeof node !== 'string') return; const m = node.match(/^#\/components\/([^\/]+)\/(.+)$/); if (!m) return; const [, tgtType, tgtKey] = m; if (this.path[0] === 'components') { const [, ownType, ownKey] = this.path; refGraph[ownType] ||= {}; refGraph[ownType][ownKey] ||= []; refGraph[ownType][ownKey].push({type: tgtType, key: tgtKey}); } else { rootRefs.add(`${tgtType}:${tgtKey}`); } }); // Mark visited components const visited = new Set(); const queue = [...rootRefs]; while (queue.length) { const id = queue.shift(); if (visited.has(id)) continue; visited.add(id); const [type, key] = id.split(':'); const edges = (refGraph[type] && refGraph[type][key]) || []; for (const {type: ct, key: ck} of edges) { const childId = `${ct}:${ck}`; if (!visited.has(childId)) queue.push(childId); } } // Mark not visited as unused for (const t of ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers']) { unusedComp[t] = Object.keys(comps[t] || {}).filter(k => !visited.has(`${t}:${k}`)); } // TODO rework this logic unusedComp.meta = { total: unusedComp.schemas.length + unusedComp.responses.length + unusedComp.parameters.length + unusedComp.examples.length + unusedComp.requestBodies.length + unusedComp.headers.length }; // Update options.unusedComp with all identified unused components if (optFs.includes('schemas')) options.unusedComp.schemas = [...options.unusedComp.schemas, ...unusedComp.schemas]; if (optFs.includes('responses')) options.unusedComp.responses = [...options.unusedComp.responses, ...unusedComp.responses]; if (optFs.includes('parameters')) options.unusedComp.parameters = [...options.unusedComp.parameters, ...unusedComp.parameters]; if (optFs.includes('examples')) options.unusedComp.examples = [...options.unusedComp.examples, ...unusedComp.examples]; if (optFs.includes('requestBodies')) options.unusedComp.requestBodies = [...options.unusedComp.requestBodies, ...unusedComp.requestBodies]; if (optFs.includes('headers')) options.unusedComp.headers = [...options.unusedComp.headers, ...unusedComp.headers]; // TODO rework this logic // Update unusedComp.meta.total after each recursion options.unusedComp.meta.total = options.unusedComp.schemas.length + options.unusedComp.responses.length + options.unusedComp.parameters.length + options.unusedComp.examples.length + options.unusedComp.requestBodies.length + options.unusedComp.headers.length; // Clean-up jsonObj traverse(jsonObj).forEach(function (node) { // Remove unused component if (this.path[0] === 'components' && this.level === 3 && stripUnused.length > 0) { if (stripUnused.includes(this.path[1]) && unusedComp[this.path[1]].includes(this.key)) { // debugFilterStep = 'Filter - Remove unused components' this.delete(); // Trigger recurse doRecurse = true; } } // Filter out OpenAPI.tags & OpenAPI.x-tagGroups matching the fixedFlags if ((this.key === 'tags' || this.key === 'x-tagGroups') && this.parent.key === undefined && Array.isArray(node)) { if (fixedFlags.length > 0) { // debugFilterStep = 'Filter - tag/x-tagGroup - fixed flags' // Deep filter array of tag/x-tagGroup let oaTags = JSON.parse(JSON.stringify(node)); // Deep copy of the object const oaFilteredTags = oaTags .filter(item => !fixedFlags.some(i => Object.keys(item || {}).includes(i))) .filter(e => e); this.update(oaFilteredTags); } } // Remove empty objects if (node && Object.keys(node).length === 0 && node.constructor === Object) { // Remove empty objects - preserveEmptyObjects: undefined if ( typeof filterSet.preserveEmptyObjects === 'undefined' && !['security', 'schemas', 'default', 'oneOf', 'allOf'].includes(this.parent.key) && (this.key === 'examples' || this.key === 'example' || (!this.path.includes('example') && !this.path.includes('examples'))) ) { // debugFilterStep = 'Filter - Remove empty objects' this.delete(); // Trigger recurse doRecurse = true; } // Remove empty objects - preserveEmptyObjects: false if (filterSet.preserveEmptyObjects === false && !['security', 'schemas', 'default'].includes(this.parent.key)) { // debugFilterStep = 'Filter - Remove empty objects' this.delete(); // Trigger recurse doRecurse = true; } // Remove empty objects - preserveEmptyObjects: [...] if ( Array.isArray(filterSet.preserveEmptyObjects) && ((!['security', 'schemas', 'default'].includes(this.parent.key) && !filterSet.preserveEmptyObjects.includes(this.key)) || !filterSet.preserveEmptyObjects.some(v => this.path.includes(v))) ) { // debugFilterStep = 'Filter - Remove empty objects' this.delete(); // Trigger recurse doRecurse = true; } } // Remove path items without operations if (this.parent && this.parent.key === 'paths' && this.level === 2 && !httpVerbs.some(i => this.keys.includes(i))) { // debugFilterStep = 'Filter - Remove empty paths' this.delete(); } // Strip flags if (stripFlags.length > 0 && stripFlags.includes(this.key)) { // debugFilterStep = 'Filter - Strip flags' this.delete(); } }); // Recurse to strip any remaining unusedComp, to a maximum depth of 10 if ( doRecurse === true || options.unusedDepth === 0 || (stripUnused.length > 0 && unusedComp.meta.total > 0 && options.unusedDepth <= 10) ) { options.unusedDepth++; const resultObj = await openapiFilter(jsonObj, options); jsonObj = resultObj.data; unusedComp = JSON.parse(JSON.stringify(options.unusedComp)); } // Prepare totalComp for the final result const totalComp = { schemas: Object.keys(comps.schemas), responses: Object.keys(comps.responses), parameters: Object.keys(comps.parameters), examples: Object.keys(comps.examples), requestBodies: Object.keys(comps.requestBodies), headers: Object.keys(comps.headers), meta: {total: comps.meta.total} }; // Return result object return {data: jsonObj, resultData: {unusedComp: unusedComp, totalComp: totalComp}}; } /** * OpenAPI Change Case function * Traverse through all keys and based on the key name, change the case the props according to the casing configuration. * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format casing options * @returns {object} Change casing OpenAPI document */ async function openapiChangeCase(oaObj, options) { let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object let defaultCasing = {}; // JSON.parse(fs.readFileSync(__dirname + "/defaultFilter.json", 'utf8')) let casingSet = Object.assign({}, defaultCasing, options.casingSet); let debugCasingStep = ''; // uncomment // debugCasingStep below to see which sort part is triggered // Could add a default for all types pretty easily. const changeCasingKeyPlans = { query: casingSet.componentsParametersQuery, path: casingSet.componentsParametersPath, header: casingSet.componentsParametersHeader, cookie: casingSet.componentsParametersCookie }; // Could add a default for all types pretty easily. const changeCasingNamePlans = { query: casingSet.parametersQuery, path: casingSet.parametersPath, header: casingSet.parametersHeader, cookie: casingSet.parametersCookie }; // Initiate components tracking const comps = { parameters: {} }; // Recursive traverse through OpenAPI document to update components traverse(jsonObj).forEach(function (node) { // Focus only on the components if (this.path[0] === 'components') { // Change components/schemas - names if (this.path[1] === 'schemas' && this.path.length === 2 && casingSet.componentsSchemas) { // debugCasingStep = 'Casing - components/schemas - names' this.update(changeObjKeysCase(node, casingSet.componentsSchemas)); } // Change components/examples - names if (this.path[1] === 'examples' && this.path.length === 2 && casingSet.componentsExamples) { // debugCasingStep = 'Casing - components/examples - names' this.update(changeObjKeysCase(node, casingSet.componentsExamples)); } // Change components/headers - names if (this.path[1] === 'headers' && this.path.length === 2 && casingSet.componentsHeaders) { // debugCasingStep = 'Casing - components/headers - names' this.update(changeObjKeysCase(node, casingSet.componentsHeaders)); } // Change components/parameters - in:query/in:headers/in:path/in:cookie - key if ( this.path[1] === 'parameters' && this.path.length === 2 && changeComponentParametersCasingEnabled(casingSet) ) { const orgObj = JSON.parse(JSON.stringify(node)); let replacedItems = Object.keys(orgObj).map(key => { const parameterFoundIn = orgObj[key].in; if (orgObj[key].in && changeCasingKeyPlans.hasOwnProperty(parameterFoundIn)) { const changeCasingKeyPlan = changeCasingKeyPlans[parameterFoundIn]; if (changeCasingKeyPlan) { // debugCasingStep = `Casing - components/parameters - in:${parameterFoundIn} - key` const newKey = changeCase(key, changeCasingKeyPlan); comps.parameters[key] = newKey; return {[newKey]: orgObj[key]}; } } }); this.update(Object.assign({}, ...replacedItems)); } // Change components/parameters - query/header/path/cookie name if (this.path[1] === 'parameters' && this.path.length === 3) { if (node.in && changeCasingNamePlans.hasOwnProperty(node.in)) { const changeCasingNamePlan = changeCasingNamePlans[node.in]; if (changeCasingNamePlan) { // debugCasingStep = `Casing - path > parameters/${node.in} - name` node.name = changeCase(node.name, changeCasingNamePlan); this.update(node); } } } // Change components/responses - names if (this.path[1] === 'responses' && this.path.length === 2 && casingSet.componentsResponses) { // debugCasingStep = 'Casing - components/responses - names' this.update(changeObjKeysCase(node, casingSet.componentsResponses)); } // Change components/requestBodies - names if (this.path[1] === 'requestBodies' && this.path.length === 2 && casingSet.componentsRequestBodies) { // debugCasingStep = 'Casing - components/requestBodies - names' this.update(changeObjKeysCase(node, casingSet.componentsRequestBodies)); } // Change components/securitySchemes - names if (this.path[1] === 'securitySchemes' && this.path.length === 2 && casingSet.componentsSecuritySchemes) { // debugCasingStep = 'Casing - components/securitySchemes - names' this.update(changeObjKeysCase(node, casingSet.componentsSecuritySchemes)); } } }); // Recursive traverse through OpenAPI document for non-components traverse(jsonObj).forEach(function (node) { // Change components $ref names if (this.key === '$ref') { if (node.startsWith('#/components/schemas/') && casingSet.componentsSchemas) { const compName = node.replace('#/components/schemas/', ''); this.update(`#/components/schemas/${changeCase(compName, casingSet.componentsSchemas)}`); } if (node.startsWith('#/components/examples/') && casingSet.componentsExamples) { const compName = node.replace('#/components/examples/', ''); this.update(`#/components/examples/${changeCase(compName, casingSet.componentsExamples)}`); } if (node.startsWith('#/components/responses/') && casingSet.componentsResponses) { const compName = node.replace('#/components/responses/', ''); this.update(`#/components/responses/${changeCase(compName, casingSet.componentsResponses)}`); } if (node.startsWith('#/components/parameters/')) { const compName = node.replace('#/components/parameters/', ''); if (comps.parameters[compName]) { this.update(`#/components/parameters/${comps.parameters[compName]}`); } } if (node.startsWith('#/components/headers/') && casingSet.componentsHeaders) { const compName = node.replace('#/components/headers/', ''); this.update(`#/components/headers/${changeCase(compName, casingSet.componentsHeaders)}`); } if (node.startsWith('#/components/requestBodies/') && casingSet.componentsRequestBodies) { const compName = node.replace('#/components/requestBodies/', ''); this.update(`#/components/requestBodies/${changeCase(compName, casingSet.componentsRequestBodies)}`); } if (node.startsWith('#/components/securitySchemes/') && casingSet.componentsSecuritySchemes) { const compName = node.replace('#/components/securitySchemes/', ''); this.update(`#/components/securitySchemes/${changeCase(compName, casingSet.componentsSecuritySchemes)}`); } } // Change operationId if (this.key === 'operationId' && casingSet.operationId && this.path[0] === 'paths' && this.path.length === 4) { // debugCasingStep = 'Casing - Single field - OperationId' this.update(changeCase(node, casingSet.operationId)); } // Change summary if (this.key === 'summary' && casingSet.summary) { // debugCasingStep = 'Casing - Single field - summary' this.update(changeCase(node, casingSet.summary)); } // Change description if (this.key === 'description' && casingSet.description) { // debugCasingStep = 'Casing - Single field - description' this.update(changeCase(node, casingSet.description)); } // Change paths > examples - name if (this.path[0] === 'paths' && this.key === 'examples' && casingSet.componentsExamples) { // debugCasingStep = 'Casing - Single field - examples name' this.update(changeObjKeysCase(node, casingSet.componentsExamples)); } // Change components/schemas - properties if ( this.path[1] === 'schemas' && this.key === 'properties' && casingSet.properties && this.parent && this.parent.key !== 'properties' && this.parent.key !== 'value' ) { // debugCasingStep = 'Casing - components/schemas - properties name' this.update(changeObjKeysCase(node, casingSet.properties)); } // Change components/schemas - required properties if (this.path[1] === 'schemas' && this.parent.key === 'required' && casingSet.properties) { // debugCasingStep = 'Casing - components/schemas - required properties' this.update(changeCase(node, casingSet.properties)); } // Change paths > schema - properties if ( this.path[0] === 'paths' && this.key === 'properties' && casingSet.properties && this.parent && this.parent.key !== 'properties' && this.parent.key !== 'value' ) { // debugCasingStep = 'Casing - paths > schema - properties name' this.update(changeObjKeysCase(node, casingSet.properties)); } // Change security - keys if (this.path[0] === 'paths' && this.key === 'security' && isArray(node) && casingSet.componentsSecuritySchemes) { // debugCasingStep = 'Casing - path > - security' this.update(changeArrayObjKeysCase(node, casingSet.componentsSecuritySchemes)); } // Change parameters - name if (this.path[0] === 'paths' && this.key === 'parameters' && changeParametersCasingEnabled(casingSet)) { // debugCasingStep = 'Casing - components > parameters - name' // Loop over parameters array let params = JSON.parse(JSON.stringify(node)); // Deep copy of the schema object for (let i = 0; i < params.length; i++) { if (params[i].in && changeCasingNamePlans.hasOwnProperty(params[i].in)) { const changeCasingNamePlan = changeCasingNamePlans[params[i].in]; if (changeCasingNamePlan) { // debugCasingStep = 'Casing - path > parameters/query- name' params[i].name = changeCase(params[i].name, changeCasingNamePlan); } } } this.update(params); } }); // Return result object return {data: jsonObj, resultData: {}}; } /** * OpenAPI Generate elements function * Traverse through all key. * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format generate options * @returns {object} Updated OpenAPI document with generated elements */ async function openapiGenerate(oaObj, options) { let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object let generateDefault = {overwriteExisting: false}; let generateSet = Object.assign({}, generateDefault, options.generateSet); // Recursive traverse through OpenAPI document to update components traverse(jsonObj).forEach(function (node) { if (this.path[0] === 'paths' && this.path.length === 3 && generateSet?.operationIdTemplate) { const operationIdExists = !!node.operationId; // Generate operationId element if (generateSet.overwriteExisting || !operationIdExists) { const oaOperation = getOperation(this.path[1], this.path[2], oaObj); const newOperationId = parseTpl({ template: generateSet.operationIdTemplate, oaOperation }); // debugGenerateStep = 'Generate - OperationId' node.operationId = newOperationId; this.update(node); } } }); // Return result object return {data: jsonObj, resultData: {}}; } /** * Split the OpenAPI document into a multi-file structure * @param {object} oaObj OpenAPI document * @param options * @returns {Promise<void>} */ async function openapiSplit(oaObj, options = {}) { if (!options.output) { throw new Error('Output is required'); } options.outputDir = dirname(options.output); options.extension = extname(options.output).substring(1); if (oaObj?.components) { await writeComponents(oaObj.components, options); } if (oaObj?.paths) { await writePaths(oaObj.paths, options); } await writeSplitOpenAPISpec(oaObj, options); } /** * OpenAPI convert version function * Convert OpenAPI from version 3.0 to 3.1 * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format convert options * @returns {object} converted OpenAPI document */ async function openapiConvertVersion(oaObj, options) { let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object // let debugConvertVersionStep = '' // uncomment // debugConvertVersionStep below to see which sort part is triggered // Change OpenAPI version jsonObj.openapi = '3.1.0'; // Change x-webhooks to webhooks if (jsonObj['x-webhooks']) { jsonObj = setInObject(jsonObj, 'webhooks', jsonObj['x-webhooks'], 'x-webhooks'); // jsonObj.webhooks = jsonObj['x-webhooks'] delete jsonObj['x-webhooks']; } // Recursive traverse through OpenAPI document for deprecated 3.0 properties traverse(jsonObj).forEach(function (node) { if (typeof node === 'object' && node !== null) { // Change components/schemas - properties if (node.type) { // Change type > nullable node = convertNullable(node); // Change type > example node = convertExample(node); // Change type > exclusiveMinimum node = convertExclusiveMinimum(node); // Change type > exclusiveMaximum node = convertExclusiveMaximum(node); // Change type > single enum node = convertConst(node); this.update(node); } // Change components/schemas - schema if (node.schema) { // File Upload Payloads if ( get(this, 'parent.key') && this.parent.key === 'content' && get(this, 'parent.parent.key') && this.parent.parent.key === 'requestBody' ) { // Remove schema for application/octet-stream if (this.key === 'application/octet-stream') { this.update({}); } // Convert schema for images if (this.key && this.key.startsWith('image/')) { node = convertImageBase64(node); this.update(node); } } } // Convert schema for multipart file uploads with a binary file if (get(this, 'parent.parent.parent.key') && this.parent.parent.parent.key === 'multipart/form-data') { node = convertMultiPartBinary(node); this.update(node); } } }); // Return result object return {data: jsonObj, resultData: {}}; } /** * OpenAPI rename function * Change the title of the OpenAPI document with a provided value. * @param {object} oaObj OpenAPI document * @param {object} options OpenAPI-format options * @returns {object} Renamed OpenAPI document */ async function openapiRename(oaObj, options) { let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object // OpenAPI 3 if (jsonObj.info && jsonObj.info.title && options.rename && options.rename !== '') { jsonObj.info.title = options.rename; } // Return result object return {data: jsonObj, resultData: {}}; } module.exports = { openapiFilter: openapiFilter, openapiGenerate: openapiGenerate, openapiSort: openapiSort, openapiChangeCase: openapiChangeCase, openapiOverlay: openapiOverlay, openapiSplit: openapiSplit, openapiConvertVersion: openapiConvertVersion, openapiRename: openapiRename, readFile: readFile, parseFile: parseFile, parseString: parseString, stringify: stringify, writeFile: writeFile, detectFormat: detectFormat, analyzeOpenApi: analyzeOpenApi, changeCase: changeCase, resolveJsonPath: resolveJsonPath, resolveJsonPathValue: resolveJsonPathValue };