Created
December 4, 2017 16:26
-
-
Save TimMensch/45df32352b9d32353ab8b5912362632b to your computer and use it in GitHub Desktop.
Validation of JSON based on a TypeScript declaration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright 2017, Tim Mensch. | |
Permission is hereby granted, free of charge, to any person obtaining | |
a copy of this software and associated documentation files (the | |
"Software"), to deal in the Software without restriction, including | |
without limitation the rights to use, copy, modify, merge, publish, | |
distribute, sublicense, and/or sell copies of the Software, and to | |
permit persons to whom the Software is furnished to do so, subject | |
to the following conditions: | |
The above copyright notice and this permission notice shall be | |
included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
DEALINGS IN THE SOFTWARE. | |
*/ | |
"use strict"; | |
const gulp = require('gulp'); | |
const mapStream = require('map-stream'); | |
const newer = require('gulp-newer'); | |
const transform = require('vinyl-transform'); | |
const rename = require('gulp-rename'); | |
const strip = require('gulp-strip-comments'); | |
const fs = require('fs'); | |
const promisify = require('es6-promisify'); | |
const readFile = promisify(fs.readFile); | |
const cache = require('gulp-cached'); | |
const _ = require("lodash"); | |
let TJS; | |
let tv4; | |
/** | |
* Temporary (?) workaround for the fact that, sometimes, when there's a | |
* type|otherType given, it will give *both* a type and a oneOf object. | |
* The type in this case is completely wrong. This tree traversal deletes | |
* any "type" object members that exist alongside a "oneOf" member. | |
* | |
* @param {any} schema | |
*/ | |
function cleanSchema(schema) { | |
for (let key in schema) { | |
if (schema.hasOwnProperty(key)) { | |
let member = schema[ key ]; | |
if (typeof member === "object") { | |
if (member.type && member.oneOf) { | |
delete member.type; | |
} | |
cleanSchema(member); | |
} | |
} | |
} | |
} | |
function makeGenerator(typeToValidate, cb) { | |
return transform((filename) => { | |
if (!TJS) | |
TJS = require("typescript-json-schema"); | |
return mapStream((chunk, next) => { | |
// Thrown errors from within a mapStream callback | |
// aren't propagated, so I'm wrapping this in a | |
// try/catch. | |
try { | |
const schema = TJS.generateSchema( | |
TJS.getProgramFromFiles([ "./src/declarations/es6.d.ts", filename ]), | |
typeToValidate | |
); | |
if (!schema) { | |
return cb("Schema generation failed."); | |
} | |
cleanSchema(schema); | |
return next(null, JSON.stringify(schema)); | |
} catch (e) { | |
return cb(e); | |
} | |
}); | |
}); | |
} | |
function generateSchema(type, cb) { | |
const generate = makeGenerator(type, cb); | |
gulp.src("./src/declarations/scenario.d.ts") | |
.pipe(newer({ dest: `./int/schema/${type}`, extra: [ "./src/declarations/trait.d.ts" ] })) | |
.pipe(generate) | |
.pipe(rename(type)) | |
.pipe(gulp.dest("./int/schema")) | |
.on('end', cb); | |
} | |
let schemas = {}; | |
function validateSchema(type, files) { | |
const promise = new Promise((resolve, reject) => { | |
const schemaSource = readFile(`./int/schema/${type}`); | |
const validate = transform((filename) => { | |
return mapStream((chunk, next) => { | |
schemaSource.then((schemaJson) => { | |
try { | |
const json = JSON.parse(chunk); | |
if (schemaJson.length === 0) { | |
return reject("Bad schema!"); | |
} | |
if (!schemas[ type ]) { | |
schemas[ type ] = JSON.parse(schemaJson.toString()); | |
} | |
const schema = schemas[ type ]; | |
if (!tv4) { | |
tv4 = require("tv4"); | |
} | |
const result = tv4.validateResult(json, schema, false, true); | |
if (!result.valid) { | |
const suberrors = _.map( result.error.subErrors, (suberror) => { | |
return `${suberror.message}: ${suberror.dataPath}`; | |
}).join("\n"); | |
return reject(`Validation error in ${filename}: ${result.error.message}. | |
${suberrors} | |
At data path: ${result.error.dataPath}`); | |
} | |
return next(null, chunk); | |
} catch (e) { | |
console.error(`Error in parsing ${filename}: ${e}.`); | |
return reject(e); | |
} | |
}); | |
}); | |
}); | |
gulp.src(files) | |
.pipe(cache(`${type}-linting`)) | |
.pipe(strip()) | |
.pipe(validate) | |
.on("end", () => { | |
resolve(); | |
}); | |
}); | |
return promise; | |
} | |
function generateAndValidate(task, type, paths) { | |
var generateTask = task + "-generate"; | |
gulp.task(generateTask, function (cb) { | |
generateSchema(type, cb); | |
}); | |
gulp.task(task, [ generateTask ], function () { | |
return validateSchema(type, paths); | |
}); | |
} | |
module.exports = generateAndValidate; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
My JavaScript style is heavily influenced by my TypeScript style:
Double quotes are the standard I adhere to. JSON requires double quotes, in fact, and I like consistency. Single quotes are for ex-JavaScript-only devs who don't also use C, C++, C#, Go, Java, and Swift on a regular basis. If you note, all TypeScript samples use double quotes as well.
TypeScript itself will enforce consistent case in file includes. I also build most everything on Linux. It's an aesthetic difference, and I prefer to use camel case.
TJS
is loaded lazily, if it's needed. Most of the time the schemas don't change, soTJS
isn't needed. AndTJS
is big, counting its dependencies. I like my gulpfile to load faster when it can.Not sure what you mean about the EE.prototype.once. If you mean the
.on("end")
calls, literally every example is seen for gulp pipes useson
. Is there an advantage toonce
, given thatend
can only happen once?