yavl v0.1.0
yavl!
Yet Another Validation Language (for JavaScript)
... but this one is beautiful.
var as = require('yavl');
var schema = {
name : String,
age : Number
};
as(schema).matches({
name : 'Fred',
age : 40
}); // => true
as(schema).cast({
name : 'Fred',
age : '40'
}); // => { name : 'Fred', age : 40 }
as(schema).validate({
name : 'Fred',
age : '40'
}); // => throws TypeErrorfeedback and contributions welcome
- let's get crazy
- basically
- types
- literals and logic
- objects
- arrays
- operators
- transformations
- definitions
- classes
- functions
- getting feedback
- alternatives
let's get crazy
as({
id : /^[a-f0-9]{32}$/,
type : as('addition', 'removal', 'update'),
shape : as.defined('shape', {
name : as('polygon', 'polyline', 'line', 'rect', 'ellipse', 'circle', 'path'),
attr : as({ undefined : as(String, Number) }).size(as.lte(100)),
text : as(String).size(as.lte(1000)),
children : as([as.defined('shape')]).or(undefined),
bbox : { x : Number, y : Number, width : Number, height : Number, undefined : Error },
undefined : Error
})
}).matches({
id : 'df13fbb92b9d43a7b53339abfb912cb4',
type : 'update',
shape : {
name : 'circle',
attr : { cx : 10, cy : 10, r : 10 },
bbox : { x : 0, y : 0, width : 20, height : 10 }
}
}); // => truebasically
The function returned from require('yavl') (we'll label it as from now on) transforms a schema into a checker for that schema. A checker has the three methods we've seen above:
matches(value)returnstrueif the value matches the schemacast(value)does its best to cast the value to something matching the schemavalidate(value)throws aTypeErrorif the value doesn't match the schema
Schemas can be hashes (as above), arrays, or a selection of JavaScript global objects representing basic types. Once a schema is established, it can be refined with chained, nested or branched operators and filters. Sounds complicated? It isn't. Let's dive in.
types
as(Number).matches(1);
as(String).matches('1');
as(Boolean).matches(true);
as(Date).matches(new Date);
as(Object).matches({});
as(Array).matches([]);
as(Function).matches(function () {});
as(JSON).matches('"1"');The Error object is used to force a mis-match. Only undefined matches Error.
as(Error).matches(undefined);The as function itself matches anything. This is useful for constructs like '... or anything' (see below).
as.matches(1) && as.matches('1') && as.matches({}) && as.matches(undefined)literals and logic
as('woah').matches('woah');
as(String).and('woah').matches('woah');
as('woah').or('dude').matches('woah');
as('woah', 'dude').matches('woah'); // shorthand for the aboveobjects
Objects are strict about their declared keys.
as({ a : Number }).matches({}) === false;To allow a key to be undefined, use logic.
as({ a : as(Number).or(undefined) }).matches({});
as({ a : as(Number, undefined) }).matches({}); // Shorthand orOn the other hand, an object schema is easy about additional keys ('be liberal in what you accept from others').
as({}).matches({ a : 1 });A key of 'undefined' means 'anything else'.
as({ undefined : Number }).matches({ a : 1 });
as({ undefined : Number }).matches({ b : 1 });So you can prevent additional keys using Error.
as({ undefined : Error }).matches({ a : 1 }) === false;arrays
An empty array is a shortcut for (any) Array.
as([]).matches([]);
as([]).matches([1, 2]);But arrays are strict about their declared indexes.
as([Number]).matches([]) === false;To allow an index to be undefined, use logic.
as([as(Number).or(undefined)]).matches([]);
as([as(Number, undefined)]).matches([]); // Shorthand orOn the other hand, an array schema is easy about additional indexes. However, they need to match the last declared index.
as([Number]).matches([1, 2]);
as([Number]).matches([1, '2']) === false;
as([Number, String]).matches([1, '2', '3']);
as([Number, String]).matches([1, '2', 3]) === false;To get around this, use the as function to match anything.
as([Number, String, as]).matches([1, '2', 3, new Date]);You can prevent additional keys entirely using Error.
as([Number, Error]).matches([1, 2]) === false;operators
We've met equality already, with literals. These are actually a shorthand:
as('woah').matches('woah');
as.eq('woah').matches('woah'); // shorthand for the aboveyavl's operators are inherited from lodash. So, we have
eq, lt, lte, gt, and gte.
as.gt(0).lt(10).matches(1);We also have regexes, which also has a shorthand:
as.regexp(/a/).matches('a');
as(/a/).matches('a');transformations
Objects and arrays can be transformed with size, first, last, nth, ceil, floor, max, mean, min and sum.
as.size(1).matches([1]);
as.first(1).matches([1, 2]);Additional arguments in an aggregation function are actually a schema. So:
as.size(1, 2).matches(['a']); // Is shorthand for...
as.size(as.eq(1).or(2)).matches(['a']);
as.size(1, 2).matches(['a', 'b']);
as.size(1, 2).matches(['a', 'b', 'c']) === false;When using cast and validate, the output of the transformation depends on whether you provided
schema arguments. If you did not, the output is the result of the transformation.
as.size().cast(['a']) === 1;But if you did, the result is an attempt to cast the contents of the input to suit. This only works
for size, first, last, and nth.
as.size(2).cast(['a']).length === 2;
as.first(1).cast([0, 2]); // => [1, 2]These behaviours can be useful in complex casts, like extracting typed information from a regex:
as(/([0-9\.]+)(\w{2})/).nth(1).and(Number).cast('12.3px') === 12.3;
as(/([0-9\.]+)(\w{2})/).nth(1, Number).cast('12.3px'); // => ['12.3px', 12.3, 'px']definitions
Sometimes you want to define something for later. define creates a definition (without applying it),
and defined applies something previously defined.
as.define('number', Number).defined('number').matches(1); // Not a particularly useful exampleThis is useful in recursion. Note that using defined with more than one argument will
both create and apply the definition.
assert.isTrue(as.defined('group', {
members : [as(Number).or(as.defined('group'))]
}).matches({
members : [1, 2, { members : [3, 4] }]
})); // That's betterclasses
function MyObject() {}
as(MyObject).matches(new MyObject());This is an instanceof check and so works with sub-classes.
Casting to a class passes the value into the constructor:
function MyObject(n) { this.n = n; }
as(MyObject).cast(1).n === 1;functions
You can check function parameters and return values using as.function() followed optionally by returns.
However, since the checking only happens when you actually call the function, we need to use
cast or validate. Casting will also cast the parameters if possible:
function addOne(n) { return n + 1; }
as.function(Number).returns(Number).cast(addOne)('1') === 2;
as.function(Number).returns(Number).validate(addOne)('1'); // throws TypeError (on the argument)
as.function(Number).returns(String).validate(addOne)(1); // throws TypeError (on the return value)getting feedback
An error thrown by validation will have a message which indicates where the failure happened. If you want to get feedback
from a match, provide a second argument of object type as.Status to the function. The object
will be populated with an array of failure locations.
var status = new as.Status();
as({ a : Number }).matches({ a : '1' }, status);
// => status.failures is ['object.a.number']alternatives
OK let's face it, sometimes you just need something that's been around for a while.