Added back the semantic validation for required array items

This commit is contained in:
Jeremy Whitlock 2015-06-16 17:48:29 -06:00
Родитель 58aea79902
Коммит bc4615e529
10 изменённых файлов: 1771 добавлений и 708 удалений

215
browser/swagger-core-api-min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

453
browser/swagger-core-api-standalone-min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -43,11 +43,36 @@ npm install swagger-core-api --save
The swagger-core-api project's API documentation can be found here: https://github.com/apigee-127/swagger-core-api/blob/master/docs/API.md
## Swagger Versions
swagger-core-api uses [The Factory Method Pattern][factory-method-pattern] to create the `SwaggerApi` object you see
documented in the API documentation above. The core API is concrete but how each version of Swagger generates the
`SwaggerApi` object and its business logic is Swagger version dependent. That being said, below are the supported
versions of Swagger and their documentation:
* [2.0][version-2.0-documentation]
## Swagger Validation
Swagger validation can be broken up into three phases:
* `Structural Validation`: This is where we use the Swagger provided JSON Schema linked above and use a JSON Schema
validator to validate the structure of your Swagger document
* `Semantic Validation`: This is where to do validation above and beyond the general structure of your Swagger document.
The reason for this is that there are some situations that cannot be described using JSON Schema. There are also
situations where the existing JSON Schema for Swagger is broken or not as strict as it could be.
* `Custom Validation`: This is user-configurable validation that typically fall into stylistic checks.
`Structural Validation` is the only type of validation that occurs in a special way. If structural validation fails,
no other validation will occur. But once the structural validation happens, `Semantic Validation` and
`Custom Validation` will happen.
## Dependencies
Below is the list of projects being used by swagger-core-api and the purpose(s) they are used for:
* [debug][debug]: Used for producing useful debugging information
* [js-base64][js-base64]: Used for generating mock/sample data for the `byte` format
* [js-yaml][js-yaml]: Used for parsing YAML Swagger files
* [json-refs][json-refs]: Used for dereferncing JSON References in Swagger files
* [json-schema-faker][json-schema-faker]: Used for generating mock/sample values from JSON Schemas
@ -61,6 +86,8 @@ they did just in case they wanted to use these libraries.)_
[bower]: http://bower.io/
[debug]: https://www.npmjs.com/package/debug
[factory-method-pattern]: https://en.wikipedia.org/wiki/Factory_method_pattern
[js-base64]: https://www.npmjs.com/package/js-base64
[js-yaml]: https://www.npmjs.com/package/js-yaml
[json-refs]: https://www.npmjs.com/package/json-refs
[json-schema-faker]: https://www.npmjs.com/package/json-schema-faker
@ -69,5 +96,6 @@ they did just in case they wanted to use these libraries.)_
[path-loader]: https://www.npmjs.com/package/path-loader
[promises]: https://www.promisejs.org/
[npm]: https://www.npmjs.org/
[version-2.0-documentation]: https://github.com/apigee-127/swagger-core-api/blob/master/docs/versions/2.0.md
[swagger]: http://swagger.io
[z-schema]: https://www.npmjs.com/package/z-schema

15
docs/versions/2.0.md Normal file
Просмотреть файл

@ -0,0 +1,15 @@
swagger-core-api's Swagger 2.0 support is documented below. There are also some helpful pieces of information about
Swagger 2.0 as well.
## Swagger 2.0 Resources
* Specification Documentation: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md
* JSON Schema: https://github.com/swagger-api/swagger-spec/blob/master/schemas/v2.0/schema.json
## Semantic Validation
| Description | Type |
| :---------: | :---: |
| All places where a [Schema Object][schema-object] can be, and primitive parameters, the `items` property is required when `type` is set to `array` but this is **not** enforced in the JSON Schema. _(See [swagger-api/swagger-spec/issues/174](https://github.com/swagger-api/swagger-spec/issues/174))_ | Error |
[schema-object]: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#schemaObject

Просмотреть файл

@ -0,0 +1,27 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Apigee Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
'use strict';
module.exports.supportedHttpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch'];

Просмотреть файл

@ -25,11 +25,12 @@
'use strict';
var _ = require('lodash-compat');
var formatGenerators = require('./format-generators');
var validators = require('./validators');
var helpers = require('../../helpers');
var JsonRefs = require('json-refs');
var formatGenerators = require('./format-generators');
var helpers = require('../../helpers');
var types = require('../../types');
var validators = require('./validators');
var vHelpers = require('./helpers');
var docsUrl = 'https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md';
var mocker = helpers.createJSONSchemaMocker({
@ -55,7 +56,6 @@ var parameterSchemaProperties = [
'type',
'uniqueItems'
];
var supportedHttpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch'];
var version = '2.0';
function getParameterSchema (parameter) {
@ -81,7 +81,7 @@ function getParameterSchema (parameter) {
module.exports.documentation = docsUrl;
// The array of supported HTTP methods for each path
module.exports.supportedHttpMethods = supportedHttpMethods;
module.exports.supportedHttpMethods = vHelpers.supportedHttpMethods;
// The version for this Swagger version
module.exports.version = version;
@ -154,7 +154,7 @@ module.exports.getOperations = function (api) {
_.forEach(pathDef, function (operation, method) {
// Do not process non-operations
if (_.indexOf(supportedHttpMethods, method) === -1) {
if (_.indexOf(vHelpers.supportedHttpMethods, method) === -1) {
return;
}

Просмотреть файл

@ -24,9 +24,60 @@
'use strict';
var _ = require('lodash-compat');
var customFormatValidators = require('./format-validators');
var helpers = require('../../helpers');
var JsonRefs = require('json-refs');
var swaggerSchema = require('./schema.json');
var vHelpers = require('./helpers');
function walkSchema (blacklist, schema, path, handler) {
var type = schema.type || 'object';
function shouldSkip (cPath) {
return _.indexOf(blacklist, JsonRefs.pathToPointer(cPath)) > -1;
}
// Do not process items in the blacklist as they've been processed already
if (shouldSkip(path)) {
return;
}
function walker (pSchema, pPath) {
// Do not process items in the blacklist as they've been processed already
if (shouldSkip(pPath)) {
return;
}
_.forEach(pSchema, function (item, name) {
if (_.isNumber(name)) {
name = name.toString();
}
walkSchema(blacklist, item, pPath.concat(name), handler);
});
handler(pSchema, pPath);
}
if (!_.isUndefined(schema.schema)) {
walkSchema(blacklist, schema.schema, path.concat('schema'), handler);
} else if (type === 'array' && !_.isUndefined(schema.items)) {
walker(schema.items, path.concat('items'));
} else if (type === 'object') {
if (!_.isUndefined(schema.additionalProperties)) {
walkSchema(blacklist, schema.additionalProperties, path.concat('additionalProperties'), handler);
}
_.forEach(['allOf', 'properties'], function (propName) {
if (!_.isUndefined(schema[propName])) {
walker(schema[propName], path.concat(propName));
}
});
}
handler(schema, path);
}
/**
* Validates the resolved Swagger document against the Swagger 2.0 JSON Schema.
@ -41,9 +92,106 @@ function validateStructure (api) {
}), swaggerSchema, api.resolved);
}
/**
* Validates that all arrays have their required items property.
*
* @see {@link https://github.com/swagger-api/swagger-spec/issues/174}
*
* @param {SwaggerApi} api - The SwaggerApi object
*
* @returns {object} Object containing the errors and warnings of the validation
*/
function validateArrayItems (api) {
// Build a blacklist to avoid cascading errors/warnings
var blacklist = _.reduce(api.references, function (list, metadata, ptr) {
var refPath = JsonRefs.pathFromPointer(ptr);
// Remove the $ref part of the path
refPath.pop();
list.push(JsonRefs.pathToPointer(refPath));
return list;
}, []);
var response = {
errors: [],
warnings: []
};
function validate (schema, path) {
if (schema.type === 'array' && _.isUndefined(schema.items)) {
response.errors.push({
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: path
});
}
}
function validateParameters (parameters, path) {
_.forEach(parameters, function (parameterDef, name) {
if (_.isNumber(name)) {
name = name.toString();
}
walkSchema(blacklist, parameterDef, path.concat(name), validate);
});
}
function validateResponses (responses, path) {
_.forEach(responses, function (responseDef, name) {
var rPath = path.concat(name);
_.forEach(responseDef.headers, function (header, hName) {
walkSchema(blacklist, header, rPath.concat(['headers', hName]), validate);
});
if (!_.isUndefined(responseDef.schema)) {
walkSchema(blacklist, responseDef.schema, rPath.concat('schema'), validate);
}
});
}
// Validate definitions
_.forEach(api.resolved.definitions, function (definitionDef, name) {
walkSchema(blacklist, definitionDef, ['definitions', name], validate);
});
// Validate global parameter definitions
validateParameters(api.resolved.parameters, ['parameters']);
// Validate global response definitions
validateResponses(api.resolved.responses, ['responses']);
// Validate paths and operations
_.forEach(api.resolved.paths, function (pathDef, path) {
var pPath = ['paths', path];
// Validate path-level parameter definitions
validateParameters(pathDef.parameters, pPath.concat('parameters'));
_.forEach(pathDef, function (operationDef, method) {
var oPath = pPath.concat(method);
// Do not process non-operations
if (_.indexOf(vHelpers.supportedHttpMethods, method) === -1) {
return;
}
// Validate operation parameter definitions
validateParameters(operationDef.parameters, oPath.concat('parameters'));
// Validate operation response definitions
validateResponses(operationDef.responses, oPath.concat('responses'));
});
});
return response;
}
module.exports = {
jsonSchemaValidator: validateStructure,
semanticValidators: [
validateArrayItems
]
};

Просмотреть файл

@ -584,38 +584,523 @@ describe('swagger-core-api (Swagger 2.0)', function () {
});
describe('should throw an Error for an invalid document', function () {
var resolvedDefinition;
beforeEach(function () {
resolvedDefinition = swagger.resolved;
});
afterEach(function () {
swagger.resolved = resolvedDefinition;
});
// For testing we will manipulate the internal state of the SwaggerApi object. This is just for simplicity
// and is not something we support or suggest doing.
it('does not validate against JSON Schema', function () {
var cSwagger = _.cloneDeep(swagger.resolved);
var result;
it('does not validate against JSON Schema', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
delete cSwagger.paths;
swagger.resolved = cSwagger;
swaggerApi.create({
definition: cSwagger
})
.then(function (api) {
var result = api.validate();
result = swagger.validate();
assert.ok(result === false);
assert.deepEqual([], api.getLastWarnings());
assert.deepEqual([
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: paths',
path: []
}
], api.getLastErrors());
})
.then(done, done);
});
assert.ok(result === false);
assert.deepEqual([], swagger.getLastWarnings());
assert.deepEqual([
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: paths',
path: []
}
], swagger.getLastErrors());
describe('array type missing required items property', function () {
function validateBrokenArray (cSwagger, path, done) {
swaggerApi.create({
definition: cSwagger
})
.then(function (api) {
var result = api.validate();
assert.ok(result === false);
assert.deepEqual([], api.getLastWarnings());
assert.deepEqual([
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: path
}
], api.getLastErrors());
})
.then(done, done);
}
describe('schema definitions', function () {
describe('array', function () {
it('no items', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'array'
};
validateBrokenArray(cSwagger, ['definitions', 'Pet'], done);
});
it('items object', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'array',
items: {
type: 'array'
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'items'], done);
});
it('items array', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'array',
items: [
{
type: 'array'
}
]
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'items', '0'], done);
});
});
describe('object', function () {
describe('additionalProperties', function () {
it('no items', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
additionalProperties: {
type: 'array'
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'additionalProperties'], done);
});
it('items object', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
additionalProperties: {
type: 'array',
items: {
type: 'array'
}
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'additionalProperties', 'items'], done);
});
it('items array', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
additionalProperties: {
type: 'array',
items: [
{
type: 'array'
}
]
}
};
validateBrokenArray(cSwagger,
['definitions', 'Pet', 'additionalProperties', 'items', '0'],
done);
});
});
describe('properties', function () {
it('no items', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
properties: {
aliases: {
type: 'array'
}
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'properties', 'aliases'], done);
});
it('items object', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
properties: {
aliases: {
type: 'array',
items: {
type: 'array'
}
}
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'properties', 'aliases', 'items'], done);
});
it('items array', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
properties: {
aliases: {
type: 'array',
items: [
{
type: 'array'
}
]
}
}
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'properties', 'aliases', 'items', '0'], done);
});
});
describe('allOf', function () {
it('no items', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
allOf: [
{
type: 'array'
}
]
};
validateBrokenArray(cSwagger, ['definitions', 'Pet', 'allOf', '0'], done);
});
it('items object', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
allOf: [
{
type: 'object',
properties: {
aliases: {
type: 'array',
items: {
type: 'array'
}
}
}
}
]
};
validateBrokenArray(cSwagger,
['definitions', 'Pet', 'allOf', '0', 'properties', 'aliases', 'items'],
done);
});
it('items array', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.definitions.Pet = {
type: 'object',
allOf: [
{
type: 'object',
properties: {
aliases: {
type: 'array',
items: [
{
type: 'array'
}
]
}
}
}
]
};
validateBrokenArray(cSwagger,
['definitions', 'Pet', 'allOf', '0', 'properties', 'aliases', 'items', '0'],
done);
});
});
});
it('recursive', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
var errorSchema = {
type: 'object',
allOf: [
{
type: 'array'
}
],
properties: {
aliases: {
type: 'array'
}
},
additionalProperties: {
type: 'array'
}
};
cSwagger.definitions.Pet = {
allOf: [
errorSchema
],
properties: {
aliases: errorSchema
},
additionalProperties: errorSchema
};
swaggerApi.create({
definition: cSwagger
})
.then(function (api) {
var result = api.validate();
assert.ok(result === false);
assert.deepEqual([], api.getLastWarnings());
assert.deepEqual([
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'additionalProperties', 'additionalProperties']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'additionalProperties', 'allOf', '0']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'additionalProperties', 'properties', 'aliases']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'allOf', '0', 'additionalProperties']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'allOf', '0', 'allOf', '0']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'allOf', '0', 'properties', 'aliases']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'properties', 'aliases', 'additionalProperties']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'properties', 'aliases', 'allOf', '0']
},
{
code: 'OBJECT_MISSING_REQUIRED_PROPERTY',
message: 'Missing required property: items',
path: ['definitions', 'Pet', 'properties', 'aliases', 'properties', 'aliases']
}
], api.getLastErrors());
})
.then(done, done);
});
});
describe('parameter definitions', function () {
describe('global', function () {
it('body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.parameters = {
petInBody: {
in: 'body',
name: 'body',
description: 'A Pet',
required: true,
schema: {
properties: {
aliases: {
type: 'array'
}
}
}
}
};
validateBrokenArray(cSwagger, ['parameters', 'petInBody', 'schema', 'properties', 'aliases'], done);
});
it('non-body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.parameters = {
petStatus: _.cloneDeep(cSwagger.paths['/pet/findByStatus'].get.parameters[0])
};
delete cSwagger.parameters.petStatus.items;
validateBrokenArray(cSwagger, ['parameters', 'petStatus'], done);
});
});
describe('path-level', function () {
it('body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.paths['/pet'].parameters = [
{
in: 'body',
name: 'body',
description: 'A Pet',
required: true,
schema: {
properties: {
aliases: {
type: 'array'
}
}
}
}
];
validateBrokenArray(cSwagger,
['paths', '/pet', 'parameters', '0', 'schema', 'properties', 'aliases'],
done);
});
it('non-body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.paths['/pet'].parameters = [
_.cloneDeep(cSwagger.paths['/pet/findByStatus'].get.parameters[0])
];
delete cSwagger.paths['/pet'].parameters[0].items;
validateBrokenArray(cSwagger, ['paths', '/pet', 'parameters', '0'], done);
});
});
describe('operation', function () {
it('body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
delete cSwagger.paths['/user/createWithArray'].post.parameters[0].schema.items;
validateBrokenArray(cSwagger,
['paths', '/user/createWithArray', 'post', 'parameters', '0', 'schema'],
done);
});
it('non-body parameter', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
delete cSwagger.paths['/pet/findByStatus'].get.parameters[0].items;
validateBrokenArray(cSwagger, ['paths', '/pet/findByStatus', 'get', 'parameters', '0'], done);
});
});
});
describe('responses', function () {
describe('global', function () {
it('headers', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.responses = {
success: {
description: 'A response indicative of a successful request',
headers: {
'X-Broken-Array': {
type: 'array'
}
}
}
};
validateBrokenArray(cSwagger, ['responses', 'success', 'headers', 'X-Broken-Array'], done);
});
it('schema definition', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.responses = {
success: {
description: 'A response indicative of a successful request',
schema: {
type: 'array'
}
}
};
validateBrokenArray(cSwagger, ['responses', 'success', 'schema'], done);
});
});
describe('operation', function () {
it('headers', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
cSwagger.paths['/pet/findByStatus'].get.responses['200'].headers = {
'X-Broken-Array': {
type: 'array'
}
};
validateBrokenArray(cSwagger,
[
'paths',
'/pet/findByStatus',
'get',
'responses',
'200',
'headers',
'X-Broken-Array'
],
done);
});
it('schema definition', function (done) {
var cSwagger = _.cloneDeep(swaggerDoc);
delete cSwagger.paths['/pet/findByStatus'].get.responses['200'].schema.items;
validateBrokenArray(cSwagger,
['paths', '/pet/findByStatus', 'get', 'responses', '200', 'schema'],
done);
});
});
});
});
});
});