Skip to content

Instantly share code, notes, and snippets.

@TimMensch
Created December 4, 2017 16:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TimMensch/45df32352b9d32353ab8b5912362632b to your computer and use it in GitHub Desktop.
Save TimMensch/45df32352b9d32353ab8b5912362632b to your computer and use it in GitHub Desktop.
Validation of JSON based on a TypeScript declaration
/*
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;
@ORESoftware
Copy link

ballerific

@ORESoftware
Copy link

ORESoftware commented Dec 5, 2017

Here's my feedback

  1. EE.prototype.once instead of EE.prototype.on in general
  2. Get rid of double quotes - " , single quotes are one of the greatest inventions ever lol
  3. Why not just put this at the top of the file:
if (!TJS)
      TJS = require("typescript-json-schema");

@ORESoftware
Copy link

ORESoftware commented Dec 5, 2017

Also, recommend naming js files like so:

validate-schema.js

not like so:

validateSchema.js

that's my preference

why? because *nix vs macos differ in case sensititivy, so I keep all filenames lowercase because i am badass like that, but last time I checked Mensch was on Windows so he's in the clear on this one :)

@TimMensch
Copy link
Author

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, so TJS isn't needed. And TJS 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 uses on. Is there an advantage to once, given that end can only happen once?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment