Skip to content

Commit e634a0e

Browse files
committed
enabling extensions for all public apis
1 parent cf00cad commit e634a0e

File tree

5 files changed

+340
-28
lines changed

5 files changed

+340
-28
lines changed

src/index.js

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,18 @@ module.exports = {
105105
*/
106106
async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats, allPropertiesRequired,
107107
specPostprocessor = (spec) => spec,
108-
validatorFactory = (spec, { ignoreFormats }) => _initValidatorFactory(spec, { ignoreFormats })
108+
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
109109
} = {}) {
110110
const impl = Determiner.getImplementation(openapiSpec);
111111
openapiSpec = await refParser.dereference(openapiSpec);
112112
openapiSpec = impl.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
113-
if (typeof specPostprocessor === 'function') {
114-
openapiSpec = specPostprocessor(openapiSpec);
115-
}
113+
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
116114
let pathsExamples = impl.getJsonPathsToExamples()
117115
.reduce((res, pathToExamples) => {
118116
return res.concat(_extractExamplePaths(openapiSpec, pathToExamples));
119117
}, [])
120118
.map(impl.escapeExampleName);
121-
const createValidator = validatorFactory(openapiSpec, { ignoreFormats });
119+
const createValidator = validatorFactory(openapiSpec, ignoreFormats);
122120
return _validateExamplesPaths({ impl, createValidator }, pathsExamples, openapiSpec);
123121
}
124122

@@ -131,16 +129,24 @@ async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFor
131129
* "unsupported format" errors). If an Array with only one string is
132130
* provided where the formats are separated with `\n`, the entries
133131
* will be expanded to a new array containing all entries.
132+
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
133+
* @param {Function} [validatorFactory] Validator factory provider
134134
* @returns {ValidationResponse}
135135
*/
136-
async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, allPropertiesRequired } = {}) {
136+
async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, allPropertiesRequired,
137+
specPostprocessor = (spec) => spec,
138+
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
139+
} = {}) {
137140
let openapiSpec = null;
138141
try {
139142
openapiSpec = await _parseSpec(filePath);
140143
} catch (err) {
141144
return createValidationResponse({ errors: [ApplicationError.create(err)] });
142145
}
143-
return validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats, allPropertiesRequired });
146+
return validateExamples(openapiSpec, {
147+
noAdditionalProperties, ignoreFormats, allPropertiesRequired,
148+
specPostprocessor, validatorFactory
149+
});
144150
}
145151

146152
/**
@@ -157,11 +163,15 @@ async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, a
157163
* "unsupported format" errors). If an Array with only one string is
158164
* provided where the formats are separated with `\n`, the entries
159165
* will be expanded to a new array containing all entries.
166+
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
167+
* @param {Function} [validatorFactory] Validator factory provider
160168
* @returns {ValidationResponse}
161169
*/
162170
async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
163-
{ cwdToMappingFile, noAdditionalProperties, ignoreFormats, allPropertiesRequired } = {}
164-
) {
171+
{ cwdToMappingFile, noAdditionalProperties, ignoreFormats, allPropertiesRequired,
172+
specPostprocessor = (spec) => spec,
173+
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
174+
} = {}) {
165175
let matchingFilePathsMapping = 0;
166176
const filePathsMaps = glob.sync(
167177
globMapExternalExamples,
@@ -179,6 +189,7 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
179189
openapiSpec = await _parseSpec(filePathSchema);
180190
openapiSpec = Determiner.getImplementation(openapiSpec)
181191
.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
192+
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
182193
} catch (err) {
183194
responses.push(createValidationResponse({ errors: [ApplicationError.create(err)] }));
184195
continue;
@@ -189,13 +200,11 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
189200
responses.push(
190201
_validate(
191202
statistics => {
192-
return _handleExamplesByMapValidation(
193-
openapiSpec, mapExternalExamples, statistics, {
194-
cwdToMappingFile,
195-
dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples),
196-
ignoreFormats
197-
}
198-
).map(
203+
return _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statistics, {
204+
cwdToMappingFile,
205+
dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples),
206+
ignoreFormats, validatorFactory
207+
}).map(
199208
(/** @type ApplicationError */ error) => Object.assign(error, {
200209
mapFilePath: path.normalize(filePathMapExternalExamples)
201210
})
@@ -226,12 +235,16 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
226235
* "unsupported format" errors). If an Array with only one string is
227236
* provided where the formats are separated with `\n`, the entries
228237
* will be expanded to a new array containing all entries.
238+
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
239+
* @param {Function} [validatorFactory] Validator factory provider
229240
* @returns {ValidationResponse}
230241
*/
231242
async function validateExample(filePathSchema, pathSchema, filePathExample, {
232243
noAdditionalProperties,
233244
ignoreFormats,
234-
allPropertiesRequired
245+
allPropertiesRequired,
246+
specPostprocessor = (spec) => spec,
247+
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
235248
} = {}) {
236249
let example = null,
237250
schema = null,
@@ -241,13 +254,15 @@ async function validateExample(filePathSchema, pathSchema, filePathExample, {
241254
openapiSpec = await _parseSpec(filePathSchema);
242255
openapiSpec = Determiner.getImplementation(openapiSpec)
243256
.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
257+
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
244258
schema = _extractSchema(pathSchema, openapiSpec);
245259
} catch (err) {
246260
return createValidationResponse({ errors: [ApplicationError.create(err)] });
247261
}
262+
const createValidator = validatorFactory(openapiSpec, ignoreFormats);
248263
return _validate(
249264
statistics => _validateExample({
250-
createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
265+
createValidator,
251266
schema,
252267
example,
253268
statistics,
@@ -322,11 +337,12 @@ function _validate(validationHandler) {
322337
* "unsupported format" errors). If an Array with only one string is
323338
* provided where the formats are separated with `\n`, the entries
324339
* will be expanded to a new array containing all entries.
340+
* @param {Function} [validatorFactory] Validator factory provider
325341
* @returns {Array.<ApplicationError>}
326342
* @private
327343
*/
328344
function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statistics,
329-
{ cwdToMappingFile = false, dirPathMapExternalExamples, ignoreFormats }
345+
{ cwdToMappingFile = false, dirPathMapExternalExamples, ignoreFormats, validatorFactory }
330346
) {
331347
return flatMap(Object.entries(mapExternalExamples), ([pathSchema, filePathsExample]) => {
332348
let schema = null;
@@ -336,6 +352,8 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
336352
// If the schema can't be found, don't even attempt to process the examples
337353
return ApplicationError.create(err);
338354
}
355+
const createValidator = validatorFactory(openapiSpec, ignoreFormats);
356+
339357
return flatMap(
340358
flatten([filePathsExample]),
341359
filePathExample => {
@@ -362,7 +380,7 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
362380
return [ApplicationError.create(err)];
363381
}
364382
return flatMap(examples, example => _validateExample({
365-
createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
383+
createValidator,
366384
schema,
367385
example: example.content,
368386
statistics,
@@ -429,7 +447,7 @@ function _extractExamplePaths(openapiSpec, jsonPathToExamples) {
429447
* @returns {ValidationResponse}
430448
* @private
431449
*/
432-
function _validateExamplesPaths({ impl, createValidator }, pathsExamples, openapiSpec) {
450+
function _validateExamplesPaths({ impl, createValidator }, pathsExamples, openapiSpec) {
433451
const statistics = _initStatistics(),
434452
validationResult = {
435453
valid: true,
@@ -581,18 +599,32 @@ function _validateExample({ createValidator, schema, example, statistics, filePa
581599
* @private
582600
*/
583601
function _initValidatorFactory(specSchema, { ignoreFormats }) {
602+
const formats = ignoreFormats && ignoreFormats.reduce((result, entry) => {
603+
result[entry] = () => true;
604+
return result;
605+
}, {});
584606
return getValidatorFactory(specSchema, {
585607
schemaId: 'auto',
586608
discriminator: true,
587609
strict: false,
588610
allErrors: true,
589-
formats: ignoreFormats && ignoreFormats.reduce((result, entry) => {
590-
result[entry] = () => true;
591-
return result;
592-
}, {})
611+
formats
593612
});
594613
}
595614

615+
/***
616+
* Run spec postprocess if defined.
617+
* @returns modified OAS spec or the original one in case of errors
618+
*/
619+
function _postprocess(oasSpec, specPostprocessor) {
620+
let result = oasSpec;
621+
if (typeof specPostprocessor === 'function') {
622+
result = specPostprocessor(oasSpec);
623+
if (!result) { throw new Error('Postprocessor has to be specified'); }
624+
}
625+
return result;
626+
}
627+
596628
/**
597629
* Extracts the schema in the OpenAPI-spec at the given JSON-path.
598630
* @param {string} pathSchema JSON-path to the schema

src/validator.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ module.exports = {
2121
* Get a factory-function to create a prepared validator-instance
2222
* @param {Object} specSchema OpenAPI-spec of which potential local references will be extracted
2323
* @param {Object} [options] Options for the validator
24+
* @param {Function} [provider] Ajv provider
2425
* @returns {function(): (ajv | ajv.Ajv)}
2526
*/
26-
function getValidatorFactory(specSchema, options, { provider = (opt) => new Ajv(opt) }) {
27+
function getValidatorFactory(specSchema, options, { provider = (opt) => new Ajv(opt) } = {}) {
2728
const preparedSpecSchema = _createReferenceSchema(specSchema);
2829
return () => {
2930
const validator = provider(options);
@@ -49,7 +50,7 @@ function compileValidate(validator, responseSchema) {
4950
try {
5051
result = validator.compile(preparedResponseSchema);
5152
} catch (e) {
52-
result = () => {};
53+
result = () => { };
5354
result.errors = [e];
5455
}
5556
return result;
@@ -79,7 +80,7 @@ function _replaceRefsToPreparedSpecSchema(schema) {
7980
json: schema,
8081
callback(value, type, payload) {
8182
if (!value.startsWith('#')) { return; }
82-
payload.parent[payload.parentProperty] = `${ ID__SPEC_SCHEMA }${ value }`;
83+
payload.parent[payload.parentProperty] = `${ID__SPEC_SCHEMA}${value}`;
8384
}
8485
});
8586
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"version": "1.0.0",
5+
"title": "Swagger Petstore",
6+
"license": {
7+
"name": "MIT"
8+
}
9+
},
10+
"tags": [
11+
{
12+
"name": "foo",
13+
"description": "Everything about your foo"
14+
}
15+
],
16+
"servers": [
17+
{
18+
"url": "http://petstore.swagger.io/v1"
19+
}
20+
],
21+
"paths": {
22+
"/foo": {
23+
"post": {
24+
"summary": "Add foo",
25+
"operationId": "addFoo",
26+
"description": "Create Foo",
27+
"tags": [
28+
"foo"
29+
],
30+
"requestBody": {
31+
"content": {
32+
"application/json": {
33+
"schema": {
34+
"$ref": "#/components/schemas/Foo"
35+
},
36+
"examples": {
37+
"invalid1": {
38+
"value": {
39+
"bar": "aaa",
40+
"baz": 0
41+
}
42+
},
43+
"invalid2": {
44+
"value": {
45+
"bar": "aaa",
46+
"xxx": "xxx"
47+
}
48+
}
49+
}
50+
}
51+
}
52+
},
53+
"responses": {
54+
"201": {
55+
"description": "Created",
56+
"content": {
57+
"application/json": {
58+
"schema": {
59+
"$ref": "#/components/schemas/Foo"
60+
},
61+
"examples": {
62+
"valid": {
63+
"description": "here it should be correct",
64+
"value": {
65+
"bar": "aaa",
66+
"baz": 0
67+
}
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
},
77+
"components": {
78+
"schemas": {
79+
"Foo": {
80+
"type": "object",
81+
"description": "Simple Foo",
82+
"properties": {
83+
"bar": {
84+
"type": "string"
85+
},
86+
"baz": {
87+
"type": "integer",
88+
"readOnly": true
89+
}
90+
},
91+
"additionalProperties": false
92+
}
93+
}
94+
}
95+
}

test/specs/extensions.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const
2+
{ 'default': validateExamples } = require('../../src'),
3+
{ loadTestData } = require('../util/setup-tests');
4+
// { applyCallbackToAllObjectModels } = require('../../src/impl/service/common');
5+
const { JSONPath: jp } = require('jsonpath-plus');
6+
7+
function removeReadOnlyFromPostModels(oasSpec) {
8+
// please note that $RefParser.dereference used internally is reusing objectss
9+
oasSpec = JSON.parse(JSON.stringify(oasSpec));
10+
const path = '$..requestBody..schema..*[?(@.readOnly)]';
11+
jp({ path, json: oasSpec, resultType: 'parentProperty', callback: (val, _, obj) => {
12+
const parent = obj.parent;
13+
if (parent[val].readOnly === true) {
14+
delete parent[val];
15+
}
16+
} });
17+
return oasSpec;
18+
}
19+
20+
describe('OAS postprocessor', function() {
21+
describe('validateExamples', function() {
22+
describe('API version 3', function() {
23+
it('should find errors in all request examples', async function() {
24+
const result = await validateExamples(loadTestData('v3/custom-postprocessing/readOnly'),
25+
{ specPostprocessor: removeReadOnlyFromPostModels });
26+
result.valid.should.equal(false);
27+
result.errors.length.should.equal(2);
28+
result.statistics.examplesTotal.should.equal(3);
29+
});
30+
});
31+
});
32+
});

0 commit comments

Comments
 (0)