Skip to content

Commit

Permalink
Fix 'Maximum call stack size exceeded' issue with OpenAPI & swagger c…
Browse files Browse the repository at this point in the history
…ircular references
  • Loading branch information
danielkenyonjonesfs committed Apr 11, 2024
1 parent a82883e commit 78a570f
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 1 deletion.
37 changes: 36 additions & 1 deletion src/processors/JsonSchemaInputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
input = JsonSchemaInputProcessor.reflectSchemaNames(
input,
{},
new Set(),
'root',
true
);
Expand Down Expand Up @@ -134,6 +135,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
input = JsonSchemaInputProcessor.reflectSchemaNames(
input,
{},
new Set(),
'root',
true
);
Expand Down Expand Up @@ -163,6 +165,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
input = JsonSchemaInputProcessor.reflectSchemaNames(
input,
{},
new Set(),
'root',
true
);
Expand Down Expand Up @@ -258,7 +261,8 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
* This reflects all the common keywords that are shared between draft-4, draft-7 and Swagger 2.0 Schema
*
* @param schema to process
* @param namesStack is a aggegator of previous used names
* @param namesStack is a aggregator of previous used names
* @param seenSchemas is a set of schema already seen and named
* @param name to infer
* @param isRoot indicates if performed schema is a root schema
*/
Expand All @@ -272,14 +276,28 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
| OpenapiV3Schema
| boolean,
namesStack: Record<string, number>,
seenSchemas: Set<
| Draft4Schema
| Draft6Schema
| Draft7Schema
| SwaggerV2Schema
| OpenapiV3Schema
>,
name?: string,
isRoot?: boolean
): any {
if (typeof schema === 'boolean') {
return schema;
}

// short-circuit circular references
if (seenSchemas.has(schema)) {
return schema;
}
seenSchemas.add(schema);

schema = { ...schema };

if (isRoot) {
namesStack[String(name)] = 0;
(schema as any)[this.MODELGEN_INFFERED_NAME] = name;
Expand All @@ -304,6 +322,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
this.reflectSchemaNames(
item,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'allOf', idx)
)
);
Expand All @@ -313,6 +332,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
this.reflectSchemaNames(
item,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'oneOf', idx)
)
);
Expand All @@ -322,6 +342,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
this.reflectSchemaNames(
item,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'anyOf', idx)
)
);
Expand All @@ -330,6 +351,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
schema.not = this.reflectSchemaNames(
schema.not,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'not')
);
}
Expand All @@ -340,6 +362,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
schema.additionalItems = this.reflectSchemaNames(
schema.additionalItems,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'additionalItem')
);
}
Expand All @@ -350,6 +373,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
schema.additionalProperties = this.reflectSchemaNames(
schema.additionalProperties,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'additionalProperty')
);
}
Expand All @@ -360,13 +384,15 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
this.reflectSchemaNames(
item,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'item', idx)
)
);
} else {
schema.items = this.reflectSchemaNames(
schema.items,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'item')
);
}
Expand All @@ -380,6 +406,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
properties[String(propertyName)] = this.reflectSchemaNames(
propertySchema,
namesStack,
seenSchemas,
this.ensureNamePattern(name, propertyName)
);
}
Expand All @@ -394,6 +421,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
dependencies[String(dependencyName)] = this.reflectSchemaNames(
dependency as any,
namesStack,
seenSchemas,
this.ensureNamePattern(name, dependencyName)
);
} else {
Expand All @@ -412,6 +440,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
this.reflectSchemaNames(
patternProperty as any,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'pattern_property', idx)
);
}
Expand All @@ -425,6 +454,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
definitions[String(definitionName)] = this.reflectSchemaNames(
definition,
namesStack,
seenSchemas,
this.ensureNamePattern(name, definitionName)
);
}
Expand All @@ -437,13 +467,15 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
schema.contains = this.reflectSchemaNames(
schema.contains,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'contain')
);
}
if (schema.propertyNames !== undefined) {
schema.propertyNames = this.reflectSchemaNames(
schema.propertyNames,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'propertyName')
);
}
Expand All @@ -453,20 +485,23 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor {
schema.if = this.reflectSchemaNames(
schema.if,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'if')
);
}
if (schema.then !== undefined) {
schema.then = this.reflectSchemaNames(
schema.then,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'then')
);
}
if (schema.else !== undefined) {
schema.else = this.reflectSchemaNames(
schema.else,
namesStack,
seenSchemas,
this.ensureNamePattern(name, 'else')
);
}
Expand Down
1 change: 1 addition & 0 deletions src/processors/OpenAPIInputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export class OpenAPIInputProcessor extends AbstractInputProcessor {
const namedSchema = JsonSchemaInputProcessor.reflectSchemaNames(
schema,
{},
new Set(),
name,
true
);
Expand Down
1 change: 1 addition & 0 deletions src/processors/SwaggerInputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export class SwaggerInputProcessor extends AbstractInputProcessor {
schema = JsonSchemaInputProcessor.reflectSchemaNames(
schema,
{},
new Set(),
name,
true
);
Expand Down
1 change: 1 addition & 0 deletions test/processors/JsonSchemaInputProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ describe('JsonSchemaInputProcessor', () => {
const expected = JsonSchemaInputProcessor.reflectSchemaNames(
schema,
{},
new Set(),
'root',
true
) as any;
Expand Down
12 changes: 12 additions & 0 deletions test/processors/OpenAPIInputProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const basicDoc = JSON.parse(
'utf8'
)
);
const circularDoc = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, './OpenAPIInputProcessor/references_circular.json'),
'utf8'
)
);
jest.mock('../../src/utils/LoggingInterface');
const processorSpy = jest.spyOn(
OpenAPIInputProcessor,
Expand Down Expand Up @@ -134,5 +140,11 @@ describe('OpenAPIInputProcessor', () => {
'test_parameters_header_path_parameter'
]);
});
test('should be able to use $ref when circular', async () => {
const processor = new OpenAPIInputProcessor();
const commonInputModel = await processor.process(circularDoc);
expect(commonInputModel).toMatchSnapshot();
expect(processorSpy.mock.calls).toMatchSnapshot();
});
});
});
54 changes: 54 additions & 0 deletions test/processors/OpenAPIInputProcessor/references_circular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"openapi": "3.0.3",
"info": {
"title": "circular api"
},
"paths": {
"/test": {
"get": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/ApiResponse"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/ApiResponse"
}
}
}
}
}
}
}
},
"components": {
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
},
"loop": {
"$ref": "#/components/ApiResponse"
}
}
}
}
}
24 changes: 24 additions & 0 deletions test/processors/SwaggerInputProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const basicDoc = JSON.parse(
'utf8'
)
);
const circularDoc = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, './SwaggerInputProcessor/references_circular.json'),
'utf8'
)
);
jest.mock('../../src/utils/LoggingInterface');
jest.spyOn(SwaggerInputProcessor, 'convertToInternalSchema');
const mockedReturnModels = [new CommonModel()];
Expand All @@ -27,6 +33,9 @@ jest.mock('../../src/interpreter/Interpreter', () => {
});

describe('SwaggerInputProcessor', () => {
beforeEach(() => {
jest.resetAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
Expand Down Expand Up @@ -77,5 +86,20 @@ describe('SwaggerInputProcessor', () => {
).mock.calls
).toMatchSnapshot();
});
test('should be able to use $ref when circular', async () => {
JsonSchemaInputProcessor.convertSchemaToMetaModel = jest
.fn()
.mockImplementation(() => {
return mockedMetaModel;
});
const processor = new SwaggerInputProcessor();
const commonInputModel = await processor.process(circularDoc);
expect(commonInputModel).toMatchSnapshot();
expect(
(
SwaggerInputProcessor.convertToInternalSchema as any as jest.SpyInstance
).mock.calls
).toMatchSnapshot();
});
});
});
40 changes: 40 additions & 0 deletions test/processors/SwaggerInputProcessor/references_circular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"swagger": "2.0",
"info": {
"title": "circular api"
},
"paths": {
"/test": {
"get": {
"responses": {
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/ApiResponse"
}
}
}
}
}
},
"definitions": {
"ApiResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"type": {
"type": "string"
},
"message": {
"type": "string"
},
"loop": {
"$ref": "#/definitions/ApiResponse"
}
}
}
}
}
Loading

0 comments on commit 78a570f

Please sign in to comment.