Skip to content

Commit

Permalink
"default" keyword in "properties" subschemas, #42
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Jan 9, 2016
1 parent a1afce9 commit 0b725bb
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 52 deletions.
9 changes: 7 additions & 2 deletions lib/ajv.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,18 @@ function Ajv(opts) {
}
schemaObj.compiling = true;

var currentRA = self.opts.removeAdditional;
if (currentRA && schemaObj.meta) self.opts.removeAdditional = false;
var currentRA = self.opts.removeAdditional
, currentUD = self.opts.useDefaults;
if (schemaObj.meta) {
if (currentRA) self.opts.removeAdditional = false;
if (currentUD) self.opts.useDefaults = false;
}
var v;
try { v = compileSchema.call(self, schemaObj.schema, root, schemaObj.localRefs); }
finally {
schemaObj.compiling = false;
if (currentRA) self.opts.removeAdditional = currentRA;
if (currentUD) self.opts.useDefaults = currentUD;
}

schemaObj.validate = v;
Expand Down
47 changes: 30 additions & 17 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ function compile(schema, root, localRefs, baseId) {
, refs = {}
, patterns = []
, patternsHash = {}
, defaults = []
, defaultsHash = {}
, customRules = []
, customRulesHash = {};

Expand Down Expand Up @@ -50,14 +52,16 @@ function compile(schema, root, localRefs, baseId) {
resolve: resolve,
resolveRef: resolveRef,
usePattern: usePattern,
useDefault: useDefault,
useCustomRule: useCustomRule,
opts: self.opts,
formats: formats,
self: self
});

validateCode = refsCode(refVal) + patternsCode(patterns)
+ customRulesCode(customRules) + validateCode;
validateCode = vars(refVal, refValCode) + vars(patterns, patternCode)
+ vars(defaults, defaultCode) + vars(customRules, customRuleCode)
+ validateCode;

if (self.opts.beautify) {
var opts = self.opts.beautify === true ? { indent_size: 2 } : self.opts.beautify;
Expand Down Expand Up @@ -146,6 +150,25 @@ function compile(schema, root, localRefs, baseId) {
return 'pattern' + index;
}

function useDefault(value) {
switch (typeof value) {
case 'boolean':
case 'number':
return '' + value;
case 'string':
return util.toQuotedString(value);
case 'object':
if (value === null) return 'null';
var valueStr = stableStringify(value);
var index = defaultsHash[valueStr];
if (index === undefined) {
index = defaultsHash[valueStr] = defaults.length;
defaults[index] = value;
}
return 'default' + index;
}
}

function useCustomRule(rule, schema, parentSchema, it) {
var compile = rule.definition.compile
, inline = rule.definition.inline
Expand Down Expand Up @@ -173,37 +196,27 @@ function compile(schema, root, localRefs, baseId) {
}


function patternsCode(patterns) {
return _arrCode(patterns, patternCode);
}


function patternCode(i, patterns) {
return 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');';
}


function refsCode(refVal) {
return _arrCode(refVal, refCode);
function defaultCode(i) {
return 'var default' + i + ' = defaults[' + i + '];';
}


function refCode(i, refVal) {
function refValCode(i, refVal) {
return refVal[i] ? 'var refVal' + i + ' = refVal[' + i + '];' : '';
}


function customRulesCode(customRules) {
return _arrCode(customRules, customRuleCode);
}


function customRuleCode(i, rule) {
function customRuleCode(i) {
return 'var customRule' + i + ' = customRules[' + i + '];';
}


function _arrCode(arr, statement) {
function vars(arr, statement) {
if (!arr.length) return '';
var code = '';
for (var i=0; i<arr.length; i++)
Expand Down
8 changes: 5 additions & 3 deletions lib/compile/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ function toHash(arr) {
var IDENTIFIER = /^[a-z$_][a-z$_0-9]*$/i;
var SINGLE_QUOTE = /'|\\/g;
function getProperty(key) {
return IDENTIFIER.test(key)
? '.' + key
: "['" + key.replace(SINGLE_QUOTE, '\\$&') + "']";
return typeof key == 'number'
? '[' + key + ']'
: IDENTIFIER.test(key)
? '.' + key
: "['" + key.replace(SINGLE_QUOTE, '\\$&') + "']";
}


Expand Down
63 changes: 39 additions & 24 deletions lib/dot/properties.jst
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,26 @@ var valid{{=$it.level}} = true;
{{# def.ifResultValid }}
{{?}}

{{ var $useDefaults = it.opts.useDefaults && !it.compositeRule; }}

{{? $schemaKeys.length }}
{{~ $schemaKeys:$propertyKey }}
{{ var $sch = $schema[$propertyKey]; }}

{{
var $prop = it.util.getProperty($propertyKey)
, $passData = $data + $prop
, $hasDefault = $useDefaults && $sch.default !== undefined;
}}

{{? $hasDefault }}
if ({{=$passData}} === undefined)
{{=$passData}} = {{= it.useDefault($sch.default) }};
{{?}}

{{? {{# def.nonEmptySchema:$sch}} }}
{{
$it.schema = $sch;
var $prop = it.util.getProperty($propertyKey)
, $passData = $data + $prop;
$it.schemaPath = $schemaPath + $prop;
$it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey);
$it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers);
Expand All @@ -159,34 +170,38 @@ var valid{{=$it.level}} = true;
var {{=$nextData}} = {{=$passData}};
{{?}}

{{? $requiredHash && $requiredHash[$propertyKey] }}
if ({{=$useData}} === undefined) {
valid{{=$it.level}} = false;
{{
var $currentErrorPath = it.errorPath
, $currErrSchemaPath = $errSchemaPath
, $missingProperty = it.util.escapeQuotes($propertyKey);
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers);
}
$errSchemaPath = it.errSchemaPath + '/required';
}}
{{# def.error:'required' }}
{{ $errSchemaPath = $currErrSchemaPath; }}
{{ it.errorPath = $currentErrorPath; }}
} else {
{{? $hasDefault }}
{{= $code }}
{{??}}
{{? $breakOnError }}
{{? $requiredHash && $requiredHash[$propertyKey] }}
if ({{=$useData}} === undefined) {
valid{{=$it.level}} = true;
valid{{=$it.level}} = false;
{{
var $currentErrorPath = it.errorPath
, $currErrSchemaPath = $errSchemaPath
, $missingProperty = it.util.escapeQuotes($propertyKey);
if (it.opts._errorDataPathProperty) {
it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers);
}
$errSchemaPath = it.errSchemaPath + '/required';
}}
{{# def.error:'required' }}
{{ $errSchemaPath = $currErrSchemaPath; }}
{{ it.errorPath = $currentErrorPath; }}
} else {
{{??}}
if ({{=$useData}} !== undefined) {
{{? $breakOnError }}
if ({{=$useData}} === undefined) {
valid{{=$it.level}} = true;
} else {
{{??}}
if ({{=$useData}} !== undefined) {
{{?}}
{{?}}
{{?}}

{{= $code }}
}
{{= $code }}
}
{{?}} {{ /* $hasDefault */ }}
{{?}} {{ /* def.nonEmptySchema */ }}

{{# def.ifResultValid }}
Expand Down
33 changes: 28 additions & 5 deletions lib/dot/required.jst
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@


{{## def.setupLoop:
{{
var $useDefaults = it.opts.useDefaults && !it.compositeRule
, $hasProperties = it.schema.properties !== undefined
, $checkDefaults = $useDefaults && $hasProperties;
}}

{{? !$isData }}
var schema{{=$lvl}} = validate.schema{{=$schemaPath}};
{{?}}

{{? $checkDefaults }}
var propsSchema{{=$lvl}} = validate.schema{{=it.schemaPath}}.properties;
{{?}}

{{
var $i = 'i' + $lvl
, $propertyPath = 'schema' + $lvl + '[' + $i + ']'
Expand All @@ -21,6 +31,14 @@
#}}


{{## def.checkDefaults:
var property{{=$lvl}} = schema{{=$lvl}}[{{=$i}}];
{{? $checkDefaults }}
if (propsSchema{{=$lvl}}[property{{=$lvl}}].default !== undefined)
continue;
{{?}}
#}}

{{? !$isData }}
{{? $schema.length < it.opts.loopRequired &&
it.schema.properties && Object.keys(it.schema.properties).length }}
Expand All @@ -38,18 +56,22 @@


{{? $isData || $required.length }}
{{ var $currentErrorPath = it.errorPath; }}
{{
var $currentErrorPath = it.errorPath
, $loopRequired = $isData || $required.length >= it.opts.loopRequired;
}}

{{? $breakOnError }}
var missing{{=$lvl}};
{{? $isData || $required.length >= it.opts.loopRequired }}
{{? $loopRequired }}
{{# def.setupLoop }}
var {{=$valid}};
var {{=$valid}} = true;

{{?$isData}}{{# def.check$dataIsArray }}{{?}}

for (var {{=$i}} = 0; {{=$i}} < schema{{=$lvl}}.length; {{=$i}}++) {
{{=$valid}} = {{=$data}}[schema{{=$lvl}}[{{=$i}}]] !== undefined;
{{# def.checkDefaults }}
{{=$valid}} = {{=$data}}[property{{=$lvl}}] !== undefined;
if (!{{=$valid}}) break;
}

Expand All @@ -64,7 +86,7 @@
} else {
{{?}}
{{??}}
{{? $isData || $required.length >= it.opts.loopRequired }}
{{? $loopRequired }}
{{# def.setupLoop }}
{{? $isData }}
if (schema{{=$lvl}} && !Array.isArray(schema{{=$lvl}})) {
Expand All @@ -73,6 +95,7 @@
{{?}}

for (var {{=$i}} = 0; {{=$i}} < schema{{=$lvl}}.length; {{=$i}}++) {
{{# def.checkDefaults }}
if ({{=$data}}[schema{{=$lvl}}[{{=$i}}]] === undefined) {
{{# def.addError:'required' }}
}
Expand Down
2 changes: 1 addition & 1 deletion spec/ajv_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ var options = fullTest
verbose: true,
format: 'full',
inlineRefs: false,
jsonPointers: true,
jsonPointers: true
}
: { allErrors: true };

Expand Down
43 changes: 43 additions & 0 deletions spec/options.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


var Ajv = require('./ajv')
, getAjvInstances = require('./ajv_instances')
, should = require('./chai').should()


Expand Down Expand Up @@ -331,4 +332,46 @@ describe('Ajv Options', function () {
}
});
});


describe('useDefaults', function() {
it('should replace undefined property with default value', function() {
var instances = getAjvInstances({
allErrors: true,
loopRequired: 3
}, { useDefaults: true });

instances.forEach(test);


function test(ajv) {
var schema = {
properties: {
foo: { type: 'string', default: 'abc' },
bar: { type: 'number', default: 1 },
baz: { type: 'boolean', default: false },
nil: { type: 'null', default: null },
obj: { type: 'object', default: {} },
arr: { type: 'array', default: [] }
},
required: ['foo', 'bar', 'baz', 'nil', 'obj', 'arr']
};

var validate = ajv.compile(schema);

var data = {};
try {
validate(data) .should.equal(true);
} catch(e) {
console.log('failed with:', ajv.opts);
throw e;
}
data. should.eql({ foo: 'abc', bar: 1, baz: false, nil: null, obj: {}, arr:[] });

var data = { foo: 'foo', bar: 2, obj: { test: true } };
validate(data) .should.equal(true);
data. should.eql({ foo: 'foo', bar: 2, baz: false, nil: null, obj: { test: true }, arr:[] });
}
});
});
});

0 comments on commit 0b725bb

Please sign in to comment.