-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(openapi): adding a command to resolve circular and recursive references #1063
base: v9
Are you sure you want to change the base?
Changes from 4 commits
59bb8f2
d82700a
099a6f1
95208cf
6249347
0a5f141
03c092c
3660917
7ca5197
597ece5
0b87569
3039ea0
dca7b9f
349b9f8
88bd34e
0a3c4e0
0c0027e
18f7b4e
8fcc065
9fa0ca3
a4b1fc0
c0aa486
d938932
44571ae
3f1795b
b01b18a
1d1c7e2
1a25b65
751c52a
19875c1
43f2c82
37f4d5b
e8d2b9a
b293e72
e2a0948
87bdd5e
91dbb5a
6de2193
a55dd14
ce09500
b224ec6
9c037c9
da5b0ad
9249d2a
dd02080
4355f9c
a7d8555
f010a7d
f7e52bf
8bcf655
2667d1b
1d119f2
e790dec
ae39d1e
6e65d9f
91958bd
09b8039
4011248
0400bae
5df03c2
3721546
304072d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,235 @@ | ||||||||||||||||||||||||||||||||||||||||||
/* eslint-disable no-param-reassign */ | ||||||||||||||||||||||||||||||||||||||||||
import type { ZeroAuthCommandOptions } from '../../lib/baseCommand.js'; | ||||||||||||||||||||||||||||||||||||||||||
import type { OASDocument } from 'oas/types'; | ||||||||||||||||||||||||||||||||||||||||||
import type { IJsonSchema } from 'openapi-types'; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
import fs from 'node:fs'; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// eslint-disable-next-line import/no-extraneous-dependencies | ||||||||||||||||||||||||||||||||||||||||||
import jsYaml from 'js-yaml'; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
import analyzeOas from '../../lib/analyzeOas.js'; | ||||||||||||||||||||||||||||||||||||||||||
import Command, { CommandCategories } from '../../lib/baseCommand.js'; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
interface Options { | ||||||||||||||||||||||||||||||||||||||||||
spec?: string; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
type SchemaCollection = Record<string, IJsonSchema>; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
class OpenAPISolvingCircularityAndRecursiveness extends Command { | ||||||||||||||||||||||||||||||||||||||||||
constructor() { | ||||||||||||||||||||||||||||||||||||||||||
super(); | ||||||||||||||||||||||||||||||||||||||||||
this.command = 'openapi:refs'; | ||||||||||||||||||||||||||||||||||||||||||
this.usage = 'openapi:refs [file]'; | ||||||||||||||||||||||||||||||||||||||||||
this.description = | ||||||||||||||||||||||||||||||||||||||||||
'The script resolves circular and recursive references in OpenAPI by replacing them with object schemas. However, not all circular references can be resolved. You can run the openapi:inspect command to identify which references remain unresolved.'; | ||||||||||||||||||||||||||||||||||||||||||
this.cmdCategory = CommandCategories.APIS; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
this.hiddenArgs = ['spec']; | ||||||||||||||||||||||||||||||||||||||||||
this.args = [ | ||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||
name: 'spec', | ||||||||||||||||||||||||||||||||||||||||||
type: String, | ||||||||||||||||||||||||||||||||||||||||||
defaultOption: true, | ||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||
]; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Reads and parses an OpenAPI file (JSON or YAML). | ||||||||||||||||||||||||||||||||||||||||||
* @param {string} filePath - The file path to read. | ||||||||||||||||||||||||||||||||||||||||||
* @returns {any} The parsed content of the file. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static readOpenApiFile(filePath: string) { | ||||||||||||||||||||||||||||||||||||||||||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||||
return filePath.endsWith('.json') ? JSON.parse(fileContent) : jsYaml.load(fileContent); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Writes OpenAPI data to a file (JSON or YAML). | ||||||||||||||||||||||||||||||||||||||||||
* @param {string} filePath - The file path to write to. | ||||||||||||||||||||||||||||||||||||||||||
* @param {OASDocument} data - The data to be written. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static writeOpenApiFile(filePath: string, data: OASDocument) { | ||||||||||||||||||||||||||||||||||||||||||
const content = filePath.endsWith('.json') ? JSON.stringify(data, null, 2) : jsYaml.dump(data, { noRefs: true }); // Disables YAML anchors | ||||||||||||||||||||||||||||||||||||||||||
fs.writeFileSync(filePath, content, 'utf8'); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Identifies circular references in the OpenAPI document. | ||||||||||||||||||||||||||||||||||||||||||
* @param {OASDocument} document - The OpenAPI document to analyze. | ||||||||||||||||||||||||||||||||||||||||||
* @returns {Promise<string[]>} A list of circular reference paths. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static async getCircularRefsFromOas(document: OASDocument): Promise<string[]> { | ||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||
const analysis = await analyzeOas(document); | ||||||||||||||||||||||||||||||||||||||||||
const circularRefs = analysis.openapi.circularRefs; | ||||||||||||||||||||||||||||||||||||||||||
return Array.isArray(circularRefs.locations) ? circularRefs.locations : []; | ||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||
return [`Error analyzing OpenAPI document: ${error}`]; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Replaces a reference in a schema with an object if it's circular or recursive. | ||||||||||||||||||||||||||||||||||||||||||
* @param {IJsonSchema} schema - The schema to process. | ||||||||||||||||||||||||||||||||||||||||||
* @param {string[]} circularRefs - List of circular reference paths. | ||||||||||||||||||||||||||||||||||||||||||
* @param {string} schemaName - The name of the schema being processed. | ||||||||||||||||||||||||||||||||||||||||||
* @returns {IJsonSchema} The modified schema or the original. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static replaceRefWithObject(schema: IJsonSchema, circularRefs: string[], schemaName: string): IJsonSchema { | ||||||||||||||||||||||||||||||||||||||||||
if (schema.$ref) { | ||||||||||||||||||||||||||||||||||||||||||
const refSchemaName = schema.$ref.split('/').pop() as string; | ||||||||||||||||||||||||||||||||||||||||||
const isCircular = circularRefs.some(refPath => refPath.includes(refSchemaName)); | ||||||||||||||||||||||||||||||||||||||||||
const isRecursive = schemaName === refSchemaName; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (schemaName.includes('Ref') && (isCircular || isRecursive)) { | ||||||||||||||||||||||||||||||||||||||||||
return { type: 'object' } as IJsonSchema; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
return schema; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Recursively replaces references in schemas, transforming circular references to objects. | ||||||||||||||||||||||||||||||||||||||||||
* @param {IJsonSchema} schema - The schema to process. | ||||||||||||||||||||||||||||||||||||||||||
* @param {string[]} circularRefs - List of circular reference paths. | ||||||||||||||||||||||||||||||||||||||||||
* @param {string} schemaName - The name of the schema being processed. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static replaceReferencesInSchema(schema: IJsonSchema, circularRefs: string[], schemaName: string) { | ||||||||||||||||||||||||||||||||||||||||||
if (schema.type === 'object' && schema.properties) { | ||||||||||||||||||||||||||||||||||||||||||
for (const prop of Object.keys(schema.properties)) { | ||||||||||||||||||||||||||||||||||||||||||
let property = JSON.parse(JSON.stringify(schema.properties[prop])); | ||||||||||||||||||||||||||||||||||||||||||
property = OpenAPISolvingCircularityAndRecursiveness.replaceRefWithObject(property, circularRefs, schemaName); | ||||||||||||||||||||||||||||||||||||||||||
schema.properties[prop] = property; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// Handle arrays with item references | ||||||||||||||||||||||||||||||||||||||||||
if (property.type === 'array' && property.items) { | ||||||||||||||||||||||||||||||||||||||||||
property.items = JSON.parse(JSON.stringify(property.items)); | ||||||||||||||||||||||||||||||||||||||||||
property.items = OpenAPISolvingCircularityAndRecursiveness.replaceRefWithObject( | ||||||||||||||||||||||||||||||||||||||||||
property.items, | ||||||||||||||||||||||||||||||||||||||||||
circularRefs, | ||||||||||||||||||||||||||||||||||||||||||
schemaName, | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
OpenAPISolvingCircularityAndRecursiveness.replaceReferencesInSchema(property.items, circularRefs, schemaName); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* Replaces circular references within a collection of schemas. | ||||||||||||||||||||||||||||||||||||||||||
* @param {SchemaCollection} schemas - Collection of schemas to modify. | ||||||||||||||||||||||||||||||||||||||||||
* @param {string[]} circularRefs - List of circular reference paths. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
static replaceCircularRefs(schemas: SchemaCollection, circularRefs: string[]): void { | ||||||||||||||||||||||||||||||||||||||||||
const createdRefs = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
function replaceRef(schemaName: string, propertyName: string, refSchemaName: string) { | ||||||||||||||||||||||||||||||||||||||||||
schemas[schemaName]!.properties![propertyName] = { $ref: `#/components/schemas/${refSchemaName}` } as IJsonSchema; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
function createRefSchema(originalSchemaName: string, refSchemaName: string) { | ||||||||||||||||||||||||||||||||||||||||||
if (!createdRefs.has(refSchemaName) && schemas[originalSchemaName]) { | ||||||||||||||||||||||||||||||||||||||||||
schemas[refSchemaName] = { | ||||||||||||||||||||||||||||||||||||||||||
type: 'object', | ||||||||||||||||||||||||||||||||||||||||||
properties: { ...schemas[originalSchemaName].properties }, | ||||||||||||||||||||||||||||||||||||||||||
} as IJsonSchema; | ||||||||||||||||||||||||||||||||||||||||||
OpenAPISolvingCircularityAndRecursiveness.replaceReferencesInSchema( | ||||||||||||||||||||||||||||||||||||||||||
schemas[refSchemaName], | ||||||||||||||||||||||||||||||||||||||||||
circularRefs, | ||||||||||||||||||||||||||||||||||||||||||
refSchemaName, | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
createdRefs.add(refSchemaName); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
circularRefs.forEach(refPath => { | ||||||||||||||||||||||||||||||||||||||||||
const refParts = refPath.split('/'); | ||||||||||||||||||||||||||||||||||||||||||
if (refParts.length < 6) { | ||||||||||||||||||||||||||||||||||||||||||
throw new Error(`Invalid reference path: ${refPath}`); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const schemaName = refParts[3]; | ||||||||||||||||||||||||||||||||||||||||||
const propertyName = refParts[5]; | ||||||||||||||||||||||||||||||||||||||||||
const schema = schemas[schemaName]; | ||||||||||||||||||||||||||||||||||||||||||
const property = schema?.properties?.[propertyName]; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (!schema || !property) { | ||||||||||||||||||||||||||||||||||||||||||
throw new Error(`Schema or property not found for path: ${refPath}`); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
// Handle references within items in an array | ||||||||||||||||||||||||||||||||||||||||||
let refSchemaName: string | undefined; | ||||||||||||||||||||||||||||||||||||||||||
if ( | ||||||||||||||||||||||||||||||||||||||||||
refParts.length > 6 && | ||||||||||||||||||||||||||||||||||||||||||
refParts[6] === 'items' && | ||||||||||||||||||||||||||||||||||||||||||
property.type === 'array' && | ||||||||||||||||||||||||||||||||||||||||||
property.items && | ||||||||||||||||||||||||||||||||||||||||||
typeof property.items === 'object' | ||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||
const itemsRefSchemaName = (property.items as IJsonSchema).$ref?.split('/')[3]; | ||||||||||||||||||||||||||||||||||||||||||
if (itemsRefSchemaName) { | ||||||||||||||||||||||||||||||||||||||||||
refSchemaName = `${itemsRefSchemaName}Ref`; | ||||||||||||||||||||||||||||||||||||||||||
property.items = { $ref: `#/components/schemas/${refSchemaName}` } as IJsonSchema; | ||||||||||||||||||||||||||||||||||||||||||
createRefSchema(itemsRefSchemaName, refSchemaName); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||
// Handle direct reference | ||||||||||||||||||||||||||||||||||||||||||
refSchemaName = property.$ref?.split('/')[3]; | ||||||||||||||||||||||||||||||||||||||||||
if (refSchemaName) { | ||||||||||||||||||||||||||||||||||||||||||
const newRefSchemaName = `${refSchemaName}Ref`; | ||||||||||||||||||||||||||||||||||||||||||
replaceRef(schemaName, propertyName, newRefSchemaName); | ||||||||||||||||||||||||||||||||||||||||||
createRefSchema(refSchemaName, newRefSchemaName); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||
* The main execution method for the command. | ||||||||||||||||||||||||||||||||||||||||||
* @param {ZeroAuthCommandOptions<Options>} opts - Command options. | ||||||||||||||||||||||||||||||||||||||||||
* @returns {Promise<string>} Result message. | ||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||
async run(opts: ZeroAuthCommandOptions<Options>): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||
await super.run(opts); | ||||||||||||||||||||||||||||||||||||||||||
const { spec } = opts; | ||||||||||||||||||||||||||||||||||||||||||
if (!spec) { | ||||||||||||||||||||||||||||||||||||||||||
return 'File path is required.'; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
const openApiData = OpenAPISolvingCircularityAndRecursiveness.readOpenApiFile(spec); | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rather than using this static method to read the file, can you use our existing rdme/src/cmds/openapi/convert.ts Line 57 in 5db25a1
|
||||||||||||||||||||||||||||||||||||||||||
const circularRefs = await OpenAPISolvingCircularityAndRecursiveness.getCircularRefsFromOas(openApiData); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (circularRefs.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||
return 'The file does not contain circular or recursive references.'; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (openApiData.components?.schemas && circularRefs.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||
OpenAPISolvingCircularityAndRecursiveness.replaceCircularRefs(openApiData.components.schemas, circularRefs); | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
let remainingCircularRefs = await OpenAPISolvingCircularityAndRecursiveness.getCircularRefsFromOas(openApiData); | ||||||||||||||||||||||||||||||||||||||||||
let iterationCount = 0; | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
while (remainingCircularRefs.length > 0 && iterationCount < 5) { | ||||||||||||||||||||||||||||||||||||||||||
OpenAPISolvingCircularityAndRecursiveness.replaceCircularRefs( | ||||||||||||||||||||||||||||||||||||||||||
openApiData.components.schemas, | ||||||||||||||||||||||||||||||||||||||||||
remainingCircularRefs, | ||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||
remainingCircularRefs = remainingCircularRefs.length > 0 ? [] : remainingCircularRefs; | ||||||||||||||||||||||||||||||||||||||||||
iterationCount += 1; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
if (iterationCount >= 5) { | ||||||||||||||||||||||||||||||||||||||||||
return 'Maximum iteration limit reached. Some circular references may remain unresolved.'; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
OpenAPISolvingCircularityAndRecursiveness.writeOpenApiFile(spec, openApiData); | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rather than overwriting the existing file, can you add a prompt (and a corresponding rdme/src/cmds/openapi/convert.ts Lines 66 to 85 in 5db25a1
|
||||||||||||||||||||||||||||||||||||||||||
return `Processed and updated ${spec}`; | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
export default OpenAPISolvingCircularityAndRecursiveness; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you delete this file?