Skip to content
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

Opening for extension #189

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions dist/Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion dist/cli.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/cli.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

96 changes: 67 additions & 29 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ module.exports = {
'default': validateExamples,
validateFile,
validateExample,
validateExamplesByMap
validateExamplesByMap,
getValidatorFactory
};

// IMPLEMENTATION DETAILS
Expand Down Expand Up @@ -99,17 +100,24 @@ module.exports = {
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
* @param {Function} [validatorFactory] Validator factory provider
* @returns {ValidationResponse}
*/
async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats, allPropertiesRequired } = {}) {
async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats, allPropertiesRequired,
specPostprocessor = (spec) => spec,
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
} = {}) {
const impl = Determiner.getImplementation(openapiSpec);
openapiSpec = await refParser.dereference(openapiSpec);
openapiSpec = impl.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
let pathsExamples = impl.getJsonPathsToExamples()
.reduce((res, pathToExamples) => {
return res.concat(_pathToPointer(pathToExamples, openapiSpec));
}, []);
return _validateExamplesPaths({ impl }, pathsExamples, openapiSpec, { ignoreFormats });
const createValidator = validatorFactory(openapiSpec, ignoreFormats);
return _validateExamplesPaths({ impl, createValidator }, pathsExamples, openapiSpec);
}

/**
Expand All @@ -121,16 +129,24 @@ async function validateExamples(openapiSpec, { noAdditionalProperties, ignoreFor
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
* @param {Function} [validatorFactory] Validator factory provider
* @returns {ValidationResponse}
*/
async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, allPropertiesRequired } = {}) {
async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, allPropertiesRequired,
specPostprocessor = (spec) => spec,
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
} = {}) {
let openapiSpec = null;
try {
openapiSpec = await _parseSpec(filePath);
} catch (err) {
return createValidationResponse({ errors: [ApplicationError.create(err)] });
}
return validateExamples(openapiSpec, { noAdditionalProperties, ignoreFormats, allPropertiesRequired });
return validateExamples(openapiSpec, {
noAdditionalProperties, ignoreFormats, allPropertiesRequired,
specPostprocessor, validatorFactory
});
}

/**
Expand All @@ -147,11 +163,15 @@ async function validateFile(filePath, { noAdditionalProperties, ignoreFormats, a
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
* @param {Function} [validatorFactory] Validator factory provider
* @returns {ValidationResponse}
*/
async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
{ cwdToMappingFile, noAdditionalProperties, ignoreFormats, allPropertiesRequired } = {}
) {
{ cwdToMappingFile, noAdditionalProperties, ignoreFormats, allPropertiesRequired,
specPostprocessor = (spec) => spec,
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
} = {}) {
let matchingFilePathsMapping = 0;
const filePathsMaps = glob.sync(
globMapExternalExamples,
Expand All @@ -169,6 +189,7 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
openapiSpec = await _parseSpec(filePathSchema);
openapiSpec = Determiner.getImplementation(openapiSpec)
.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
} catch (err) {
responses.push(createValidationResponse({ errors: [ApplicationError.create(err)] }));
continue;
Expand All @@ -179,13 +200,11 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
responses.push(
_validate(
statistics => {
return _handleExamplesByMapValidation(
openapiSpec, mapExternalExamples, statistics, {
cwdToMappingFile,
dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples),
ignoreFormats
}
).map(
return _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statistics, {
cwdToMappingFile,
dirPathMapExternalExamples: path.dirname(filePathMapExternalExamples),
ignoreFormats, validatorFactory
}).map(
(/** @type ApplicationError */ error) => Object.assign(error, {
mapFilePath: path.normalize(filePathMapExternalExamples)
})
Expand Down Expand Up @@ -216,12 +235,16 @@ async function validateExamplesByMap(filePathSchema, globMapExternalExamples,
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @param {Function} [specPostprocessor] Provides implementation of spec postprocessor
* @param {Function} [validatorFactory] Validator factory provider
* @returns {ValidationResponse}
*/
async function validateExample(filePathSchema, pathSchema, filePathExample, {
noAdditionalProperties,
ignoreFormats,
allPropertiesRequired
allPropertiesRequired,
specPostprocessor = (spec) => spec,
validatorFactory = (spec, ignoreFormats) => _initValidatorFactory(spec, { ignoreFormats })
} = {}) {
let example = null,
schema = null,
Expand All @@ -231,13 +254,15 @@ async function validateExample(filePathSchema, pathSchema, filePathExample, {
openapiSpec = await _parseSpec(filePathSchema);
openapiSpec = Determiner.getImplementation(openapiSpec)
.prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired });
openapiSpec = _postprocess(openapiSpec, specPostprocessor);
schema = _extractSchema(_getSchmaPointer(pathSchema, openapiSpec), openapiSpec);
} catch (err) {
return createValidationResponse({ errors: [ApplicationError.create(err)] });
}
const createValidator = validatorFactory(openapiSpec, ignoreFormats);
return _validate(
statistics => _validateExample({
createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
createValidator,
schema,
example,
statistics,
Expand Down Expand Up @@ -312,11 +337,12 @@ function _validate(validationHandler) {
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @param {Function} [validatorFactory] Validator factory provider
* @returns {Array.<ApplicationError>}
* @private
*/
function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statistics,
{ cwdToMappingFile = false, dirPathMapExternalExamples, ignoreFormats }
{ cwdToMappingFile = false, dirPathMapExternalExamples, ignoreFormats, validatorFactory }
) {
return flatMap(Object.entries(mapExternalExamples), ([pathSchema, filePathsExample]) => {
let schema = null;
Expand All @@ -326,6 +352,8 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
// If the schema can't be found, don't even attempt to process the examples
return ApplicationError.create(err);
}
const createValidator = validatorFactory(openapiSpec, ignoreFormats);

return flatMap(
flatten([filePathsExample]),
filePathExample => {
Expand All @@ -352,7 +380,7 @@ function _handleExamplesByMapValidation(openapiSpec, mapExternalExamples, statis
return [ApplicationError.create(err)];
}
return flatMap(examples, example => _validateExample({
createValidator: _initValidatorFactory(openapiSpec, { ignoreFormats }),
createValidator,
schema,
example: example.content,
statistics,
Expand Down Expand Up @@ -435,23 +463,19 @@ function _getSchmaPointer(pathSchema, openapiSpec) {
/**
* Validates examples at the given paths in the OpenAPI-spec.
* @param {Object} impl Spec-dependant validator
* @param {Function} createValidator Validator factory
* @param {Array.<String>} pathsExamples JSON-paths to examples
* @param {Object} openapiSpec OpenAPI-spec
* @param {Array.<string>} [ignoreFormats] List of datatype formats that shall be ignored (to prevent
* "unsupported format" errors). If an Array with only one string is
* provided where the formats are separated with `\n`, the entries
* will be expanded to a new array containing all entries.
* @returns {ValidationResponse}
* @private
*/
function _validateExamplesPaths({ impl }, pathsExamples, openapiSpec, { ignoreFormats }) {
function _validateExamplesPaths({ impl, createValidator }, pathsExamples, openapiSpec) {
const statistics = _initStatistics(),
validationResult = {
valid: true,
statistics,
errors: []
},
createValidator = _initValidatorFactory(openapiSpec, { ignoreFormats });
};
let validationMap;
try {
// Create mapping between JSON-schemas and examples
Expand Down Expand Up @@ -590,18 +614,32 @@ function _validateExample({ createValidator, schema, example, statistics, filePa
* @private
*/
function _initValidatorFactory(specSchema, { ignoreFormats }) {
const formats = ignoreFormats && ignoreFormats.reduce((result, entry) => {
result[entry] = () => true;
return result;
}, {});
return getValidatorFactory(specSchema, {
schemaId: 'auto',
discriminator: true,
strict: false,
allErrors: true,
formats: ignoreFormats && ignoreFormats.reduce((result, entry) => {
result[entry] = () => true;
return result;
}, {})
formats
});
}

/***
* Run spec postprocess if defined.
* @returns modified OAS spec or the original one in case of errors
*/
function _postprocess(oasSpec, specPostprocessor) {
let result = oasSpec;
if (typeof specPostprocessor === 'function') {
result = specPostprocessor(oasSpec);
if (!result) { throw new Error('Postprocessor does not return processed document'); }
}
return result;
}

/**
* Extracts the schema in the OpenAPI-spec at the given JSON-pointer.
* @param {string} schemaPointer JSON-pointer to the schema
Expand Down
9 changes: 5 additions & 4 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ module.exports = {
* Get a factory-function to create a prepared validator-instance
* @param {Object} specSchema OpenAPI-spec of which potential local references will be extracted
* @param {Object} [options] Options for the validator
* @param {Function} [provider] Ajv provider
* @returns {function(): (ajv | ajv.Ajv)}
*/
function getValidatorFactory(specSchema, options) {
function getValidatorFactory(specSchema, options, { provider = (opt) => new Ajv(opt) } = {}) {
const preparedSpecSchema = _createReferenceSchema(specSchema);
return () => {
const validator = new Ajv(options);
const validator = provider(options);
addFormats(validator);

validator.addSchema(preparedSpecSchema);
Expand All @@ -49,7 +50,7 @@ function compileValidate(validator, responseSchema) {
try {
result = validator.compile(preparedResponseSchema);
} catch (e) {
result = () => {};
result = () => { };
result.errors = [e];
}
return result;
Expand Down Expand Up @@ -79,7 +80,7 @@ function _replaceRefsToPreparedSpecSchema(schema) {
json: schema,
callback(value, type, payload) {
if (!value.startsWith('#')) { return; }
payload.parent[payload.parentProperty] = `${ ID__SPEC_SCHEMA }${ value }`;
payload.parent[payload.parentProperty] = `${ID__SPEC_SCHEMA}${value}`;
}
});
}
Expand Down
89 changes: 89 additions & 0 deletions test/data/v3/custom-postprocessing/readOnly.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"license": {
"name": "MIT"
}
},
"tags": [
{
"name": "foo",
"description": "Everything about your foo"
}
],
"servers": [
{
"url": "http://petstore.swagger.io/v1"
}
],
"paths": {
"/foo": {
"post": {
"summary": "Add foo",
"operationId": "addFoo",
"description": "Create Foo",
"tags": [
"foo"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Foo"
},
"examples": {
"invalid1": {
"value": {
"bar": "aaa",
"baz": 0
}
}
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Foo"
},
"examples": {
"valid": {
"description": "here it should be correct",
"value": {
"bar": "aaa",
"baz": 0
}
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Foo": {
"type": "object",
"description": "Simple Foo",
"properties": {
"bar": {
"type": "string"
},
"baz": {
"type": "integer",
"readOnly": true
}
},
"additionalProperties": false
}
}
}
}
Loading