"type" rule passes all tests

master
Evgeny Poberezkin 2015-05-26 02:11:36 +01:00
parent 7dd2218fd4
commit 8902a1bcfe
13 changed files with 333 additions and 79 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "JSON-Schema-Test-Suite"]
path = JSON-Schema-Test-Suite
url = https://github.com/epoberezkin/JSON-Schema-Test-Suite

@ -0,0 +1 @@
Subproject commit d319afab25afa93143623a8a395d9cd7211faa33

View File

@ -1 +0,0 @@
module.exports = require('./lib/jv');

View File

@ -1,15 +1,23 @@
'use strict';
var doT = require('dot');
var doT = require('dot')
, fs = require('fs');
var RULES = require('./rules')
, validateTemplateStr = fs.readFileSync(__dirname + '/validate.dot.js')
, validateTemplate = doT.compile(validateTemplateStr);
module.exports = compile;
function compileSchema(schema) {
var self = this; // jv instance
function compile(schema) {
var validateCode = validateTemplate.call(this, { schema: schema, RULES: RULES });
// console.log('\n\n\n *** \n', validateCode);
var validate;
eval(validateCode);
return {
validate: function(data, opts) { return true; },
validate: validate,
schema: schema,
errors: []
};
}
}

View File

@ -0,0 +1,88 @@
'use strict';
var fs = require('fs')
, doT = require('dot');
var RULES = module.exports = {
type: { code: fs.readFileSync(__dirname + '/type.dot.js') },
enum: { code: '' },
allOf: { code: '' },
anyOf: { code: '' },
oneOf: { code: '' },
not: { code: '' },
maximum: {
code: '',
type: 'number'
},
minimum: {
code: '',
type: 'number'
},
multipleOf: {
code: '',
type: 'number'
},
maxLength: {
code: '',
type: 'string'
},
minLength: {
code: '',
type: 'string'
},
pattern: {
code: '',
type: 'string'
},
additionalItems: {
code: '',
type: 'array'
},
items: {
code: '',
type: 'array'
},
maxItems: {
code: '',
type: 'array'
},
minItems: {
code: '',
type: 'array'
},
uniqueItems: {
code: '',
type: 'array'
},
maxProperties: {
code: '',
type: 'object'
},
minProperties: {
code: '',
type: 'object'
},
required: {
code: '',
type: 'object'
},
additionalProperties: {
code: '',
type: 'object'
},
properties: {
code: '',
type: 'object'
},
patternProperties: {
code: '',
type: 'object'
},
dependencies: {
code: '',
type: 'object'
}
};
for (var r in RULES) RULES[r].code = doT.compile(RULES[r].code);

View File

@ -0,0 +1,32 @@
{{
var $schema = it.schema;
var $isArray = Array.isArray($schema);
if ($isArray && $schema.length == 1) {
$schema = $schema[0];
$isArray = false;
}
}}
{{? $isArray }}
{{? $schema.indexOf('integer') >= 0 }}
valid = {{~ $schema:$t:i }}
{{? $t != 'integer' }}
{{? $notFirst }} || {{?}}
{{ var $notFirst = true; }}
dataType == '{{=$t}}'
{{?}}
{{~}};
if (!valid) valid = dataType == 'number' && data === parseInt(data);
{{??}}
valid = {{~ $schema:$t:i }}
{{? i }} || {{?}}
dataType == '{{=$t}}'
{{~}};
{{?}}
{{??}}
valid = dataType == {{? $schema == 'integer' }}
'number' && data === parseInt(data)
{{??}}
'{{= $schema }}'
{{?}};
{{?}}

View File

@ -0,0 +1,57 @@
'use strict';
validate = function(data) {
var types = [], errors = [], valid, error;
{{ var $paths = []; /* do I need it? */ }}
{{ var $opts = this.opts; }}
{{ (function $generateValidator($schema) { }}
var dataType = typeof data;
if (dataType == 'object') {
if (Array.isArray(data)) dataType = 'array';
else if (data == null) dataType = 'null';
}
types.push(dataType);
{{ var $schemaKeys = Object.keys($schema); }}
{{ /* sort keys so that those that are easier to fail (e.g. type) are validated sooner */ }}
{{~ $schemaKeys:$key }}
{{ var $rule = it.RULES[$key]; }}
{{? $rule }}
var rule = RULES.{{=$key}};
/* check if rule applies to data type */
if ( !rule.type || rule.type == dataType ) {
{{ $paths.push($key); /* do I need it? */ }}
{{= $rule.code({ schema: $schema[$key], generate: $generateValidator }) }}
if (!valid) {
{{? $opts.allErrors }}
errors.push(error);
{{??}}
return {
valid: false,
errors: [error]
{{? $opts.verbose }}
, data: data
{{?}}
};
{{?}}
}
{{ $paths.pop(); /* do I need it? */ }}
{{ /* reporting? */ }}
}
{{?}}
{{~}}
{{ })(it.schema); }}
return {
valid: {{? $opts.allErrors }}!errors.length{{??}}true{{?}},
errors: errors
};
};

133
lib/jv.js
View File

@ -12,90 +12,81 @@ module.exports = Jv;
* @return {Object} jv instance
*/
function Jv(opts) {
if (!(this instanceof Jv) return new Jv(opts);
this._opts = opts || {};
if (!(this instanceof Jv)) return new Jv(opts);
var self = this;
this.opts = opts || {};
this._schemas = {};
this._byJson = {};
}
Jv.prototype.addSchema = addSchema;
Jv.prototype.getSchema = getSchema;
Jv.prototype.validate = validate;
Jv.prototype.validator = validator;
// this is done on purpose, so that methods are bound to the instance
// (without using bind) so that they can be used without the instance
this.validate = validate;
this.compile = compile;
this.addSchema = addSchema;
this.getSchema = getSchema;
/**
* Adds schema to the instance.
* @param {String|Object|Array} schema schema or array of schemas. If array is passed, `name` will be ignored.
* @param {String} name Optional schema name. Will be used in addition to `schema.id` to find schema by `$ref`.
* @return {Object} compiled schema with method `validate` that accepts parameters `data` and `opts`.
*/
function addSchema(schema, name) {
if (Array.isArray(schema)) return schema.map(addSchema, this);
name = name || schema.id;
if (!name) throw new Error('no schema name');
if (this._schemas[name] || this._schemas[schema.id])
throw new Error('schema already exists');
var compiled = _addSchema.call(this, schema);
this._schemas[name] = compiled;
if (name != schema.id) this._schemas[schema.id] = compiled;
return compiled;
}
/**
* Validate data using schema
* Schema will be compiled and cached (using serialized JSON as key. [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used to serialize.
* @param {String|Object} schema
* @param {Any} data to be validated
* @return {Boolean} validation result. Errors from the last validation will be available in `jv.errors` (and also in compiled schema: `schema.errors`).
*/
function validate(schema, data) {
var compiled = _addSchema(schema);
return compiled.validate(data);
}
/**
* Get schema from the instance by `name` or by `id`
* @param {String} name `schema.id` or schema `name` that was passed to `addSchema` (schema will be availbale by id even if the name was passed).
* @return {Object} compiled schema with property `schema` and method `validate`.
*/
function getSchema(name) {
return this._schemas[name];
}
/**
* Create validator for passed schema.
* @param {String|Object} schema
* @return {Object} validation result { valid: true/false, errors: [...] }
*/
function compile(schema) {
var compiled = _addSchema(schema);
return function validate(data) {
return compiled.validate(data);
};
}
function _addSchema(schema) {
if (typeof schema == 'string') schema = JSON.parse(schema);
if (typeof schema != 'object') throw new Error('schema has invalid type');
var str = stableStringify(schema);
return (this._byJson[str] = this._byJson[str] || compileSchema.call(this, schema));
}
/**
* Adds schema to the instance.
* @param {String|Object|Array} schema schema or array of schemas. If array is passed, `name` will be ignored.
* @param {String} id Optional schema id. Will be used in addition to `schema.id` to find schema by `$ref`.
* @return {Object} compiled schema with method `validate` that accepts `data`.
*/
function addSchema(schema, id) {
if (Array.isArray(schema)) return schema.map(addSchema);
if (!id && !schema.id) throw new Error('no schema id');
if (self._schemas[id] || self._schemas[schema.id])
throw new Error('schema already exists');
var compiled = _addSchema(schema);
self._schemas[id] = compiled;
self._schemas[schema.id] = compiled;
return compiled;
}
/**
* Validate data using schema
* Schema will be compiled and cached (using serialized JSON as key. [json-stable-stringify](https://github.com/substack/json-stable-stringify) is used to serialize.
* @param {Any} data to be validated
* @param {String|Object} schema
* @param {Object} opts optional options that will extend the options passed to the instance.
* @return {Boolean} validation result. Errors from the last validation will be available in `jv.errors` (and also in compiled schema: `schema.errors`).
*/
function validate(data, schema, opts) {
var compiled = _addSchema.call(this, schema);
var _opts = _getOpts.call(this, opts);
return compiled.validate(data, _opts );
}
/**
* Get schema from the instance by by `id`
* @param {String} id `schema.id` or `id` that was passed to `addSchema` (schema will be availbale by its internal id even if the id was passed).
* @return {Object} compiled schema with property `schema` and method `validate`.
*/
function getSchema(id) {
return self._schemas[id];
}
/**
* Create validator function for passed schema and options.
* @param {String|Object} schema
* @param {[type]} opts Optional options
* @return {[type]} validation result. Errors from the last validation will be available at `v.errors` where `v` is the created validator function.
*/
function validator(schema, opts) {
var compiled = _addSchema.call(this, schema);
var _opts = _getOpts.call(this, opts);
var self = this;
return function v(data) {
var result = compiled.validate(data, _opts);
v.errors = compiled.errors;
return result;
};
}
function _getOpts(opts) {
return opts ? copy(opts, copy(this._opts)) : this._opts;
function _addSchema(schema) {
if (typeof schema == 'string') schema = JSON.parse(schema);
if (typeof schema != 'object') throw new Error('schema has invalid type');
var str = stableStringify(schema);
return (self._byJson[str] = self._byJson[str] || compileSchema.call(self, schema));
}
}

View File

@ -2,7 +2,7 @@
"name": "jv",
"version": "0.0.1",
"description": "JSON schema validator",
"main": "index.js",
"main": "lib/jv.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -24,5 +24,9 @@
"dependencies": {
"dot": "^1.0.3",
"json-stable-stringify": "^1.0.0"
},
"devDependencies": {
"JSON-Schema-Test-Suite": "git+ssh://git@github.com/json-schema/JSON-Schema-Test-Suite.git",
"mocha": "^2.2.5"
}
}

10
spec/compile.spec.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
var compileSchema = require('../lib/compile');
var mockInstance = { opts: {} };
describe('Schema compilation', function() {
it('works', function() {
compileSchema.call(mockInstance, { type: 'string' });
});
});

58
spec/json-schema.spec.js Normal file
View File

@ -0,0 +1,58 @@
'use strict';
var fs = require('fs')
, path = require('path')
, assert = require('assert')
, TESTS_PATH = 'JSON-Schema-Test-Suite/tests/draft4/';
var ONLY_RULES;
ONLY_RULES = ['type'];
var jv = require('../lib/jv')();
describe.only('JSON-Schema tests', function (done) {
var testsPath = path.join(__dirname, '..', TESTS_PATH);
var files = getTestFilesRecursive(testsPath);
files.forEach(function (file) {
if (ONLY_RULES && ONLY_RULES.indexOf(file.name) == -1) return;
describe(file.name, function() {
var testSets = require(file.path);
testSets.forEach(function (testSet) {
describe(testSet.description, function() {
var validate;
before(function() {
validate = jv.compile(testSet.schema);
});
testSet.tests.forEach(function (test) {
// if (test.description != 'an integer is valid') return;
it(test.description, function() {
var result = validate(test.data);
assert.equal(result.valid, test.valid);
});
});
});
});
});
});
});
function getTestFilesRecursive(rootPath) {
var list = fs.readdirSync(rootPath);
var files = [];
list.forEach(function (item) {
var itemPath = path.join(rootPath, item);
var stat = fs.statSync(itemPath);
if (stat.isFile()) files.push({ name: path.basename(item, '.json'), path: itemPath });
else if (stat.isDirectory()) {
var _files = getTestFilesRecursive(itemPath);
_files.forEach(function (f) {
files.push({ name: path.join(item, f.name), path: f.path })
});
}
});
return files;
}

3
spec/jv.spec.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';

View File