-
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
Open
olehshh
wants to merge
58
commits into
next
Choose a base branch
from
oleh/openapi-adding-command-to-solve-circularity-and-recursiveness
base: next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,341
−2
Open
Changes from 7 commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
59bb8f2
Adding refs to the list of openapi commands
d82700a
Code to execute the openapi:refs command
099a6f1
style: apply Prettier formatting
95208cf
Add 'openapi:refs' command and update related test snapshots
6249347
docs(openapi) command description
0a5f141
fix(openapi) use of a premade solution for processing a file
03c092c
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 3660917
chore: update package-lock.json
7ca5197
Merge branch 'next' of https://github.com/readmeio/rdme into oleh/ope…
597ece5
refactor: align code with recent updates
0b87569
Merge branch 'oleh/openapi-adding-command-to-solve-circularity-and-re…
3039ea0
chore: minor correction
dca7b9f
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 349b9f8
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 88bd34e
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 0a3c4e0
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 0c0027e
test: openapi:refs
18f7b4e
Merge branch 'oleh/openapi-adding-command-to-solve-circularity-and-re…
8fcc065
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 9fa0ca3
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh a4b1fc0
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh c0aa486
Merge remote-tracking branch 'origin/next' into oleh/openapi-adding-c…
d938932
docs: updating documentation after merge
44571ae
chore: small text change
3f1795b
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh b01b18a
refactor: align code with recent updates
1d1c7e2
feat: add check and display appropriate message for files that cannot…
1a25b65
test: unresolvable files
751c52a
chore: gaps
19875c1
feat: processing of circularity that cannot be processed
43f2c82
test: unprocessable files
37f4d5b
chore: correction
e8d2b9a
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh b293e72
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
e2a0948
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 87bdd5e
refactor: requested changes
91dbb5a
chore: deleting a file
6de2193
chore: oops
a55dd14
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
ce09500
refactor: requested changes
b224ec6
fix: `openapi:refs` touch ups (#1103)
kanadgupta 9c037c9
test: schemes without properties
da5b0ad
Merge remote-tracking branch 'origin' into oleh/openapi-adding-comman…
9249d2a
chore: minor updates
dd02080
Revert "chore: minor updates"
4355f9c
chore: minor updates
a7d8555
chore: lint
f010a7d
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh f7e52bf
feat: processing schemas with only (beta)
8bcf655
Merge branch 'oleh/openapi-adding-command-to-solve-circularity-and-re…
2667d1b
chore: lint
1d119f2
chore: cleanup
e790dec
chore: cleanup
ae39d1e
test: complex case
6e65d9f
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 91958bd
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 09b8039
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh 4011248
Merge branch 'next' into oleh/openapi-adding-command-to-solve-circula…
olehshh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
/* 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'; | ||
import path from 'node:path'; | ||
|
||
import prompts from 'prompts'; | ||
|
||
import analyzeOas from '../../lib/analyzeOas.js'; | ||
import Command, { CommandCategories } from '../../lib/baseCommand.js'; | ||
import prepareOas from '../../lib/prepareOas.js'; | ||
import promptTerminal from '../../lib/promptWrapper.js'; | ||
import { validateFilePath } from '../../lib/validatePromptInput.js'; | ||
|
||
interface Options { | ||
out?: string; | ||
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, | ||
}, | ||
{ | ||
name: 'out', | ||
type: String, | ||
description: 'Output file path to write converted file to', | ||
}, | ||
]; | ||
} | ||
|
||
/** | ||
* 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 { preparedSpec, specPath } = await prepareOas(spec, 'openapi:refs', { convertToLatest: true }); | ||
const openApiData = JSON.parse(preparedSpec); | ||
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.'; | ||
} | ||
} | ||
|
||
prompts.override({ | ||
outputPath: opts.out, | ||
}); | ||
|
||
const promptResults = await promptTerminal([ | ||
{ | ||
type: 'text', | ||
name: 'outputPath', | ||
message: 'Enter the path to save your processed API definition to:', | ||
initial: () => { | ||
const extension = path.extname(specPath); | ||
return `${path.basename(specPath).split(extension)[0]}.openapi.json`; | ||
}, | ||
validate: value => validateFilePath(value), | ||
}, | ||
]); | ||
|
||
Command.debug(`Saving processed spec to ${promptResults.outputPath}`); | ||
fs.writeFileSync(promptResults.outputPath, JSON.stringify(openApiData, null, 2), 'utf8'); | ||
Command.debug('Processed spec saved'); | ||
|
||
return `Your API definition has been processed and saved to ${promptResults.outputPath}!`; | ||
} | ||
} | ||
|
||
export default OpenAPISolvingCircularityAndRecursiveness; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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?