├── .travis.yml ├── test └── fixtures │ ├── format │ ├── fail.json │ ├── pass.json │ └── schema.json │ ├── object_props │ ├── fail_c.json │ ├── fail_a.json │ ├── fail_b.json │ ├── pass_a.json │ ├── pass_b.json │ ├── pass_c.json │ └── schema.json │ ├── bulk │ ├── pass.json │ ├── fail.json │ └── schema │ │ ├── alpha.json │ │ ├── beta.json │ │ └── schema.json │ ├── remote │ ├── pass.json │ ├── fail.json │ └── schema │ │ ├── alpha.json │ │ ├── beta.json │ │ └── schema.json │ └── subError │ ├── pass.json │ ├── fail.json │ ├── fail_deeper.json │ └── schema.json ├── .gitignore ├── .npmignore ├── .jshintrc ├── LICENSE-MIT ├── package.json ├── lib ├── loader.js └── runner.js ├── tasks └── tv4.js ├── README.md └── Gruntfile.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /test/fixtures/format/fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "dateProp": "01-01-2000" 3 | } -------------------------------------------------------------------------------- /test/fixtures/format/pass.json: -------------------------------------------------------------------------------- 1 | { 2 | "dateProp": "2000-01-01" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | 5 | /.idea 6 | /*.tgz 7 | -------------------------------------------------------------------------------- /test/fixtures/object_props/fail_c.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": {}, 3 | "stringProp": [] 4 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/fail_a.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": "nope", 3 | "stringProp": 0 4 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/fail_b.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": false, 3 | "stringProp": null 4 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/pass_a.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": 1, 3 | "stringProp": "one" 4 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/pass_b.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": 2, 3 | "stringProp": "two" 4 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/pass_c.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": 3, 3 | "stringProp": "three" 4 | } -------------------------------------------------------------------------------- /test/fixtures/bulk/pass.json: -------------------------------------------------------------------------------- 1 | { 2 | "alphaType": { 3 | "betaType": { 4 | "intProp": 1, 5 | "stringProp": "one" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/remote/pass.json: -------------------------------------------------------------------------------- 1 | { 2 | "alphaType": { 3 | "betaType": { 4 | "intProp": 1, 5 | "stringProp": "one" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/bulk/fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "alphaType": { 3 | "betaType": { 4 | "intProp": "Hello!", 5 | "stringProp": 3210 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/remote/fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "alphaType": { 3 | "betaType": { 4 | "intProp": "Hello!", 5 | "stringProp": 3210 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/fixtures/bulk/schema/alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "alpha", 3 | "properties": { 4 | "required" : ["betaType"], 5 | "betaType": { 6 | "$ref": "beta" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | .gitmodules 4 | .travis.yml 5 | .jshintrc 6 | bower.json 7 | Gruntfile.js 8 | 9 | /.idea 10 | /node_modules 11 | /test 12 | /*.tgz 13 | /*.txt 14 | -------------------------------------------------------------------------------- /test/fixtures/format/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "formatSchema", 3 | "type": "object", 4 | "required" : ["dateProp"], 5 | "properties": { 6 | "dateProp": { 7 | "format": "date" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/remote/schema/alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required" : ["betaType"], 4 | "properties": { 5 | "betaType": { 6 | "$ref": "http://localhost:9090/remote/schema/beta.json" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test/fixtures/bulk/schema/beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "beta", 3 | "properties": { 4 | "required" : ["intProp", "stringProp"], 5 | "intProp": { 6 | "type": "integer" 7 | }, 8 | "stringProp": { 9 | "type": "string" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/remote/schema/beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required" : ["intProp", "stringProp"], 4 | "properties": { 5 | "intProp": { 6 | "type": "integer" 7 | }, 8 | "stringProp": { 9 | "type": "string" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/object_props/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "object_props", 3 | "type": "object", 4 | "required" : ["intProp", "stringProp"], 5 | "properties": { 6 | "intProp": { 7 | "type": "integer" 8 | }, 9 | "stringProp": { 10 | "type": "string" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/fixtures/subError/pass.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": 1, 3 | "stringProp": "aa1", 4 | "shallowArray": [ 5 | 2, 6 | "bb2" 7 | ], 8 | "deepArray": [ 9 | 3, 10 | "cc3", 11 | [ 12 | 4, 13 | "dd4", 14 | [ 15 | 5, 16 | "ee5" 17 | ] 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /test/fixtures/remote/schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://localhost:9090/remote/schema/schema.json", 3 | "type": "object", 4 | "required" : ["alphaType"], 5 | "properties": { 6 | "alphaType": { 7 | "$ref": "http://localhost:9090/remote/schema/alpha.json" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/subError/fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": false, 3 | "stringProp": null, 4 | "shallowArray": [ 5 | false, 6 | [ 7 | false 8 | ] 9 | ], 10 | "deepArray": [ 11 | 3, 12 | "cc3", 13 | false, 14 | [ 15 | false, 16 | [ 17 | false 18 | ] 19 | ] 20 | ] 21 | } -------------------------------------------------------------------------------- /test/fixtures/subError/fail_deeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "intProp": false, 3 | "stringProp": null, 4 | "shallowArray": [ 5 | false, 6 | true 7 | ], 8 | "deepArray": [ 9 | 3, 10 | "cc3", 11 | [ 12 | 4, 13 | "dd4", 14 | [ 15 | 5, 16 | [ 17 | 6, 18 | false 19 | ] 20 | ] 21 | ] 22 | ] 23 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "smarttabs": true, 14 | "node": true, 15 | "browser": true, 16 | "globals": { 17 | "describe": true, 18 | "it": true, 19 | "tv4": true, 20 | "define": true, 21 | "chai": true, 22 | "assert": true 23 | } 24 | } -------------------------------------------------------------------------------- /test/fixtures/bulk/schema/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["alphaType"], 4 | "properties": { 5 | "alphaType": { 6 | "$ref": "#/definitions/alpha" 7 | } 8 | }, 9 | "definitions": { 10 | "alpha": { 11 | "type": "object", 12 | "required": ["betaType"], 13 | "properties": { 14 | "required": ["betaType"], 15 | "betaType": { 16 | "$ref": "#/definitions/beta" 17 | } 18 | } 19 | }, 20 | "beta": { 21 | "type": "object", 22 | "required": ["intProp", "stringProp"], 23 | "properties": { 24 | "intProp": { 25 | "type": "integer" 26 | }, 27 | "stringProp": { 28 | "type": "string" 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /test/fixtures/subError/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "required": ["intProp", "stringProp", "shallowArray", "deepArray"], 4 | "properties": { 5 | "stringProp": { 6 | "$ref": "#/definitions/multiType" 7 | }, 8 | "intProp": { 9 | "$ref": "#/definitions/multiType" 10 | }, 11 | "shallowArray": { 12 | "$ref": "#/definitions/multiType" 13 | }, 14 | "deepArray": { 15 | "$ref": "#/definitions/multiType" 16 | } 17 | }, 18 | "definitions": { 19 | "multiType": { 20 | "oneOf": [ 21 | { 22 | "type": "string" 23 | }, 24 | { 25 | "type": "number" 26 | }, 27 | { 28 | "type": "array", 29 | "items": { 30 | "$ref": "#/definitions/multiType" 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Bart van der Schoor 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-tv4", 3 | "description": "Validate values against json-schema v4", 4 | "version": "0.4.0", 5 | "homepage": "https://github.com/Bartvds/grunt-tv4", 6 | "author": { 7 | "name": "Bart van der Schoor", 8 | "url": "https://github.com/Bartvds" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Bartvds/grunt-tv4" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/Bartvds/grunt-tv4/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/Bartvds/grunt-tv4/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "keywords": [ 24 | "gruntplugin", 25 | "validate", 26 | "validator", 27 | "json", 28 | "schema", 29 | "json-schema", 30 | "tv4" 31 | ], 32 | "main": "Gruntfile.js", 33 | "engines": { 34 | "node": ">= 0.10.0" 35 | }, 36 | "scripts": { 37 | "test": "grunt test" 38 | }, 39 | "dependencies": { 40 | "jsonpointer.js": "0.3.0", 41 | "request": "2.25.0", 42 | "tv4": "~1.0.14", 43 | "ministyle": "~0.1.0", 44 | "miniwrite": "~0.1.0", 45 | "tv4-reporter": "~0.0.3" 46 | }, 47 | "devDependencies": { 48 | "grunt-cli": "~0.1", 49 | "grunt": "~0.4.1", 50 | "grunt-contrib-connect": "~0.3.0", 51 | "grunt-continue": "0.0.1", 52 | "grunt-contrib-jshint": "~0.7.2", 53 | "jshint-path-reporter": "~0.1.3", 54 | "package.json-schema": "~0.1.2" 55 | }, 56 | "peerDependencies": { 57 | "grunt": "~0.4.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var fs = require('fs'); 3 | 4 | function loadHTTP(uri, options, callback) { 5 | request.get({url: uri, timeout: options ? options.timeout : 5000}, function (err, res) { 6 | if (err) { 7 | return callback(err); 8 | } 9 | if (!res || !res.body) { 10 | return callback(new Error('empty response at: ' + uri)); 11 | } 12 | if (res.statusCode < 200 || res.statusCode >= 400) { 13 | return callback(new Error('http bad response code: ' + res.statusCode + ' on ' + uri)); 14 | } 15 | var value; 16 | try { 17 | value = JSON.parse(res.body); 18 | } 19 | catch (e) { 20 | return callback(e); 21 | } 22 | callback(null, value); 23 | }); 24 | } 25 | 26 | function loadPath(src, options, callback) { 27 | fs.readFile(src, 'utf8', function (err, str) { 28 | if (err) { 29 | return callback(err); 30 | } 31 | var value; 32 | try { 33 | value = JSON.parse(str); 34 | } 35 | catch (e) { 36 | return callback(e); 37 | } 38 | callback(null, value); 39 | }); 40 | } 41 | 42 | function getLoaders() { 43 | var scope = {}; 44 | scope.loadHTTP = loadHTTP; 45 | scope.loadPath = loadPath; 46 | 47 | scope.loadFile = function (uri, options, callback) { 48 | scope.loadPath(uri, options, callback); 49 | }; 50 | scope.load = function (uri, options, callback) { 51 | if (/^https?:/.test(uri)) { 52 | scope.loadHTTP(uri, options, callback); 53 | return; 54 | } 55 | else if (/^file?:/.test(uri)) { 56 | scope.loadFile(uri, options, callback); 57 | return; 58 | } 59 | else { 60 | scope.loadPath(uri, options, callback); 61 | return; 62 | } 63 | }; 64 | return scope; 65 | } 66 | 67 | module.exports = { 68 | getLoaders: getLoaders 69 | }; 70 | -------------------------------------------------------------------------------- /tasks/tv4.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-tv4 3 | * https://github.com/Bartvds/grunt-tv4 4 | * 5 | * Copyright (c) 2013 Bart van der Schoor 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var taskTv4 = require('tv4').freshApi(); 12 | var reporter = require('tv4-reporter'); 13 | var ministyle = require('ministyle'); 14 | var miniwrite = require('miniwrite'); 15 | var loader = require('../lib/loader'); 16 | var runnerModule = require('../lib/runner'); 17 | 18 | //var util = require('util'); 19 | 20 | module.exports = function (grunt) { 21 | 22 | var out = miniwrite.grunt(grunt); 23 | var style = ministyle.grunt(); 24 | var report = reporter.getReporter(out, style); 25 | var runner = runnerModule.getRunner(taskTv4, loader.getLoaders(), out, style); 26 | 27 | grunt.registerMultiTask('tv4', 'Validate values against json-schema v4.', function () { 28 | var done = this.async(); 29 | 30 | //import options 31 | var context = runner.getContext(this.options(runner.getOptions({ 32 | timeout: 5000 33 | }))); 34 | var objects = []; 35 | 36 | if (context.options.fresh) { 37 | context.tv4 = taskTv4.freshApi(); 38 | } 39 | else { 40 | context.tv4 = taskTv4; 41 | } 42 | 43 | grunt.util._.each(context.options.schemas, function (schema, uri) { 44 | context.tv4.addSchema(uri, schema); 45 | }); 46 | 47 | context.tv4.addFormat(context.options.formats); 48 | 49 | grunt.util._.each(context.options.languages, function (language, id) { 50 | context.tv4.addLanguage(id, language); 51 | }); 52 | if (context.options.language) { 53 | context.tv4.language(context.options.language); 54 | } 55 | 56 | //flatten list for sanity 57 | grunt.util._.each(this.files, function (f) { 58 | grunt.util._.some(f.src, function (filePath) { 59 | if (!grunt.file.exists(filePath)) { 60 | grunt.log.warn('file "' + filePath + '" not found.'); 61 | return true; 62 | } 63 | objects.push({ 64 | path: filePath, 65 | label: filePath, 66 | root: context.options.root 67 | }); 68 | }); 69 | }); 70 | 71 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 72 | 73 | var values = this.data.values; 74 | if (typeof values === 'function') { 75 | values = values(); 76 | } 77 | 78 | if (typeof values === 'object') { 79 | var keyPrefix = (Array.isArray(values) ? 'value #' : ''); 80 | grunt.util._.some(values, function (value, key) { 81 | objects.push({ 82 | label: keyPrefix + key, 83 | value: value, 84 | root: context.options.root 85 | }); 86 | }); 87 | } 88 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 89 | 90 | if (context.options.add && Array.isArray(context.options.add)) { 91 | grunt.util._.some(context.options.add, function (schema) { 92 | if (typeof schema === 'string') { 93 | //juggle 94 | schema = grunt.file.readJSON(schema); 95 | } 96 | if (typeof schema.id === 'undefined') { 97 | grunt.log.warn('options.add: schema missing required id field (use options.schema to map it manually)'); 98 | grunt.log.writeln(); 99 | context.done(false); 100 | return true; 101 | } 102 | context.tv4.addSchema(schema.id, schema); 103 | }); 104 | } 105 | 106 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 107 | 108 | //grunt.verbose.writeln(util.inspect(context)); 109 | 110 | context.validate(objects, function (err, job) { 111 | if (err) { 112 | throw err; 113 | } 114 | if (job) { 115 | report.reportBulk(job.failed, job.passed); 116 | if (!job.success) { 117 | grunt.log.writeln(''); 118 | } 119 | done(job.success); 120 | } 121 | else { 122 | done(false); 123 | } 124 | }); 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-tv4 2 | 3 | [![Build Status](https://secure.travis-ci.org/Bartvds/grunt-tv4.png?branch=master)](http://travis-ci.org/Bartvds/grunt-tv4) [![Dependency Status](https://gemnasium.com/Bartvds/grunt-tv4.png)](https://gemnasium.com/Bartvds/grunt-tv4) [![NPM version](https://badge.fury.io/js/grunt-tv4.png)](http://badge.fury.io/js/grunt-tv4) 4 | 5 | > Use grunt and [Tiny Validator tv4](https://github.com/geraintluff/tv4) to validate values against [json-schema](http://json-schema.org/) draft v4 6 | 7 | ## Getting Started 8 | 9 | This plugin requires Grunt `~0.4.1` 10 | 11 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 12 | 13 | ```shell 14 | $ npm install grunt-tv4 --save-dev 15 | ``` 16 | 17 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 18 | 19 | ```js 20 | grunt.loadNpmTasks('grunt-tv4'); 21 | ``` 22 | 23 | ## The "tv4" task 24 | 25 | ### Notes 26 | 27 | * Uses [Tiny Validator tv4 ](https://github.com/geraintluff/tv4) so schemas must conform to [json-schema draft v4](http://json-schema.org/documentation.html). 28 | * Supports automatically resolution and loading remote references by http via `$ref`. 29 | * To use `$ref` see the [json-schema](http://json-schema.org/) documentation or [this help](http://spacetelescope.github.io/understanding-json-schema/structuring.html). 30 | * For general help with json-schema see this excelent [guide](http://spacetelescope.github.io/understanding-json-schema/) and usable [reference](http://spacetelescope.github.io/understanding-json-schema/reference/index.html). 31 | * Errors formatted by the [tv4-reporter](https://github.com/Bartvds/tv4-reporter) library. 32 | 33 | ### API change 34 | 35 | As of version `v0.2.0` the API was changed to follow the Grunt options- and file-selection conventions. The old pattern (which abused the destination-specifier) is no longer supported. The readme for the previous API can be found [here](https://github.com/Bartvds/grunt-tv4/tree/71ef1726945d05efd5daca29f26cbf4ab09c858e). 36 | 37 | The root schema must now to be specified as `options.root`. 38 | 39 | ### Example 40 | 41 | * Demo of version `v0.3.0` output on [travis-ci](https://travis-ci.org/Bartvds/grunt-tv4/jobs/14468920) 42 | 43 | ### Basic usage 44 | 45 | Validate from .json files: 46 | 47 | ```js 48 | grunt.initConfig({ 49 | tv4: { 50 | options: { 51 | root: grunt.file.readJSON('schema/main.json') 52 | } 53 | myTarget: { 54 | src: ['data/*.json'] 55 | } 56 | } 57 | }) 58 | ``` 59 | 60 | Valdiate values: 61 | 62 | ```js 63 | grunt.initConfig({ 64 | tv4: { 65 | myTarget: { 66 | options: { 67 | root: { 68 | type: 'object', 69 | properties: { ... } 70 | } 71 | }, 72 | values: [ 73 | { ... }, 74 | { ... } 75 | ] 76 | } 77 | } 78 | }) 79 | ```` 80 | 81 | ### Advanced usage 82 | 83 | ```js 84 | grunt.initConfig({ 85 | tv4: { 86 | options: { 87 | // specify the main schema, one of: 88 | // - path to json 89 | // - http-url 90 | // - schema object 91 | // - callback that returns one of the above 92 | root: grunt.file.readJSON('schema/main.json'), 93 | 94 | // show multiple errors per file (off by default) 95 | multi: true, 96 | 97 | // create a new tv4 instance for every target (off by default) 98 | fresh: true, 99 | 100 | // add schemas in bulk (each required to have an 'id' property) (can be a callback) 101 | add: [ 102 | grunt.file.readJSON('schema/apple.json'), 103 | grunt.file.readJSON('schema/pear.json') 104 | ], 105 | 106 | // set schemas by URI (can be a callback) 107 | schemas: { 108 | 'http://example.com/schema/apple': grunt.file.readJSON('schema/apple.json'), 109 | 'http://example.com/schema/pear': grunt.file.readJSON('schema/pear.json') 110 | }, 111 | 112 | // map of custom formats passed to tv4.addFormat() 113 | formats: { 114 | date: function (data, schema) { 115 | if (typeof data !== 'string' || !dateRegex.test(data)) { 116 | return 'value must be string of the form: YYYY-MM-DD'; 117 | } 118 | return null; 119 | } 120 | }, 121 | 122 | // passed to tv4.validate() 123 | checkRecursive: false 124 | // passed to tv4.validate() 125 | banUnknownProperties: false 126 | // passed tv4.language() 127 | language: 'de' 128 | // passed tv4.addLanguage() 129 | languages: { 130 | 'de': { ... } 131 | } 132 | }, 133 | // load json from disk 134 | myFiles: { 135 | src: ['data/*.json', 'data/fruit/**/*.json'] 136 | }, 137 | 138 | myValues: { 139 | // validate values 140 | values: [ 141 | grunt.file.readJSON('data/apple.json'), 142 | grunt.file.readJSON('data/pear.json') 143 | ], 144 | }, 145 | 146 | myValueMap: { 147 | // alternately pass as object and the keys will be used as labels in the reports 148 | values: { 149 | 'apple': grunt.file.readJSON('data/apple.json'), 150 | 'pear': grunt.file.readJSON('data/pear.json') 151 | }, 152 | }, 153 | 154 | myCallback: { 155 | // alternately pass a function() to return a collection of values (array or object) 156 | values: function() { 157 | return { 158 | 'apple': grunt.file.readJSON('data/apple.json'), 159 | 'pear': grunt.file.readJSON('data/pear.json') 160 | } 161 | } 162 | } 163 | } 164 | }) 165 | ``` 166 | 167 | ## History 168 | 169 | * 0.4.0 - Updated some depedencies. `root`, `add` and `schemas` can be a callback function (for lazy init). 170 | * 0.3.0 - Big internal rewrite: 171 | * Added `.values` option. 172 | * Extracted reporting to [tv4-reporter](https://github.com/Bartvds/tv4-reporter), [miniwrite](https://github.com/Bartvds/miniwrite) and [ministyle](https://github.com/Bartvds/ministyle). 173 | * Moved loader logic to own stand-alone module (for later extraction) 174 | * Extracted test-running logic to own module (for later extraction) 175 | * 0.2.1 - Added support to report subErrors (for anyOf/oneOf) 176 | * 0.2.0 - Updated to follow the Grunt conventions. 177 | * 0.1.4 - Updated `tv4` to version `1.0.11` 178 | * Added support for `tv4.addFormat()` / `languages` / `checkRecursive` / `banUnknownProperties`. 179 | * 0.1.3 - Support for loading remote references (JSON Schema's `$ref`). 180 | * 0.1.1 - Bugfixes and improved reporting 181 | * 0.1.0 - First release with synchronous validation 182 | 183 | 184 | ## Contributing 185 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 186 | 187 | 188 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/Bartvds/grunt-tv4/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 189 | 190 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-tv4 3 | * https://github.com/Bartvds/grunt-tv4 4 | * 5 | * Copyright (c) 2013 Bart van der Schoor 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | /*jshint unused:false*/ 13 | 14 | grunt.loadNpmTasks('grunt-contrib-jshint'); 15 | grunt.loadNpmTasks('grunt-contrib-connect'); 16 | grunt.loadNpmTasks('grunt-continue'); 17 | 18 | grunt.loadTasks('tasks'); 19 | 20 | var util = require('util'); 21 | 22 | //used by format checker 23 | var dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; 24 | var dateValidateCallback = function (data, schema) { 25 | if (typeof data !== 'string' || !dateRegex.test(data)) { 26 | // return error message 27 | return 'value must be string of the form: YYYY-MM-DD'; 28 | } 29 | return null; 30 | }; 31 | 32 | grunt.initConfig({ 33 | jshint: { 34 | options: grunt.util._.defaults(grunt.file.readJSON('.jshintrc'), { 35 | reporter: './node_modules/jshint-path-reporter' 36 | }), 37 | all: [ 38 | 'Gruntfile.js', 39 | 'lib/**/*.js', 40 | 'tasks/**/*.js' 41 | ] 42 | }, 43 | connect: { 44 | run: { 45 | options: { 46 | port: 9090, 47 | base: 'test/fixtures/' 48 | } 49 | } 50 | }, 51 | tv4: { 52 | pass_prop: { 53 | _twice: true, 54 | options: { 55 | fresh: true, 56 | root: 'test/fixtures/object_props/schema.json' 57 | }, 58 | src: [ 59 | 'test/fixtures/object_props/pass_a.json' 60 | ] 61 | }, 62 | pass_many: { 63 | options: { 64 | fresh: true, 65 | root: 'test/fixtures/object_props/schema.json' 66 | }, 67 | src: [ 68 | 'test/fixtures/object_props/pass_a.json', 69 | 'test/fixtures/object_props/pass_b.json', 70 | 'test/fixtures/object_props/pass_c.json' 71 | ] 72 | }, 73 | pass_remote: { 74 | _twice: true, 75 | options: { 76 | fresh: true, 77 | root: 'http://localhost:9090/remote/schema/schema.json' 78 | }, 79 | src: [ 80 | 'test/fixtures/remote/pass.json', 81 | 'test/fixtures/remote/pass.json' 82 | ] 83 | }, 84 | pass_remote_local: { 85 | _twice: true, 86 | options: { 87 | fresh: true, 88 | root: 'test/fixtures/remote/schema/schema.json' 89 | }, 90 | src: [ 91 | 'test/fixtures/remote/pass.json', 92 | 'test/fixtures/remote/pass.json' 93 | ] 94 | }, 95 | fail_single: { 96 | options: { 97 | fresh: true, 98 | root: 'test/fixtures/object_props/schema.json' 99 | }, 100 | src: [ 101 | 'test/fixtures/object_props/pass_a.json', 102 | 'test/fixtures/object_props/fail_a.json' 103 | ] 104 | }, 105 | fail_multi: { 106 | options: { 107 | fresh: true, 108 | multi: true, 109 | root: 'test/fixtures/object_props/schema.json' 110 | }, 111 | src: [ 112 | 'test/fixtures/object_props/fail_a.json' 113 | ] 114 | }, 115 | fail_many_multi: { 116 | options: { 117 | fresh: true, 118 | multi: true, 119 | root: 'test/fixtures/object_props/schema.json' 120 | }, 121 | src: [ 122 | 'test/fixtures/object_props/fail_a.json', 123 | 'test/fixtures/object_props/fail_b.json', 124 | 'test/fixtures/object_props/fail_c.json' 125 | ] 126 | }, 127 | fail_remote: { 128 | options: { 129 | fresh: true, 130 | root: 'http://localhost:9090/remote/schema/schema.json' 131 | }, 132 | src: [ 133 | 'test/fixtures/remote/fail.json' 134 | ] 135 | }, 136 | fail_remoteNotFound: { 137 | options: { 138 | fresh: true, 139 | root: 'http://localhost:9090/remote/schema/non-existing.json' 140 | }, 141 | src: [ 142 | 'test/fixtures/remote/pass.json' 143 | ] 144 | }, 145 | pass_format: { 146 | options: { 147 | fresh: true, 148 | root: 'test/fixtures/format/schema.json', 149 | formats: { 150 | 'date': dateValidateCallback 151 | } 152 | }, 153 | src: [ 154 | 'test/fixtures/format/pass.json' 155 | ] 156 | }, 157 | fail_format: { 158 | options: { 159 | fresh: true, 160 | root: 'test/fixtures/format/schema.json', 161 | formats: { 162 | 'date': dateValidateCallback 163 | } 164 | }, 165 | src: [ 166 | 'test/fixtures/format/fail.json' 167 | ] 168 | }, 169 | pass_bulk: { 170 | options: { 171 | fresh: true, 172 | root: 'test/fixtures/bulk/schema/schema.json', 173 | add: [ 174 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 175 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 176 | ] 177 | }, 178 | src: [ 179 | 'test/fixtures/bulk/pass.json', 180 | 'test/fixtures/bulk/pass.json' 181 | ] 182 | }, 183 | fail_bulk: { 184 | options: { 185 | fresh: true, 186 | root: 'test/fixtures/bulk/schema/schema.json', 187 | add: [ 188 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 189 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 190 | ] 191 | }, 192 | src: [ 193 | 'test/fixtures/bulk/fail.json' 194 | ] 195 | }, 196 | pass_rootObject: { 197 | options: { 198 | fresh: true, 199 | root: grunt.file.readJSON('test/fixtures/bulk/schema/schema.json'), 200 | add: [ 201 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 202 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 203 | ] 204 | }, 205 | src: [ 206 | 'test/fixtures/bulk/pass.json', 207 | 'test/fixtures/bulk/pass.json' 208 | ] 209 | }, 210 | pass_rootObject_cb: { 211 | options: { 212 | fresh: true, 213 | root: function () { 214 | return grunt.file.readJSON('test/fixtures/bulk/schema/schema.json'); 215 | }, 216 | add: [ 217 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 218 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 219 | ] 220 | }, 221 | src: [ 222 | 'test/fixtures/bulk/pass.json', 223 | 'test/fixtures/bulk/pass.json' 224 | ] 225 | }, 226 | pass_rootObject_add_cb: { 227 | options: { 228 | fresh: true, 229 | root: grunt.file.readJSON('test/fixtures/bulk/schema/schema.json'), 230 | add: function () { 231 | return [ 232 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 233 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 234 | ]; 235 | } 236 | }, 237 | src: [ 238 | 'test/fixtures/bulk/pass.json', 239 | 'test/fixtures/bulk/pass.json' 240 | ] 241 | }, 242 | fail_rootObject: { 243 | options: { 244 | fresh: true, 245 | root: grunt.file.readJSON('test/fixtures/bulk/schema/schema.json'), 246 | add: [ 247 | grunt.file.readJSON('test/fixtures/bulk/schema/alpha.json'), 248 | grunt.file.readJSON('test/fixtures/bulk/schema/beta.json') 249 | ] 250 | }, 251 | src: [ 252 | 'test/fixtures/bulk/fail.json' 253 | ] 254 | }, 255 | pass_subError: { 256 | options: { 257 | fresh: true, 258 | root: grunt.file.readJSON('test/fixtures/subError/schema.json') 259 | }, 260 | src: [ 261 | 'test/fixtures/subError/pass.json' 262 | ] 263 | }, 264 | fail_subError: { 265 | options: { 266 | fresh: true, 267 | root: grunt.file.readJSON('test/fixtures/subError/schema.json') 268 | }, 269 | src: [ 270 | 'test/fixtures/subError/fail.json', 271 | 'test/fixtures/subError/fail_deeper.json', 272 | ] 273 | }, 274 | fail_subErrorMulti: { 275 | options: { 276 | fresh: true, 277 | multi: true, 278 | root: grunt.file.readJSON('test/fixtures/subError/schema.json') 279 | }, 280 | src: [ 281 | 'test/fixtures/subError/fail_deeper.json', 282 | ] 283 | }, 284 | pass_values: { 285 | options: { 286 | fresh: true, 287 | multi: true, 288 | root: { 289 | type: 'string' 290 | } 291 | }, 292 | values: [ 293 | 'valueAlpha', 294 | 'valueBravo' 295 | ] 296 | }, 297 | fail_valuesArray: { 298 | options: { 299 | fresh: true, 300 | multi: true, 301 | root: { 302 | type: 'string' 303 | } 304 | }, 305 | values: [ 306 | false, 307 | 123 308 | ] 309 | }, 310 | fail_valuesObject: { 311 | options: { 312 | fresh: true, 313 | multi: true, 314 | root: { 315 | type: 'string' 316 | } 317 | }, 318 | values: { 319 | 'myBooleanValue': false, 320 | 'myNumberValue': 1 321 | } 322 | }, 323 | fail_valuesCallback: { 324 | options: { 325 | fresh: true, 326 | multi: true, 327 | root: { 328 | type: 'string' 329 | } 330 | }, 331 | values: function () { 332 | return { 333 | 'callbackBoolean': false, 334 | 'callbackNumber': 1 335 | }; 336 | } 337 | }, 338 | pass_package: { 339 | options: { 340 | fresh: true, 341 | multi: false, 342 | root: require('package.json-schema').get() 343 | }, 344 | "src": ['./package.json'] 345 | } 346 | } 347 | }); 348 | 349 | //used by format checker 350 | var passNames = []; 351 | var failNames = []; 352 | var conf = grunt.config.get('tv4'); 353 | 354 | Object.keys(conf).sort().forEach(function (name) { 355 | if (/^pass_/.test(name)) { 356 | passNames.push('tv4:' + name); 357 | if (conf[name]._twice) { 358 | passNames.push('tv4:' + name); 359 | } 360 | } 361 | else if (/^fail_/.test(name)) { 362 | failNames.push('tv4:' + name); 363 | if (conf[name]._twice) { 364 | failNames.push('tv4:' + name); 365 | } 366 | } 367 | else { 368 | passNames.push('tv4:' + name); 369 | } 370 | }); 371 | 372 | grunt.registerTask('pass', passNames); 373 | grunt.registerTask('fail', failNames); 374 | 375 | grunt.registerTask('test', ['jshint', 'connect', 'pass', 'continueOn', 'fail', 'continueOff']); 376 | 377 | grunt.registerTask('dev', ['jshint', 'connect', 'tv4:fail_many_multi']); 378 | grunt.registerTask('edit_01', ['jshint', 'tv4:fail_subErrorMulti']); 379 | grunt.registerTask('edit_02', ['jshint', 'tv4:pass_values', 'tv4:fail_valuesArray', 'tv4:fail_valuesObject', 'tv4:fail_valuesCallback']); 380 | grunt.registerTask('edit_03', ['jshint', 'tv4:fail_subError']); 381 | 382 | grunt.registerTask('run', ['fail']); 383 | grunt.registerTask('dev', ['tv4:pass_package']); 384 | grunt.registerTask('default', ['test']); 385 | }; 386 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | // Bulk validation core: composites with tv4, miniwrite, ministyle and loaders 2 | 3 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 | 5 | function nextTick(call) { 6 | //lame setImmediate shimable 7 | if (typeof setImmediate === 'function') { 8 | setImmediate(call); 9 | } 10 | else if (process && typeof process.nextTick === 'function') { 11 | process.nextTick(call); 12 | } 13 | else { 14 | setTimeout(call, 1); 15 | } 16 | } 17 | 18 | // for-each async style 19 | function forAsync(items, iter, callback) { 20 | var keys = Object.keys(items); 21 | var step = function (err, callback) { 22 | nextTick(function () { 23 | if (err) { 24 | return callback(err); 25 | } 26 | if (keys.length === 0) { 27 | return callback(); 28 | } 29 | var key = keys.pop(); 30 | iter(items[key], key, function (err) { 31 | step(err, callback); 32 | }); 33 | }); 34 | }; 35 | step(null, callback); 36 | } 37 | 38 | function copyProps(target, source, recursive) { 39 | if (source) { 40 | Object.keys(source).forEach(function (key) { 41 | if (recursive && typeof source[key] === 'object') { 42 | target[key] = copyProps((Array.isArray(source[key]) ? [] : {}), source[key], recursive); 43 | return; 44 | } 45 | target[key] = source[key]; 46 | }); 47 | } 48 | return target; 49 | } 50 | 51 | function sortLabel(a, b) { 52 | if (a.label < b.label) { 53 | return 1; 54 | } 55 | if (a.label > b.label) { 56 | return -1; 57 | } 58 | // a must be equal to b 59 | return 0; 60 | } 61 | 62 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 63 | 64 | function isURL(uri) { 65 | return (/^https?:/.test(uri) || /^file:/.test(uri)); 66 | } 67 | 68 | var headExp = /^(\w+):/; 69 | 70 | function getURLProtocol(uri) { 71 | if (isURL(uri)) { 72 | headExp.lastIndex = 0; 73 | var res = headExp.exec(uri); 74 | if ((res && res.length >= 2)) { 75 | return res[1]; 76 | } 77 | } 78 | return ''; 79 | } 80 | 81 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 82 | 83 | function getOptions(merge) { 84 | var options = { 85 | root: null, 86 | schemas: {}, 87 | add: [], 88 | formats: {}, 89 | fresh: false, 90 | multi: false, 91 | timeout: 5000, 92 | checkRecursive: false, 93 | banUnknown: true, 94 | languages: {}, 95 | language: null 96 | }; 97 | return copyProps(options, merge); 98 | } 99 | 100 | function getRunner(tv4, loader, out, style) { 101 | 102 | function getContext(options) { 103 | var context = {}; 104 | context.tv4 = (options.fresh ? tv4.freshApi() : tv4); 105 | context.options = {}; 106 | 107 | //import options 108 | if (options) { 109 | context.options = getOptions(options); 110 | } 111 | 112 | if (typeof context.options.root === 'function') { 113 | context.options.root = context.options.root(); 114 | } 115 | 116 | if (typeof context.options.schemas === 'function') { 117 | context.options.schemas = context.options.schemas(); 118 | } 119 | 120 | if (typeof context.options.add === 'function') { 121 | context.options.add = context.options.add(); 122 | } 123 | 124 | // main validation method 125 | context.validate = function (objects, callback) { 126 | // retunr value 127 | var job = { 128 | context: context, 129 | total: objects.length, 130 | objects: objects, //TODO rename objects to values 131 | success: true, 132 | error: null, 133 | passed: [], 134 | failed: [] 135 | }; 136 | 137 | if (job.objects.length === 0) { 138 | job.error = new Error('zero objects selected'); 139 | finaliseTask(job.error, job, callback); 140 | return; 141 | } 142 | job.objects.sort(sortLabel); 143 | 144 | //start the flow 145 | loadSchemaList(job, job.context.tv4.getMissingUris(), function (err) { 146 | if (err) { 147 | return finaliseTask(err, job, callback); 148 | } 149 | // loop all objects 150 | forAsync(job.objects, function (object, index, callback) { 151 | validateObject(job, object, callback); 152 | 153 | }, function (err) { 154 | finaliseTask(err, job, callback); 155 | }); 156 | }); 157 | }; 158 | return context; 159 | } 160 | 161 | var repAccent = style.accent('/'); 162 | var repProto = style.accent('://'); 163 | 164 | function tweakURI(str) { 165 | return str.split(/:\/\//).map(function (str) { 166 | return str.replace(/\//g, repAccent); 167 | }).join(repProto); 168 | } 169 | 170 | function finaliseTask(err, job, callback) { 171 | job.success = (job.success && !job.error && job.failed.length === 0); 172 | if (job.error) { 173 | out.writeln(''); 174 | out.writeln(style.warning('warning: ') + job.error); 175 | out.writeln(''); 176 | callback(null, job); 177 | return; 178 | } 179 | if (err) { 180 | out.writeln(''); 181 | out.writeln(style.error('error: ') + err); 182 | out.writeln(''); 183 | callback(err, job); 184 | return; 185 | } 186 | out.writeln(''); 187 | callback(null, job); 188 | } 189 | 190 | //load and add batch of schema by uri, repeat until all missing are solved 191 | function loadSchemaList(job, uris, callback) { 192 | uris = uris.filter(function (value) { 193 | return !!value; 194 | }); 195 | 196 | if (uris.length === 0) { 197 | nextTick(function () { 198 | callback(); 199 | }); 200 | return; 201 | } 202 | 203 | var sweep = function () { 204 | if (uris.length === 0) { 205 | nextTick(callback); 206 | return; 207 | } 208 | forAsync(uris, function (uri, i, callback) { 209 | if (!uri) { 210 | out.writeln('> ' + style.error('cannot load') + ' "' + tweakURI(uri) + '"'); 211 | callback(); 212 | } 213 | out.writeln('> ' + style.accent('load') + ' + ' + tweakURI(uri)); 214 | 215 | loader.load(uri, job.context.options, function (err, schema) { 216 | if (err) { 217 | return callback(err); 218 | } 219 | job.context.tv4.addSchema(uri, schema); 220 | uris = job.context.tv4.getMissingUris(); 221 | callback(); 222 | }); 223 | }, function (err) { 224 | if (err) { 225 | job.error = err; 226 | return callback(null); 227 | } 228 | // sweep again 229 | sweep(); 230 | }); 231 | }; 232 | sweep(); 233 | } 234 | 235 | //supports automatic lazy loading 236 | function recursiveTest(job, object, callback) { 237 | var opts = job.context.options; 238 | if (job.context.options.multi) { 239 | object.result = job.context.tv4.validateMultiple(object.value, object.schema, opts.checkRecursive, opts.banUnknown); 240 | } 241 | else { 242 | object.result = job.context.tv4.validateResult(object.value, object.schema, opts.checkRecursive, opts.banUnknown); 243 | } 244 | 245 | //TODO verify reportOnMissing 246 | if (!object.result.valid) { 247 | job.failed.push(object); 248 | out.writeln('> ' + style.error('fail') + ' - ' + tweakURI(object.label)); 249 | return callback(); 250 | } 251 | if (object.result.missing.length === 0) { 252 | job.passed.push(object); 253 | out.writeln('> ' + style.success('pass') + ' | ' + tweakURI(object.label)); 254 | return callback(); 255 | } 256 | 257 | // test for bad fragment pointer fall-through 258 | if (!object.result.missing.every(function (value) { 259 | return (value !== ''); 260 | })) { 261 | job.failed.push(object); 262 | out.writeln('> ' + style.error('empty missing-schema url detected') + ' (this likely casued by a bad fragment pointer)'); 263 | return callback(); 264 | } 265 | 266 | out.writeln('> ' + style.accent('auto') + ' ! validation missing ' + object.result.missing.length + ' urls:'); 267 | out.writeln('> "' + object.result.missing.join('"\n> "') + '"'); 268 | 269 | // auto load missing (if loading has an error we'll bail way back) 270 | loadSchemaList(job, object.result.missing, function (err) { 271 | if (err) { 272 | return callback(err); 273 | } 274 | //check again 275 | recursiveTest(job, object, callback); 276 | }); 277 | } 278 | 279 | function startLoading(job, object, callback) { 280 | //pre fetch (saves a validation round) 281 | loadSchemaList(job, job.context.tv4.getMissingUris(), function (err) { 282 | if (err) { 283 | return callback(err); 284 | } 285 | recursiveTest(job, object, callback); 286 | }); 287 | } 288 | 289 | //validate single object 290 | function validateObject(job, object, callback) { 291 | if (typeof object.value === 'undefined') { 292 | var onLoad = function (err, obj) { 293 | if (err) { 294 | job.error = err; 295 | return callback(err); 296 | } 297 | object.value = obj; 298 | doValidateObject(job, object, callback); 299 | }; 300 | var opts = { 301 | timeout: (job.context.options.timeout || 5000) 302 | }; 303 | //TODO verify http:, file: and plain paths all load properly 304 | if (object.path) { 305 | loader.loadPath(object.path, opts, onLoad); 306 | } 307 | else if (object.url) { 308 | loader.load(object.url, opts, onLoad); 309 | } 310 | else { 311 | callback(new Error('object missing value, path or url')); 312 | } 313 | } 314 | else { 315 | doValidateObject(job, object, callback); 316 | } 317 | } 318 | 319 | function doValidateObject(job, object, callback) { 320 | if (!object.root) { 321 | //out.writeln(style.warn('no explicit root schema')); 322 | //out.writeln(''); 323 | //TODO handle this better 324 | job.error = new Error('no explicit root schema'); 325 | callback(job); 326 | return; 327 | } 328 | var t = typeof object.root; 329 | 330 | switch (t) { 331 | case 'object': 332 | if (!Array.isArray(object.root)) { 333 | object.schema = object.root; 334 | job.context.tv4.addSchema((object.schema.id || ''), object.schema); 335 | 336 | startLoading(job, object, callback); 337 | } 338 | return; 339 | case 'string': 340 | //known from previous sessions? 341 | var schema = job.context.tv4.getSchema(object.root); 342 | if (schema) { 343 | out.writeln('> ' + style.plain('have') + ' : ' + tweakURI(object.root)); 344 | object.schema = schema; 345 | 346 | recursiveTest(job, object, callback); 347 | return; 348 | } 349 | out.writeln('> ' + style.accent('root') + ' > ' + tweakURI(object.root)); 350 | 351 | loader.load(object.root, job.context.options, function (err, schema) { 352 | if (err) { 353 | job.error = err; 354 | return callback(job.error); 355 | } 356 | if (!schema) { 357 | job.error = new Error('no schema loaded from: ' + object.root); 358 | return callback(job.error); 359 | } 360 | 361 | object.schema = schema; 362 | job.context.tv4.addSchema(object.root, schema); 363 | 364 | if (object.schema.id) { 365 | job.context.tv4.addSchema(object.schema); 366 | } 367 | startLoading(job, object, callback); 368 | }); 369 | return; 370 | default: 371 | callback(new Error('don\'t know how to load: ' + object.root)); 372 | return; 373 | } 374 | } 375 | 376 | return { 377 | isURL: isURL, 378 | getURLProtocol: getURLProtocol, 379 | getOptions: getOptions, 380 | getContext: getContext 381 | }; 382 | } 383 | 384 | module.exports = { 385 | getRunner: getRunner 386 | }; 387 | --------------------------------------------------------------------------------