Skip to content

Commit

Permalink
Merge pull request #39 from epoberezkin/inline
Browse files Browse the repository at this point in the history
Inline referenced schemas without refs in them, #6
  • Loading branch information
Evgeny Poberezkin committed Aug 23, 2015
2 parents 6d37952 + cc8de5b commit 44e1af1
Show file tree
Hide file tree
Showing 10 changed files with 493 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -225,6 +225,7 @@ Options can have properties `separator` (string used to separate errors, ", " by
- _schemas_: an array or object of schemas that will be added to the instance. If the order is important, pass array. In this case schemas must have IDs in them. Otherwise the object can be passed - `addSchema(value, key)` will be called for each schema in this object.
- _meta_: add [meta-schema](http://json-schema.org/documentation.html) so it can be used by other schemas (true by default).
- _validateSchema_: validate added/compiled schemas against meta-schema (true by default). `$schema` property in the schema can either be http://json-schema.org/schema or http://json-schema.org/draft-04/schema or absent (draft-4 meta-schema will be used) or can be a reference to the schema previously added with `addMetaSchema` method. If the validation fails, the exception is thrown. Pass "log" in this option to log error instead of throwing exception. Pass `false` to skip schema validation.
- _inlineRefs_: by default the referenced schemas that don't have refs in them are inlined, regardless of their size - that substantially improves performance at the cost of the bigger size of compiled schema functions. Pass `false` to not inline referenced schemas (they will be compiled as separate functions). Pass integer number to limit the maximum number of keywords of the schema that will be inlined.
- _missingRefs_: by default if the reference cannot be resolved during compilation the exception is thrown. Pass 'ignore' to log error during compilation and pass validation. Pass 'fail' to log error and successfully compile schema but fail validation if this rule is checked.
- _uniqueItems_: validate `uniqueItems` keyword (true by default).
- _unicode_: calculate correct length of strings with unicode pairs (true by default). Pass `false` to use `.length` of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters.
Expand Down
2 changes: 1 addition & 1 deletion lib/cache.js
Expand Up @@ -18,4 +18,4 @@ Cache.prototype.get = function Cache_get(key) {

Cache.prototype.del = function Cache_del(key) {
delete this._cache[key];
}
};
70 changes: 55 additions & 15 deletions lib/compile/index.js
Expand Up @@ -49,7 +49,7 @@ function compile(schema, root, localRefs) {
formats: formats
});

validateCode = patternsCode(patterns) + validateCode;
validateCode = refsCode(refVal) + patternsCode(patterns) + validateCode;

if (self.opts.beautify) {
var opts = self.opts.beautify === true ? { indent_size: 2 } : self.opts.beautify;
Expand Down Expand Up @@ -77,38 +77,57 @@ function compile(schema, root, localRefs) {

function resolveRef(baseId, ref, isRoot) {
ref = resolve.url(baseId, ref);
if (refs[ref]) return 'refVal[' + refs[ref] + ']';
var refIndex = refs[ref];
var _refVal, refCode;
if (refIndex !== undefined) {
_refVal = refVal[refIndex];
refCode = 'refVal[' + refIndex + ']';
return resolvedRef(_refVal, refCode);
}
if (!isRoot) {
var rootRefId = root.refs[ref];
if (rootRefId !== undefined)
return addLocalRef(ref, root.refVal[rootRefId]);
if (rootRefId !== undefined) {
_refVal = root.refVal[rootRefId];
refCode = addLocalRef(ref, _refVal);
return resolvedRef(_refVal, refCode);
}
}

var refCode = addLocalRef(ref);
refCode = addLocalRef(ref);
var v = resolve.call(self, localCompile, root, ref);
if (!v) {
var localSchema = localRefs[ref];
if (localSchema) v = compile.call(self, localSchema, root, localRefs);
if (localSchema) {
v = resolve.inlineRef(localSchema, self.opts.inlineRefs)
? localSchema
: compile.call(self, localSchema, root, localRefs);
}
}

if (v) {
replaceLocalRef(ref, v);
return refCode;
return resolvedRef(v, refCode);
}
}

function addLocalRef(ref, v) {
var refId = refVal.length;
refVal[refId] = v;
refs[ref] = refId;
return 'refVal[' + refId + ']';
return 'refVal' + refId;
}

function replaceLocalRef(ref, v) {
var refId = refs[ref];
refVal[refId] = v;
}

function resolvedRef(schema, code) {
return typeof schema == 'object'
? { schema: schema, code: code }
: code;
}

function usePattern(regexStr) {
var index = patternsHash[regexStr];
if (index === undefined) {
Expand All @@ -117,14 +136,35 @@ function compile(schema, root, localRefs) {
}
return 'pattern' + index;
}
}

function patternsCode(patterns) {
if (!patterns.length) return '';
var code = '';
for (var i=0; i<patterns.length; i++)
code += 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');'
return code;
}

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 refCode(i, refVal) {
return refVal[i] ? 'var refVal' + i + ' = refVal[' + i + '];' : '';
}


function _arrCode(arr, statement) {
if (!arr.length) return '';
var code = '';
for (var i=0; i<arr.length; i++)
code += statement(i, arr);
return code;
}


Expand Down
71 changes: 66 additions & 5 deletions lib/compile/resolve.js
Expand Up @@ -11,7 +11,7 @@ resolve.normalizeId = normalizeId;
resolve.fullPath = getFullPath;
resolve.url = resolveUrl;
resolve.ids = resolveIds;

resolve.inlineRef = inlineRef;

function resolve(compile, root, ref) {
/* jshint validthis: true */
Expand All @@ -23,7 +23,9 @@ function resolve(compile, root, ref) {

refVal = refVal || this._schemas[ref];
if (refVal instanceof SchemaObject)
return refVal.validate || this._compile(refVal, root);
return inlineRef(refVal.schema, this.opts.inlineRefs)
? refVal.schema
: refVal.validate || this._compile(refVal, root);

var res = _resolve.call(this, root, ref);
var schema, v;
Expand All @@ -33,11 +35,12 @@ function resolve(compile, root, ref) {
}

if (schema instanceof SchemaObject)
var v = schema.validate || compile.call(this, schema.schema, root);
v = schema.validate || compile.call(this, schema.schema, root);
else if (schema)
var v = compile.call(this, schema, root);
v = inlineRef(schema, this.opts.inlineRefs)
? schema
: compile.call(this, schema, root);

if (v && ref[0] != '#') this._refs[ref] = v;
return v;
}

Expand Down Expand Up @@ -97,6 +100,64 @@ function getJsonPointer(parsedRef, baseId, root) {
}


var SIMPLE_INLINED = util.toHash([
'type', 'format', 'pattern',
'maxLength', 'minLength',
'maxProperties', 'minProperties',
'maxItems', 'minItems',
'maximum', 'minimum',
'uniqueItems', 'multipleOf',
'required', 'enum'
]);
function inlineRef(schema, limit) {
if (limit === undefined) return checkNoRef(schema)
else if (limit) return countKeys(schema) <= limit;
}


function checkNoRef(schema) {
var item;
if (Array.isArray(schema)) {
for (var i=0; i<schema.length; i++) {
item = schema[i];
if (typeof item == 'object' && !checkNoRef(item)) return false;
}
} else {
for (var key in schema) {
if (key == '$ref') return false;
else {
item = schema[key];
if (typeof item == 'object' && !checkNoRef(item)) return false;
};
}
}
return true;
}


function countKeys(schema) {
var count = 0, item;
if (Array.isArray(schema)) {
for (var i=0; i<schema.length; i++) {
item = schema[i];
if (typeof item == 'object') count += countKeys(item);
if (count == Infinity) return Infinity;
}
} else {
for (var key in schema) {
if (key == '$ref') return Infinity;
if (SIMPLE_INLINED[key]) count++;
else {
item = schema[key];
if (typeof item == 'object') count += countKeys(item) + 1;
if (count == Infinity) return Infinity;
};
}
}
return count;
}


function unescapeFragment(str) {
return decodeURIComponent(str)
.replace(/~1/g, '/')
Expand Down
2 changes: 1 addition & 1 deletion lib/dot/definitions.def
Expand Up @@ -169,6 +169,6 @@
oneOf: "validate.schema{{=$schemaPath}}",
pattern: "{{=it.util.toQuotedString($schema)}}",
required: "validate.schema{{=$schemaPath}}",
type: "{{? $isArray }}validate.schema{{=$schemaPath}}{{??}}'{{=$typeSchema}}'{{?}}",
type: "{{? $isArray }}['{{= $typeSchema.join(\"','\") }}']{{??}}'{{=$typeSchema}}'{{?}}",
uniqueItems: "{{=$schema}}"
} #}}
20 changes: 19 additions & 1 deletion lib/dot/ref.jst
Expand Up @@ -37,7 +37,25 @@
{{??}}
{{ throw new Error($message); }}
{{?}}
{{??}}
{{?? typeof $refVal == 'string' }}
{{# def.validateRef:$refVal }}
{{??}}
{{# def.setupNextLevel }}
{{
$it.schema = $refVal.schema;
$it.schemaPath = '';
}}
{{ var $code = it.validate($it); }}
{{? /validate\.schema/.test($code) }}
var rootSchema{{=$it.level}} = validate.schema;
validate.schema = {{=$refVal.code}};
{{= $code }}
validate.schema = rootSchema{{=$it.level}};
{{??}}
{{= $code }}
{{?}}
{{? $breakOnError}}
if (valid{{=$it.level}}) {
{{?}}
{{?}}
{{?}}
97 changes: 97 additions & 0 deletions lib/dotjs/ref.js
@@ -0,0 +1,97 @@
'use strict';
module.exports = function anonymous(it) {
var out = ' ';
var $lvl = it.level,
$dataLvl = it.dataLevel,
$schema = it.schema['$ref'],
$schemaPath = it.schemaPath + '.' + '$ref',
$breakOnError = !it.opts.allErrors;
var $data = 'data' + ($dataLvl || ''),
$valid = 'valid' + $lvl,
$errs = 'errs' + $lvl;
if ($schema == '#' || $schema == '#/') {
if (it.isRoot) {
if ($breakOnError && it.wasTop) {
out += ' if (! ' + ('validate') + '(' + ($data) + ', (dataPath || \'\')';
if (it.errorPath != '""') {
out += ' + ' + (it.errorPath);
}
out += ') ) return false; else { ';
} else {
out += ' if (! ' + ('validate') + '(' + ($data) + ', (dataPath || \'\')';
if (it.errorPath != '""') {
out += ' + ' + (it.errorPath);
}
out += ') ) { if (vErrors === null) vErrors = ' + ('validate') + '.errors; else vErrors = vErrors.concat(' + ('validate') + '.errors); errors = vErrors.length; } ';
if ($breakOnError) {
out += ' else { ';
}
}
} else {
out += ' if (! ' + ('root.refVal[0]') + '(' + ($data) + ', (dataPath || \'\')';
if (it.errorPath != '""') {
out += ' + ' + (it.errorPath);
}
out += ') ) { if (vErrors === null) vErrors = ' + ('root.refVal[0]') + '.errors; else vErrors = vErrors.concat(' + ('root.refVal[0]') + '.errors); errors = vErrors.length; } ';
if ($breakOnError) {
out += ' else { ';
}
}
} else {
var $refVal = it.resolveRef(it.baseId, $schema, it.isRoot);
if ($refVal === undefined) {
var $message = 'can\'t resolve reference ' + $schema + ' from id ' + it.baseId;
if (it.opts.missingRefs == 'fail') {
console.log($message);
if (it.wasTop && $breakOnError) {
out += ' validate.errors = [ { keyword: \'' + ('$ref') + '\', dataPath: (dataPath || \'\') + ' + (it.errorPath) + ', message: \'can\\\'t resolve reference ' + (it.util.escapeQuotes($schema)) + '\' ';
if (it.opts.verbose) {
out += ', schema: ' + (it.util.toQuotedString($schema)) + ', data: ' + ($data);
}
out += ' }]; return false; ';
} else {
out += ' var err = { keyword: \'' + ('$ref') + '\', dataPath: (dataPath || \'\') + ' + (it.errorPath) + ', message: \'can\\\'t resolve reference ' + (it.util.escapeQuotes($schema)) + '\' ';
if (it.opts.verbose) {
out += ', schema: ' + (it.util.toQuotedString($schema)) + ', data: ' + ($data);
}
out += ' }; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; ';
}
if ($breakOnError) {
out += ' if (false) { ';
}
} else if (it.opts.missingRefs == 'ignore') {
console.log($message);
if ($breakOnError) {
out += ' if (true) { ';
}
} else {
throw new Error($message);
}
} else if (typeof $refVal == 'string') {
out += ' if (! ' + ($refVal) + '(' + ($data) + ', (dataPath || \'\')';
if (it.errorPath != '""') {
out += ' + ' + (it.errorPath);
}
out += ') ) { if (vErrors === null) vErrors = ' + ($refVal) + '.errors; else vErrors = vErrors.concat(' + ($refVal) + '.errors); errors = vErrors.length; } ';
if ($breakOnError) {
out += ' else { ';
}
} else {
var $it = it.util.copy(it),
$closingBraces = '';
$it.level++;
$it.schema = $refVal.schema;
$it.schemaPath = '';
var $code = it.validate($it);
if (/validate\.schema/.test($code)) {
out += ' var rootSchema' + ($it.level) + ' = validate.schema; validate.schema = ' + ($refVal.code) + '; ' + ($code) + ' validate.schema = rootSchema' + ($it.level) + '; ';
} else {
out += ' ' + ($code) + ' ';
}
if ($breakOnError) {
out += ' if (valid' + ($it.level) + ') { ';
}
}
}
return out;
}

0 comments on commit 44e1af1

Please sign in to comment.