"dependencies" with option errorDataPath = "property" sets dataPath to missing property, error params has missingProperty, #68, #69

master
Evgeny Poberezkin 2015-11-21 12:43:44 +00:00
parent 5c0c8b3b1c
commit 56a8b5b582
8 changed files with 249 additions and 178 deletions

View File

@ -535,6 +535,7 @@ Properties of `params` object in errors depend on the keyword that failed valida
- `additionalProperties` - property `additionalProperty` (the property not used in `properties` and `patternProperties` keywords).
- `dependencies` - properties:
- `property` (dependent property),
- `missingProperty` (required missing dependency - only the first one is reported currently)
- `deps` (required dependencies, comma separated list as a string),
- `depsCount` (the number of required dependedncies).
- `format` - property `format` (the schema of the keyword).

View File

@ -200,7 +200,7 @@
additionalItems: "{ limit: {{=$schema.length}} }",
additionalProperties: "{ additionalProperty: '{{=$additionalProperty}}' }",
anyOf: "{}",
dependencies: "{ depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}', property: '{{= it.util.escapeQuotes($property) }}' }",
dependencies: "{ property: '{{= it.util.escapeQuotes($property) }}', missingProperty: '{{=$missingProperty}}', depsCount: {{=$deps.length}}, deps: '{{? $deps.length==1 }}{{= it.util.escapeQuotes($deps[0]) }}{{??}}{{= it.util.escapeQuotes($deps.join(\", \")) }}{{?}}' }",
format: "{ format: '{{=it.util.escapeQuotes($schema)}}' }",
maximum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }",
minimum: "{ comparison: '{{=$op}}', limit: {{=$schema}}, exclusive: {{=$exclusive}} }",

View File

@ -1,4 +1,5 @@
{{# def.definitions }}
{{# def.missing }}
{{# def.setup:'dependencies' }}
{{# def.setupNextLevel }}
@ -16,22 +17,21 @@
var {{=$errs}} = errors;
{{## def.checkPropertyDeps:
{{~ $deps:$dep:$i }}
{{?$i}} || {{?}}
{{=$data}}{{= it.util.getProperty($dep) }} === undefined
{{~}}
#}}
{{ var $currentErrorPath = it.errorPath; }}
var missing{{=$lvl}};
{{ for (var $property in $propertyDeps) { }}
if ({{=$data}}{{= it.util.getProperty($property) }} !== undefined) {
{{ $deps = $propertyDeps[$property]; }}
if ({{# def.checkPropertyDeps }}) {
if ({{# def.checkMissingProperty:$deps }}) {
{{# def.errorMissingProperty }}
{{# def.error:'dependencies' }}
} {{# def.elseIfValid }}
}
{{ } }}
{{ it.errorPath = $currentErrorPath; }}
{{ for (var $property in $schemaDeps) { }}
{{ var $sch = $schemaDeps[$property]; }}

20
lib/dot/missing.def Normal file
View File

@ -0,0 +1,20 @@
{{## def.checkMissingProperty:_properties:
{{~ _properties:_$property:$i }}
{{?$i}} || {{?}}
{{ var $prop = it.util.getProperty(_$property); }}
( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? _$property : $prop) }}) )
{{~}}
#}}
{{## def.errorMissingProperty:
{{
var $propertyPath = 'missing' + $lvl
, $missingProperty = '\' + ' + $propertyPath + ' + \'';
if (it.opts._errorDataPathProperty) {
it.errorPath = it.opts.jsonPointers
? it.util.getPathExpr($currentErrorPath, $propertyPath, true)
: $currentErrorPath + ' + ' + $propertyPath;
}
}}
#}}

View File

@ -1,14 +1,7 @@
{{# def.definitions }}
{{# def.missing }}
{{# def.setup:'required' }}
{{## def.checkRequired:
{{~ $required:$property:$i }}
{{? $i}} || {{?}}
{{ var $prop = it.util.getProperty($property); }}
( {{=$data}}{{=$prop}} === undefined && (missing{{=$lvl}} = {{= it.util.toQuotedString(it.opts.jsonPointers ? $property : $prop) }}) )
{{~}}
#}}
{{## def.setupLoop:
var schema{{=$lvl}} = validate.schema{{=$schemaPath}};
{{
@ -41,16 +34,8 @@
{{? $breakOnError }}
var missing{{=$lvl}};
{{? $required.length <= 20 }}
if ({{# def.checkRequired }}) {
{{
var $propertyPath = 'missing' + $lvl
, $missingProperty = '\' + ' + $propertyPath + ' + \'';
if (it.opts._errorDataPathProperty) {
it.errorPath = it.opts.jsonPointers
? it.util.getPathExpr($currentErrorPath, $propertyPath, true)
: $currentErrorPath + ' + ' + $propertyPath;
}
}}
if ({{# def.checkMissingProperty:$required }}) {
{{# def.errorMissingProperty }}
{{# def.error:'required' }}
} else {
{{??}}

View File

@ -59,7 +59,7 @@
{{~ $rulesGroup.rules:$rule }}
{{? $shouldUseRule($rule) }}
{{? $rule.custom }}
{{# def.customRule }}
{{# def.custom }}
{{??}}
{{= $rule.code(it) }}
{{?}}

View File

@ -6,8 +6,10 @@ var glob = require('glob')
, doT = require('dot')
, beautify = require('js-beautify').js_beautify;
var defs = fs.readFileSync(path.join(__dirname, '../lib/dot/definitions.def'));
var customRule = fs.readFileSync(path.join(__dirname, '../lib/dot/custom.def'));
var defs = {};
['definitions', 'custom', 'missing'].forEach(function (name) {
defs[name] = fs.readFileSync(path.join(__dirname, '../lib/dot/' + name + '.def'));
});
var files = glob.sync('../lib/dot/*.jst', { cwd: __dirname });
var dotjsPath = path.join(__dirname, '../lib/dotjs');
@ -22,7 +24,7 @@ files.forEach(function (f) {
var keyword = path.basename(f, '.jst');
var targetPath = path.join(dotjsPath, keyword + '.js');
var template = fs.readFileSync(path.join(__dirname, f));
var code = doT.compile(template, { definitions: defs, customRule: customRule });
var code = doT.compile(template, defs);
code = code.toString()
.replace(OUT_EMPTY_STRING, '')
.replace(FUNCTION_NAME, 'function generate_' + keyword + '(it) {');

View File

@ -26,7 +26,7 @@ describe('Validation errors', function () {
});
});
it('error should include dataPath in refs', function() {
it('"refs" error should include dataPath', function() {
testSchema1({
definitions: {
num: { type: 'number' }
@ -38,175 +38,238 @@ describe('Validation errors', function () {
});
it('with option errorDataPath="property" errors for additionalProperties should include property in dataPath ', function() {
createInstances('property');
testAdditional('property');
});
describe('"additionalProperties" errors', function() {
it('should include property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testAdditional('property');
});
it('WITHOUT option errorDataPath errors for additionalProperties should NOT include property in dataPath', function() {
testAdditional();
});
it('should NOT include property in dataPath WITHOUT option errorDataPath', function() {
testAdditional();
});
function testAdditional(errorDataPath) {
var schema = {
properties: {
foo: {},
bar: {}
},
additionalProperties: false
};
function testAdditional(errorDataPath) {
var schema = {
properties: {
foo: {},
bar: {}
},
additionalProperties: false
};
var data = { foo: 1, bar: 2 }
, invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 };
var data = { foo: 1, bar: 2 }
, invalidData = { foo: 1, bar: 2, baz: 3, quux: 4 };
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData);
shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData);
shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData, 2);
shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' });
shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' });
if (errorDataPath == 'property') {
fullValidate.errors
.filter(function(err) { return err.keyword == 'additionalProperties'; })
.map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); })
.forEach(function(p) { delete invalidData[p]; });
invalidData .should.eql({ foo: 1, bar: 2 });
}
}
it('with option errorDataPath="property" errors for required should include missing property in dataPath', function() {
createInstances('property');
testRequired('property');
});
it('without option errorDataPath errors for required should NOT include missing property in dataPath', function() {
testRequired();
});
function testRequired(errorDataPath) {
var schema = {
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema, '.');
}
it('required validation and errors for large data/schemas with option errorDataPath="property"', function() {
createInstances('property');
testRequiredLargeSchema('property');
});
it('required validation and errors for large data/schemas WITHOUT option errorDataPath="property"', function() {
testRequiredLargeSchema();
});
function testRequiredLargeSchema(errorDataPath) {
var schema = { required: [] }
, data = {}
, invalidData1 = {}
, invalidData2 = {};
for (var i=0; i<100; i++) {
schema.required.push(''+i); // properties from '0' to '99' are required
data[i] = invalidData1[i] = invalidData2[i] = i;
}
delete invalidData1[1]; // property '1' will be missing
delete invalidData2[2]; // properties '2' and '198' will be missing
delete invalidData2[98];
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
test();
var schema = { anyOf: [ schema ] };
test(1);
function test(extraErrors) {
extraErrors = extraErrors || 0;
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validate, invalidData2, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' });
shouldBeInvalid(validate, invalidData);
shouldBeError(validate.errors[0], 'additionalProperties', path("['baz']"), undefined, { additionalProperty: 'baz' });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' });
shouldBeInvalid(validateJP, invalidData);
shouldBeError(validateJP.errors[0], 'additionalProperties', path("/baz"), undefined, { additionalProperty: 'baz' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' });
shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' });
shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' });
shouldBeInvalid(fullValidate, invalidData, 2);
shouldBeError(fullValidate.errors[0], 'additionalProperties', path('/baz'), undefined, { additionalProperty: 'baz' });
shouldBeError(fullValidate.errors[1], 'additionalProperties', path('/quux'), undefined, { additionalProperty: 'quux' });
if (errorDataPath == 'property') {
fullValidate.errors
.filter(function(err) { return err.keyword == 'additionalProperties'; })
.map(function(err) { return fullAjv.opts.jsonPointers ? err.dataPath.substr(1) : err.dataPath.slice(2,-2); })
.forEach(function(p) { delete invalidData[p]; });
invalidData .should.eql({ foo: 1, bar: 2 });
}
}
}
it('required validation with "properties" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredAndProperties('property');
});
it('required validation with "properties" WITHOUT option errorDataPath="property"', function() {
testRequiredAndProperties();
describe('"required" errors', function() {
it('should include missing property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testRequired('property');
});
it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() {
testRequired();
});
function testRequired(errorDataPath) {
var schema = {
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema, '.');
}
it('large data/schemas with option errorDataPath="property"', function() {
createInstances('property');
testRequiredLargeSchema('property');
});
it('large data/schemas WITHOUT option errorDataPath', function() {
testRequiredLargeSchema();
});
function testRequiredLargeSchema(errorDataPath) {
var schema = { required: [] }
, data = {}
, invalidData1 = {}
, invalidData2 = {};
for (var i=0; i<100; i++) {
schema.required.push(''+i); // properties from '0' to '99' are required
data[i] = invalidData1[i] = invalidData2[i] = i;
}
delete invalidData1[1]; // property '1' will be missing
delete invalidData2[2]; // properties '2' and '198' will be missing
delete invalidData2[98];
var path = pathFunc(errorDataPath);
var msg = msgFunc(errorDataPath);
test();
var schema = { anyOf: [ schema ] };
test(1);
function test(extraErrors) {
extraErrors = extraErrors || 0;
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['1']"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validate, invalidData2, 1 + extraErrors);
shouldBeError(validate.errors[0], 'required', path("['2']"), msg('2'), { missingProperty: '2' });
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/1"), msg('1'), { missingProperty: '1' });
shouldBeInvalid(validateJP, invalidData2, 1 + extraErrors);
shouldBeError(validateJP.errors[0], 'required', path("/2"), msg('2'), { missingProperty: '2' });
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1, 1 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/1'), msg('1'), { missingProperty: '1' });
shouldBeInvalid(fullValidate, invalidData2, 2 + extraErrors);
shouldBeError(fullValidate.errors[0], 'required', path('/2'), msg('2'), { missingProperty: '2' });
shouldBeError(fullValidate.errors[1], 'required', path('/98'), msg('98'), { missingProperty: '98' });
}
}
it('with "properties" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredAndProperties('property');
});
it('with "properties" WITHOUT option errorDataPath', function() {
testRequiredAndProperties();
});
function testRequiredAndProperties(errorDataPath) {
var schema = {
properties: {
'foo': { type: 'number' },
'bar': { type: 'number' },
'baz': { type: 'number' },
},
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema);
}
it('in "anyOf" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredInAnyOf('property');
});
it('in "anyOf" WITHOUT option errorDataPath', function() {
testRequiredInAnyOf();
});
function testRequiredInAnyOf(errorDataPath) {
var schema = {
anyOf: [
{ required: ['foo', 'bar', 'baz'] }
]
};
_testRequired(errorDataPath, schema, '.', 1);
}
});
function testRequiredAndProperties(errorDataPath) {
var schema = {
properties: {
'foo': { type: 'number' },
'bar': { type: 'number' },
'baz': { type: 'number' },
},
required: ['foo', 'bar', 'baz']
};
_testRequired(errorDataPath, schema);
}
describe('"dependencies" errors', function() {
it('should include missing property in dataPath with option errorDataPath="property"', function() {
createInstances('property');
testDependencies('property');
});
it('should NOT include missing property in dataPath WITHOUT option errorDataPath', function() {
testDependencies();
});
it('required validation in "anyOf" with option errorDataPath="property"', function() {
createInstances('property');
testRequiredInAnyOf('property');
function testDependencies(errorDataPath) {
var schema = {
dependencies: {
a: ['foo', 'bar', 'baz']
}
};
var data = { a: 0, foo: 1, bar: 2, baz: 3 }
, invalidData1 = { a: 0, foo: 1, baz: 3 }
, invalidData2 = { a: 0, bar: 2 };
var path = pathFunc(errorDataPath);
var msg = 'should have properties foo, bar, baz when property a is present';
var validate = ajv.compile(schema);
shouldBeValid(validate, data);
shouldBeInvalid(validate, invalidData1);
shouldBeError(validate.errors[0], 'dependencies', path('.bar'), msg, params('.bar'));
shouldBeInvalid(validate, invalidData2);
shouldBeError(validate.errors[0], 'dependencies', path('.foo'), msg, params('.foo'));
var validateJP = ajvJP.compile(schema);
shouldBeValid(validateJP, data);
shouldBeInvalid(validateJP, invalidData1);
shouldBeError(validateJP.errors[0], 'dependencies', path('/bar'), msg, params('bar'));
shouldBeInvalid(validateJP, invalidData2);
shouldBeError(validateJP.errors[0], 'dependencies', path('/foo'), msg, params('foo'));
var fullValidate = fullAjv.compile(schema);
shouldBeValid(fullValidate, data);
shouldBeInvalid(fullValidate, invalidData1);
shouldBeError(fullValidate.errors[0], 'dependencies', path('/bar'), msg, params('bar'));
shouldBeInvalid(fullValidate, invalidData2/*, 2*/);
shouldBeError(fullValidate.errors[0], 'dependencies', path('/foo'), msg, params('foo'));
// shouldBeError(fullValidate.errors[1], 'dependencies', path('/baz'), msg, params('baz'));
function params(missing) {
var p = {
property: 'a',
deps: 'foo, bar, baz',
depsCount: 3
};
p.missingProperty = missing;
return p;
}
}
});
it('required validation with "anyOf" WITHOUT option errorDataPath="property"', function() {
testRequiredInAnyOf();
});
function testRequiredInAnyOf(errorDataPath) {
var schema = {
anyOf: [
{ required: ['foo', 'bar', 'baz'] }
]
};
_testRequired(errorDataPath, schema, '.', 1);
}
function _testRequired(errorDataPath, schema, prefix, extraErrors) {
prefix = prefix || '';
@ -257,7 +320,7 @@ describe('Validation errors', function () {
}
it('errors for items should include item index without quotes in dataPath (#48)', function() {
it('"items" errors should include item index without quotes in dataPath (#48)', function() {
var schema1 = {
id: 'schema1',
type: 'array',