source: Dev/trunk/node_modules/grunt-tv4/lib/runner.js @ 487

Last change on this file since 487 was 487, checked in by hendrikvanantwerpen, 11 years ago

Completed migration to API, without CouchDB proxy.

Move to API is now completed. The full API is password protected, a very
limited API is exposed for respondents, which works with secrets that
are passed in URLs.

Serverside the HTTPResult class was introduced, which is similar to
Promises, but specifically for HTTP. It carries a status code and
response and makes it easier to extract parts of async handling in
separate functions.

Fixed a bug in our schema (it seems optional attributes don't exist but
a required list does). Verification of our schema by grunt-tv4 didn't
work yet. Our schema is organized the wrong way (this is fixable),
but the json-schema schema has problems with simple types and $refs.

File size: 9.6 KB
Line 
1// Bulk validation core: composites with tv4, miniwrite, ministyle and loaders
2
3// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
4
5function 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
19function 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
38function 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
51function 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
64function isURL(uri) {
65        return (/^https?:/.test(uri) || /^file:/.test(uri));
66}
67
68var headExp = /^(\w+):/;
69
70function 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 '<unknown uri protocol>';
79}
80
81// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
82
83function 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
100function 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
384module.exports = {
385        getRunner: getRunner
386};
Note: See TracBrowser for help on using the repository browser.