Skip to content

Commit 6023756

Browse files
committed
enhances example(s) removal for api requests
1 parent 62d17d8 commit 6023756

File tree

3 files changed

+189
-25
lines changed

3 files changed

+189
-25
lines changed

src/middlewares/parsers/schema.preprocessor.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,24 @@ class Node<T, P> {
4444
}
4545
type SchemaObjectNode = Node<SchemaObject, SchemaObject>;
4646

47-
function isParameterObject(node: ParameterObject | ReferenceObject): node is ParameterObject {
48-
return !((node as ReferenceObject).$ref);
47+
function isParameterObject(
48+
node: ParameterObject | ReferenceObject,
49+
): node is ParameterObject {
50+
return !(node as ReferenceObject).$ref;
4951
}
50-
function isReferenceObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ReferenceObject {
51-
return !!((node as ReferenceObject).$ref);
52+
function isReferenceObject(
53+
node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject,
54+
): node is ReferenceObject {
55+
return !!(node as ReferenceObject).$ref;
5256
}
53-
function isArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ArraySchemaObject {
54-
return !!((node as ArraySchemaObject).items);
57+
function isArraySchemaObject(
58+
node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject,
59+
): node is ArraySchemaObject {
60+
return !!(node as ArraySchemaObject).items;
5561
}
56-
function isNonArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is NonArraySchemaObject {
62+
function isNonArraySchemaObject(
63+
node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject,
64+
): node is NonArraySchemaObject {
5765
return !isArraySchemaObject(node) && !isReferenceObject(node);
5866
}
5967

@@ -119,6 +127,10 @@ export class SchemaPreprocessor {
119127
// Now that we've processed paths, clone a response spec if we are validating responses
120128
this.apiDocRes = !!this.responseOpts ? cloneDeep(this.apiDoc) : null;
121129

130+
if (this.apiDoc.components) {
131+
this.removeExamples(this.apiDoc.components);
132+
}
133+
122134
const schemaNodes = {
123135
schemas: componentSchemas,
124136
requestBodies: r?.requestBodies,
@@ -163,7 +175,7 @@ export class SchemaPreprocessor {
163175
// Since OpenAPI 3.1, paths can be a #ref to reusable path items
164176
// The following line mutates the paths item to dereference the reference, so that we can process as a POJO, as we would if it wasn't a reference
165177
this.apiDoc.paths[p] = pathItem;
166-
178+
167179
for (const method of Object.keys(pathItem)) {
168180
if (httpMethods.has(method)) {
169181
const operation = <OpenAPIV3.OperationObject>pathItem[method];
@@ -173,7 +185,8 @@ export class SchemaPreprocessor {
173185
const node = new Root<OpenAPIV3.OperationObject>(operation, path);
174186
const requestBodies = this.extractRequestBodySchemaNodes(node);
175187
const responseBodies = this.extractResponseSchemaNodes(node);
176-
const requestParameters = this.extractRequestParameterSchemaNodes(node);
188+
const requestParameters =
189+
this.extractRequestParameterSchemaNodes(node);
177190

178191
requestBodySchemas.push(...requestBodies);
179192
responseSchemas.push(...responseBodies);
@@ -246,7 +259,10 @@ export class SchemaPreprocessor {
246259
recurse(node, child, opts);
247260
});
248261
} else if (schema.additionalProperties) {
249-
const child = new Node(node, schema.additionalProperties, [...node.path, 'additionalProperties']);
262+
const child = new Node(node, schema.additionalProperties, [
263+
...node.path,
264+
'additionalProperties',
265+
]);
250266
recurse(node, child, opts);
251267
}
252268
};
@@ -302,7 +318,7 @@ export class SchemaPreprocessor {
302318
this.handleReadonly(pschema, nschema, options);
303319
this.handleWriteonly(pschema, nschema, options);
304320
this.processDiscriminator(pschema, nschema, options);
305-
this.removeExamples(pschema, nschema, options)
321+
this.removeSchemaExamples(pschema, nschema, options);
306322
}
307323
}
308324
}
@@ -458,21 +474,23 @@ export class SchemaPreprocessor {
458474
}
459475
}
460476

461-
private removeExamples(
477+
private removeSchemaExamples(
462478
parent: OpenAPIV3.SchemaObject,
463479
schema: OpenAPIV3.SchemaObject,
464480
opts,
465481
) {
466-
// Remove example and examples from all schema types, not just objects
467-
if (schema?.example) {
468-
delete schema.example;
469-
}
470-
if (schema?.examples) {
471-
delete schema.examples;
472-
}
482+
this.removeExamples(parent);
483+
this.removeExamples(schema);
484+
}
485+
486+
private removeExamples(
487+
object: OpenAPIV3.SchemaObject | OpenAPIV3.MediaTypeObject,
488+
): void {
489+
delete object?.example;
490+
delete object?.examples;
473491
}
474492

475-
private handleReadonly(
493+
private handleReadonly(
476494
parent: OpenAPIV3.SchemaObject,
477495
schema: OpenAPIV3.SchemaObject,
478496
opts,
@@ -536,6 +554,10 @@ private handleReadonly(
536554
mediaTypeObject.schema,
537555
);
538556
op.requestBody.content[type].schema = mediaTypeSchema;
557+
558+
// TODO replace with visitor
559+
this.removeExamples(op.requestBody.content[type]);
560+
539561
const path = [...node.path, 'requestBody', 'content', type, 'schema'];
540562
result.push(new Root(mediaTypeSchema, path));
541563
}
@@ -575,6 +597,10 @@ private handleReadonly(
575597
type,
576598
'schema',
577599
];
600+
601+
// TODO replace with visitor
602+
this.removeExamples(rschema.content[type]);
603+
578604
schemas.push(new Root(schema, path));
579605
}
580606
}
@@ -586,21 +612,25 @@ private handleReadonly(
586612
private extractRequestParameterSchemaNodes(
587613
operationNode: Root<OperationObject>,
588614
): Root<SchemaObject>[] {
589-
590615
return (operationNode.schema.parameters ?? []).flatMap((node) => {
591616
const parameterObject = isParameterObject(node) ? node : undefined;
617+
618+
// TODO replace with visitor
619+
// TODO This does not handle JSON query parameters
620+
this.removeExamples(parameterObject);
621+
592622
if (!parameterObject?.schema) return [];
593623

594-
const schema = isNonArraySchemaObject(parameterObject.schema) ?
595-
parameterObject.schema :
596-
undefined;
624+
const schema = isNonArraySchemaObject(parameterObject.schema)
625+
? parameterObject.schema
626+
: undefined;
597627
if (!schema) return [];
598628

599629
return new Root(schema, [
600630
...operationNode.path,
601631
'parameters',
602632
parameterObject.name,
603-
parameterObject.in
633+
parameterObject.in,
604634
]);
605635
});
606636
}

test/example.removal.spec.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { expect } from 'chai';
2+
import * as cloneDeep from 'lodash.clonedeep';
3+
import * as path from 'path';
4+
import { AjvOptions } from '../src/framework/ajv/options';
5+
import { OpenApiSpecLoader } from '../src/framework/openapi.spec.loader';
6+
import { NormalizedOpenApiValidatorOpts, OpenAPIV3 } from '../src/framework/types';
7+
import { SchemaPreprocessor } from '../src/middlewares/parsers/schema.preprocessor';
8+
9+
describe('SchemaPreprocessor example removal', () => {
10+
let originalApiDoc: any;
11+
let ajvOptions: AjvOptions;
12+
13+
14+
before(async () => {
15+
const apiSpecPath = path.join('test', 'resources', 'openapi.yaml');
16+
const options: NormalizedOpenApiValidatorOpts = {
17+
apiSpec: apiSpecPath,
18+
validateApiSpec: true,
19+
validateResponses: false,
20+
validateRequests: {},
21+
validateSecurity: false,
22+
fileUploader: true,
23+
$refParser: {
24+
mode: 'bundle',
25+
},
26+
operationHandlers: false,
27+
formats: {},
28+
validateFormats: true,
29+
// unknownFormats?: never;
30+
serDes: [],
31+
};
32+
// const oav = new OpenApiValidator(options);
33+
const spec = await new OpenApiSpecLoader({
34+
apiDoc: cloneDeep(options.apiSpec),
35+
validateApiSpec: options.validateApiSpec,
36+
$refParser: options.$refParser,
37+
}).load();
38+
39+
ajvOptions = new AjvOptions(options);
40+
originalApiDoc = spec.apiDoc
41+
});
42+
43+
it('should remove example properties from the GET /pets API request document', () => {
44+
// Create preprocessor instance
45+
const preprocessor = new SchemaPreprocessor(
46+
originalApiDoc,
47+
ajvOptions.preprocessor,
48+
ajvOptions.response,
49+
);
50+
51+
// Run the preProcess method
52+
const result = preprocessor.preProcess();
53+
54+
// Check that examples were removed from parameters
55+
const paths = result.apiDoc.paths;
56+
const petsParams = paths?.['/pets']?.get
57+
?.parameters as OpenAPIV3.ParameterObject[];
58+
const limitParam = petsParams.find(
59+
(p) => (p as OpenAPIV3.ParameterObject).name === 'limit',
60+
)!;
61+
expect(limitParam.schema).to.not.have.property('example');
62+
expect(limitParam).to.not.have.property('example');
63+
64+
// TODO: Check that examples were removed from request body content
65+
// TODO: Fails because JSON query parameter example removal is not yet supported
66+
// const testJsonParam = petsParams.find((p) => p.name === 'testJson');
67+
// expect(testJsonParam?.content!['application/json']).to.not.have.property(
68+
// 'example',
69+
// );
70+
71+
// Check that examples were removed from response content
72+
const petsResponse = paths?.['/pets']?.get?.responses?.[
73+
'200'
74+
]! as OpenAPIV3.ResponseObject;
75+
expect(petsResponse.content!['application/json']).to.not.have.property(
76+
'example',
77+
);
78+
expect(
79+
petsResponse.content!['application/json'].schema,
80+
).to.not.have.property('example');
81+
});
82+
83+
84+
it('should remove example properties from the GET /pets API response document', () => {
85+
// Create preprocessor instance
86+
const preprocessor = new SchemaPreprocessor(
87+
originalApiDoc,
88+
ajvOptions.preprocessor,
89+
ajvOptions.response,
90+
);
91+
92+
// Run the preProcess method
93+
const result = preprocessor.preProcess();
94+
95+
const petsGet = result.apiDoc.paths!['/pets'].get!;
96+
const petsGetRes = petsGet?.responses!
97+
const entries = Object.entries(petsGetRes);
98+
entries.forEach(([key, value]) => {
99+
const contentMediaTypes = (value! as OpenAPIV3.ResponseObject).content!;
100+
const content = contentMediaTypes['application/json'];
101+
expect(content).to.not.have.property('example');
102+
expect(content.schema).to.not.have.property('example');
103+
});
104+
});
105+
106+
it('should remove example properties components:', () => {
107+
const preprocessor = new SchemaPreprocessor(
108+
originalApiDoc,
109+
ajvOptions.preprocessor,
110+
ajvOptions.response,
111+
);
112+
113+
const result = preprocessor.preProcess();
114+
115+
const components = result.apiDoc.components
116+
expect(components).to.not.have.property('examples');
117+
});
118+
});

test/resources/openapi.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ paths:
5959
type: integer
6060
format: int32
6161
minimum: 5
62+
example: 8
63+
example: 8
6264
- name: test
6365
in: query
6466
description: maximum number of results to return
@@ -81,6 +83,7 @@ paths:
8183
enum:
8284
- bar
8385
- baz
86+
example: {}
8487
- name: testArrayNoExplode
8588
in: query
8689
description: Array in query param
@@ -123,14 +126,21 @@ paths:
123126
application/json:
124127
schema:
125128
type: array
129+
example:
130+
- {}
126131
items:
127132
$ref: '#/components/schemas/Pet'
133+
example: {}
128134
default:
129135
description: unexpected error
130136
content:
131137
application/json:
132138
schema:
133139
$ref: '#/components/schemas/Error'
140+
examples:
141+
jsonObject:
142+
summary: A sample object
143+
externalValue: "http://example.com/examples/object-example.json"
134144
post:
135145
description: Creates a new pet in the store. Duplicates are allowed
136146
operationId: addPet
@@ -397,6 +407,12 @@ paths:
397407
$ref: '#/components/schemas/Error'
398408

399409
components:
410+
examples:
411+
objectExample:
412+
value:
413+
id: 1
414+
name: new object
415+
summary: A sample object
400416
parameters:
401417
$ref: 'xt.openapi.parameters.yaml#/parameters'
402418

0 commit comments

Comments
 (0)