diff --git a/jsen/jsen-tests.ts b/jsen/jsen-tests.ts
new file mode 100644
index 000000000..8504c0279
--- /dev/null
+++ b/jsen/jsen-tests.ts
@@ -0,0 +1,3503 @@
+///
+
+// any
+{
+ // passes validation on any type
+ {
+ const schema = { type: "any" };
+ const validate = jsen(schema);
+
+ console.assert(validate(null));
+ console.assert(validate(undefined));
+ console.assert(validate(0));
+ console.assert(validate(''));
+ console.assert(validate(Math.PI));
+ console.assert(validate('abc'));
+ console.assert(validate(77));
+ console.assert(validate(false));
+ console.assert(validate(true));
+ console.assert(validate({}));
+ console.assert(validate([]));
+ }
+}
+
+// type: array
+{
+ // required
+ {
+ const schema = { type: 'array' };
+ const validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate([]));
+ }
+ // nullable
+ {
+ const schema = { type: ['array', 'null'] };
+ const validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(null));
+ console.assert(validate([]));
+ }
+
+ // type
+ {
+ const schema = { type: 'array' };
+ const validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate(false));
+ console.assert(!validate({}));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate([]));
+ }
+
+ // minItems
+ {
+ const schema = { type: 'array', minItems: 3 };
+ const validate = jsen(schema);
+
+ console.assert(!validate([]));
+ console.assert(!validate([1, 2]));
+
+ console.assert(validate([1, 2, 3]));
+ console.assert(validate([1, 2, 3, 4]));
+ }
+
+ // maxItems
+ {
+ const schema = { type: 'array', maxItems: 3 };
+ const validate = jsen(schema);
+
+ console.assert(!validate([1, 2, 3, 4]));
+
+ console.assert(validate([]));
+ console.assert(validate([1, 2, 3]));
+ }
+
+ // items: object
+ {
+ let schema: any = { type: 'array', items: { type: 'string' } };
+ let validate = jsen(schema);
+
+ console.assert(!validate(null));
+ console.assert(!validate([1]));
+ console.assert(!validate(['a', false, 'b']));
+ console.assert(!validate(['a', 'b', 1]));
+
+ console.assert(validate([]));
+ console.assert(validate(['a']));
+ console.assert(validate(['a', 'b', 'c']));
+
+ schema = {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ strProp: { type: 'string' },
+ boolProp: { type: 'boolean' }
+ },
+ required: ['strProp']
+ }
+ };
+
+ validate = jsen(schema);
+
+ console.assert(!validate([123]));
+ console.assert(!validate([{}]));
+ console.assert(!validate([{
+ strProp: 'value',
+ boolProp: 123
+ }]));
+
+ console.assert(validate([{
+ strProp: 'value',
+ boolProp: false
+ }]));
+ }
+
+ // items: array
+ {
+ const schema = {
+ type: 'array',
+ items: [
+ { type: 'string' },
+ { type: 'number' }
+ ]
+ };
+
+ const validate = jsen(schema);
+
+ console.assert(!validate([1]));
+ console.assert(!validate([1, 'a']));
+
+ console.assert(validate([]));
+ console.assert(validate(['a']));
+ console.assert(validate(['a', 1]));
+ console.assert(validate(['a', 1, null, 'b', 2]));
+ }
+
+ // additionalItems: boolean
+ {
+ let schema: any = { type: 'array', additionalItems: false };
+ let validate = jsen(schema);
+
+ console.assert(validate([]));
+ console.assert(validate([1]));
+ console.assert(validate([1, 'a', true]));
+
+ schema.items = { type: 'number' };
+ validate = jsen(schema);
+
+ console.assert(!validate(['a']));
+
+ console.assert(validate([]));
+ console.assert(validate([1]));
+ console.assert(validate([1, 2, 3]));
+
+ schema.items = [
+ { type: 'string' },
+ { type: 'number' }
+ ];
+ validate = jsen(schema);
+
+ console.assert(!validate(['a', 1, 2]));
+
+ console.assert(validate([]));
+ console.assert(validate(['a']));
+ console.assert(validate(['a', 1]));
+ }
+
+ // additionalItems: object
+ {
+ // when `items` is an object schema, `additionalItems`
+ // is ignored and must not validate against
+ let schema: any = {
+ type: 'array',
+ items: {
+ type: 'string'
+ },
+ additionalItems: {
+ type: 'number'
+ }
+ };
+
+ let validate = jsen(schema);
+
+ console.assert(!validate(['abc', 'def', 123]));
+
+ // same as above description - only strings are valid
+ console.assert(validate(['abc', 'def']));
+
+ // when `items` is an array, any other positional
+ // data item must validate against `additionalItems`
+ schema.items = [
+ { type: 'string' },
+ { type: 'boolean' }
+ ];
+ validate = jsen(schema);
+
+ console.assert(!validate(['abc', false, 'def']));
+
+ console.assert(validate(['abc', false]));
+ console.assert(validate(['abc', false, 123]));
+ console.assert(validate(['abc', false, 123, Math.PI]));
+
+ // when `additionalItems` is an empty object, anything is valid
+ schema.additionalItems = {};
+ validate = jsen(schema);
+
+ console.assert(validate(['abc', false, 'def', 123, {}, null]));
+ }
+
+ // uniqueItems
+ {
+ let schema = {
+ type: 'array',
+ items: { type: 'number' },
+ uniqueItems: false
+ };
+
+ let validate = jsen(schema);
+
+ console.assert(validate([1, 2, 1]));
+
+ schema.uniqueItems = true;
+ validate = jsen(schema);
+
+ console.assert(!validate([1, 2, 1]));
+
+ console.assert(validate([1, 2, 3]));
+ }
+}
+
+// type: boolean
+{
+ // required
+ {
+ const schema = { type: 'boolean' };
+ const validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate(false));
+ console.assert(validate(true));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['boolean', 'null'] };
+ const validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(true));
+ console.assert(validate(false));
+ console.assert(validate(null));
+ }
+
+ // type
+ {
+ const schema = { type: 'boolean' };
+ const validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(true));
+ console.assert(validate(false));
+ }
+}
+
+// build
+{
+ // validator function has a build property
+ {
+ const validate = jsen({});
+
+ console.assert(typeof validate.build === 'function');
+ console.assert(validate.build.length === 2);
+ }
+
+ // returns default value from schema if initial undefined
+ {
+ const validate = jsen({ type: 'string', default: 'abc' });
+
+ console.assert(validate.build() === 'abc');
+ console.assert(validate.build(undefined) === 'abc');
+ }
+
+ // does not modify initial defined value
+ {
+ const validate = jsen({ type: 'string', default: 'abc' });
+ const initials = [
+ null,
+ '',
+ 'string value',
+ true,
+ false,
+ 123,
+ Math.PI
+ ];
+
+ let obj: any;
+
+ initials.forEach((initial) => {
+ obj = validate.build(initial);
+ console.assert(obj === initial);
+ });
+ }
+
+ // returns a copy of initial defined value
+ {
+ const validate = jsen({ type: 'string', default: 'abc' });
+ const initials = [
+ {},
+ [],
+ new Date()
+ ];
+
+ let obj: any;
+
+ initials.forEach((initial) => {
+ obj = validate.build(initial);
+ console.assert(obj !== initial);
+ console.assert(JSON.stringify(obj) === JSON.stringify(initial));
+ });
+ }
+
+ // returns initial if no default in schema
+ {
+ const validate = jsen({}),
+ initials = [
+ undefined,
+ null,
+ '',
+ 'string value',
+ true,
+ false,
+ 123,
+ Math.PI,
+ {},
+ [],
+ new Date()
+ ];
+
+ let obj: any;
+
+ initials.forEach((initial) => {
+ obj = validate.build(initial);
+ console.assert(JSON.stringify(obj) === JSON.stringify(initial));
+ });
+ }
+
+ // returns the default value specified in schema
+ {
+ const schemas = [
+ { default: null },
+ { default: undefined },
+ { default: '' },
+ { default: 'abc' },
+ { default: true },
+ { default: false },
+ { default: 123 },
+ { default: Math.PI }
+ ];
+
+ let validate: IJsenValidator;
+
+ schemas.forEach((schema) => {
+ validate = jsen(schema);
+ console.assert(validate.build() === schema.default);
+ });
+ }
+
+ // returns a copy of a default object or array in schema
+ {
+ const schemas = [
+ { default: { a: { b: 123 } } },
+ { default: [[1, 2, 3], { a: { b: 123 } }] },
+ { default: /\d+/ },
+ { default: new Date('05/14/2015') }
+ ];
+
+ let validate: IJsenValidator;
+ let def: any;
+
+ schemas.forEach((schema) => {
+ validate = jsen(schema);
+ def = validate.build();
+
+ console.assert(def !== schema.default);
+ console.assert(JSON.stringify(def) === JSON.stringify(schema.default));
+ });
+ }
+
+ // recursively collects default values
+ {
+ const schema: any = {
+ type: 'object',
+ default: {},
+ properties: {
+ a: {
+ type: 'array',
+ default: [],
+ items: {
+ type: 'string'
+ }
+ },
+ b: {
+ type: 'array',
+ default: [],
+ items: {
+ type: 'string',
+ default: 'abc'
+ }
+ },
+ c: {
+ type: 'object',
+ default: {},
+ properties: {
+ d: {
+ type: 'boolean',
+ default: false
+ },
+ e: {
+ type: 'date',
+ default: new Date('05/14/2015')
+ },
+ f: {
+ type: 'array',
+ default: [{}, {}],
+ items: [{
+ type: 'object',
+ properties: {
+ g: {
+ type: 'string',
+ default: 'yes'
+ }
+ }
+ }, {
+ type: 'object',
+ properties: {
+ g: {
+ type: 'integer',
+ default: 0
+ }
+ }
+ }, {
+ type: 'object',
+ properties: {
+ g: {
+ type: 'boolean',
+ default: true
+ }
+ }
+ }]
+ },
+ h: {
+ type: 'array',
+ default: [{}, {}],
+ items: {
+ type: 'object',
+ properties: {
+ i: {
+ type: 'object',
+ default: { foo: 'bar' }
+ }
+ }
+ }
+ },
+ i: {
+ type: 'object',
+ default: null,
+ properties: {
+ j: {
+ type: 'string',
+ default: 'baz'
+ }
+ }
+ }
+ }
+ },
+ j: {
+ type: 'object',
+ properties: {
+ k: {
+ type: 'boolean',
+ default: false
+ }
+ }
+ }
+ }
+ };
+
+ const expected: any = {
+ a: [],
+ b: [],
+ c: {
+ d: false,
+ e: new Date('05/14/2015'),
+ f: [
+ { g: 'yes' },
+ { g: 0 }
+ ],
+ h: [
+ { i: { foo: 'bar' } },
+ { i: { foo: 'bar' } }
+ ],
+ i: null
+ }
+ };
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // merges default values with the initial values
+ {
+ const schemas = [
+ {
+ properties: {
+ foo: { default: 'bar' }
+ }
+ },
+ {
+ properties: {
+ foo: { default: 'bar' }
+ }
+ },
+ {
+ properties: {
+ foo: { default: 'bar' }
+ }
+ },
+ {
+ items: {
+ properties: {
+ foo: { default: 'bar' }
+ }
+ }
+ },
+ {
+ items: {
+ properties: {
+ foo: { default: 'bar' }
+ }
+ }
+ },
+ {
+ items: [
+ { default: 'foo' },
+ { default: 'bar' },
+ { default: 'baz' }
+ ]
+ },
+ {
+ items: [
+ { default: 'foo' },
+ { default: 'bar' },
+ { default: 'baz' }
+ ]
+ }
+ ];
+
+ const defaults = [
+ {},
+ { foo: 'baz' },
+ { x: 'yz' },
+ [],
+ [{}],
+ [],
+ [null, {}, undefined, false]
+ ];
+
+ const expected = [
+ { foo: 'bar' },
+ { foo: 'baz' },
+ { foo: 'bar', x: 'yz' },
+ [],
+ [{ foo: 'bar' }],
+ ['foo', 'bar', 'baz'],
+ [null, {}, 'baz', false]
+ ];
+
+ let validate: IJsenValidator;
+
+ schemas.forEach((schema, index) => {
+ validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build(defaults[index])) === JSON.stringify(expected[index]));
+ });
+ }
+
+ // $ref
+ {
+ const schema = {
+ definitions: {
+ positiveInteger: {
+ type: 'integer',
+ minimum: 1,
+ default: 7
+ }
+ },
+ $ref: '#definitions/positiveInteger'
+ };
+
+ const validate = jsen(schema);
+ console.assert(validate.build() === 7);
+ }
+
+ // object
+ {
+ // clones default object and subproperties recursively
+ {
+ const schema = {
+ default: {},
+ properties: {
+ foo: {
+ type: 'string',
+ default: 'bar'
+ }
+ }
+ };
+
+ const expected = { foo: 'bar' };
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // does not recursively assign defaults of children if parent default is not an object
+ {
+ const schema: any = {
+ default: [],
+ properties: {
+ foo: {
+ type: 'string',
+ default: 'bar'
+ }
+ }
+ };
+
+ const expected: any = [];
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+ }
+
+ // array
+ {
+ // does not add new elements to default array: items schema is an object
+ {
+ const schema: any = {
+ default: [],
+ items: {
+ type: 'string',
+ default: 'bar'
+ }
+ };
+
+ const expected: any = [];
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // adds new elements to default array: items schema is an array
+ {
+ const schema: any = {
+ default: [],
+ items: [{
+ type: 'string',
+ default: 'bar'
+ }]
+ };
+
+ const expected = ['bar'];
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // adds default values to already existing child items of compatible type only: items schema is an object
+ {
+ const schema: any = {
+ default: [{}, null, []],
+ items: {
+ properties: {
+ foo: {
+ type: 'string',
+ default: 'bar'
+ }
+ }
+ }
+ };
+
+ const expected: any = [{ foo: 'bar' }, null, []];
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // adds default values to already existing child items of compatible type only: items schema is an array
+ {
+ const schema: any = {
+ default: [{}, null, {}, undefined],
+ items: [
+ {
+ properties: {
+ foo: {
+ type: 'string',
+ default: 'bar'
+ }
+ }
+ },
+ { default: 'abc' },
+ { default: 123 },
+ { default: 123 }
+ ]
+ };
+
+ const expected = [{ foo: 'bar' }, null, {}, 123];
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+
+ // does not assign default child items if parent default is not an array
+ {
+ const schema: any = {
+ default: 'foobar',
+ items: {
+ type: 'string',
+ default: 'bar'
+ }
+ };
+
+ const expected = 'foobar';
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build()) === JSON.stringify(expected));
+ }
+ }
+
+ // option: copy
+ {
+ // returns deep copy of the initial object by default
+ {
+ const schema = {};
+ const initial = {};
+
+ const validate = jsen(schema);
+
+ console.assert(JSON.stringify(validate.build(initial)) === JSON.stringify(initial));
+ console.assert(validate.build(initial) !== initial);
+ }
+
+ // modifies the initial object when copy = false
+ {
+ const schema = {
+ properties: {
+ a: { default: 'foo' },
+ b: {
+ items: {
+ properties: {
+ c: { default: 'bar' },
+ d: { default: 'baz' }
+ }
+ }
+ }
+ }
+ };
+
+ const initial = { b: [{ d: 'xyz' }] };
+ const expected = { a: 'foo', b: [{ c: 'bar', d: 'xyz' }] };
+
+ const validate = jsen(schema);
+ const actual = validate.build(initial, { copy: false });
+
+ console.assert(JSON.stringify(actual) === JSON.stringify(expected));
+ console.assert(actual === initial);
+ }
+ }
+
+ // option: additionalProperties
+ {
+ // includes by default
+ {
+ const schema = {
+ properties: {
+ foo: {}
+ }
+ };
+
+ const initial = { foo: 1, bar: 2 };
+ const expected = { foo: 1, bar: 2 };
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build(initial)) === JSON.stringify(expected));
+ }
+
+ // excludes when schema.additionalProperties = false
+ {
+ const schema = {
+ additionalProperties: false,
+ properties: {
+ foo: {}
+ }
+ };
+
+ const initial = { foo: 1, bar: 2 };
+ const expected = { foo: 1 };
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build(initial)) === JSON.stringify(expected));
+ }
+
+ // removes from the initial object when schema.additionalProperties = false and options.copy = false
+ {
+ const schema = {
+ additionalProperties: false,
+ properties: {
+ foo: {}
+ }
+ };
+
+ const initial = { foo: 1, bar: 2 };
+ const expected = { foo: 1 };
+
+ const validate = jsen(schema);
+ const actual = validate.build(initial, { copy: false });
+
+ console.assert(actual === initial);
+ console.assert(JSON.stringify(actual) === JSON.stringify(expected));
+ }
+
+ // removes from the initial object when options.additionalProperties = false and options.copy = false
+ {
+ const schema = {
+ properties: {
+ foo: {}
+ }
+ };
+
+ const initial = { foo: 1, bar: 2 };
+ const expected = { foo: 1 };
+
+ const validate = jsen(schema);
+ const actual = validate.build(initial, {
+ copy: false,
+ additionalProperties: false
+ });
+
+ console.assert(actual === initial);
+ console.assert(JSON.stringify(actual) === JSON.stringify(expected));
+ }
+
+ // schema.additionalProperties takes precedence
+ {
+ const schema = {
+ additionalProperties: true,
+ properties: {
+ foo: {}
+ }
+ };
+
+ const initial = { foo: 1, bar: 2 };
+ const expected = { foo: 1, bar: 2 };
+
+ const validate = jsen(schema);
+ console.assert(JSON.stringify(validate.build(initial, { additionalProperties: false })) === JSON.stringify(expected));
+ }
+ }
+}
+
+// clone
+{
+ const clone = jsen.clone;
+
+ // string
+ {
+ console.assert(clone('abc') === 'abc');
+ }
+
+ // number
+ {
+ console.assert(clone(123) === 123);
+ }
+
+ // integer
+ {
+ console.assert(clone(Math.PI) === Math.PI);
+ }
+
+ // boolean
+ {
+ console.assert(clone(false) === false);
+ }
+
+ // function
+ {
+ const func = () => {
+ };
+ console.assert(clone(func) === func);
+ }
+
+ // regexp
+ {
+ const regex = /a/gim;
+ console.assert(clone(regex) !== regex);
+ console.assert(clone(regex).toString() === regex.toString());
+ }
+
+ // date
+ {
+ const today = new Date('05/14/2015');
+ console.assert(clone(today) !== today);
+ console.assert(clone(today).toJSON() === today.toJSON());
+ }
+
+ // null
+ {
+ console.assert(clone(null) === null);
+ }
+
+ // undefined
+ {
+ console.assert(clone(undefined) === undefined);
+ }
+
+ // object
+ {
+ const obj = { a: 1, b: 'a', c: false, d: { e: Math.PI, f: [1, 2, 3] } };
+ console.assert(clone(obj) !== obj);
+ console.assert(JSON.stringify(clone(obj)) === JSON.stringify(obj));
+ }
+
+ // array
+ {
+ const arr = [1, 'a', false, { d: [1, 2, 3] }];
+ console.assert(clone(arr) !== arr);
+ console.assert(JSON.stringify(clone(arr)) === JSON.stringify(arr));
+ }
+}
+
+// type: date
+{
+ // required
+ {
+ const schema = { type: 'date' };
+ const validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate(new Date()));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['date', 'null'] };
+ const validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(new Date()));
+ console.assert(validate(null));
+ }
+
+ // type
+ {
+ const schema = { type: 'date' };
+ const validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(new Date()));
+ }
+}
+
+// equal
+{
+ const equal = jsen.equal;
+ // string
+ {
+ console.assert(equal('a', 'a'));
+ console.assert(!equal('a', 'b'));
+ }
+
+ // number
+ {
+ console.assert(equal(123, 123));
+ console.assert(equal(Math.PI, Math.PI));
+ console.assert(!equal(Math.PI, Math.E));
+ }
+
+ // boolean
+ {
+ console.assert(equal(true, true));
+ console.assert(!equal(true, false));
+ }
+
+ // null
+ {
+ console.assert(equal(null, null));
+ console.assert(!equal(null, undefined));
+ }
+
+ // undefined
+ {
+ console.assert(equal(undefined, undefined));
+ console.assert(!equal(null, undefined));
+ }
+
+ // function
+ {
+ const f1 = () => {
+ };
+ const f2 = () => {
+ };
+ const f3 = f1;
+
+ // two functions are only equal if they
+ // reference the same function object
+ console.assert(equal(f1, f3));
+ console.assert(!equal(f1, f2));
+ }
+
+ // array
+ {
+ const f = () => {
+ };
+ const obj1 = { a: 123, b: 'abc', c: f };
+ const obj2 = { a: 123, b: 'abc', c: f };
+ const arr1 = [1, 'a', f, obj1];
+ const arr2 = [1, 'a', f, obj1];
+ const arr3 = [1, 'a', f];
+ const arr4 = [1, 'a', f, obj2];
+
+ console.assert(equal(arr1, arr2));
+ console.assert(equal(arr1, arr4));
+
+ console.assert(!equal(arr1, arr3));
+ }
+
+ // object
+ {
+ const a = { a: 123, b: ['abc'], c: {} };
+ const b = { b: ['abc'], c: {}, a: 123 };
+ const c = a;
+ const d: any = { a: 123, b: ['abc'], c: { d: undefined } };
+
+ console.assert(equal(a, b));
+ console.assert(equal(a, c));
+
+ console.assert(!equal(a, d));
+ }
+
+ // regexp
+ {
+ const a = /a+/gim;
+ const b = new RegExp('a+', 'gim');
+ const c = /a/gim;
+ const d = a;
+
+ console.assert(equal(a, b));
+ console.assert(equal(a, d));
+
+ console.assert(!equal(a, c));
+ }
+}
+
+// error
+{
+ // errors is empty array before validation
+ {
+ const schema = { type: 'number' };
+ const validate = jsen(schema);
+
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 0);
+ }
+
+ // no errors on successful validation
+ {
+ const schema = { type: 'number' };
+ const validate = jsen(schema);
+ const valid = validate(123);
+
+ console.assert(valid);
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 0);
+ }
+
+ // has errors when validation unsuccessful
+ {
+ const schema = { type: 'number' };
+ const validate = jsen(schema);
+ const valid = validate('123');
+
+ console.assert(!valid);
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 1);
+ }
+
+ // clears errors on successive validation calls
+ {
+ const schema = { type: 'number' };
+ const validate = jsen(schema);
+
+ validate('123');
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 1);
+
+ validate(123);
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 0);
+
+ validate('123');
+ console.assert(Array.isArray(validate.errors));
+ console.assert(validate.errors.length === 1);
+ }
+
+ // two successive runs return different arrays
+ {
+ const schema = { type: 'number' };
+ const validate = jsen(schema);
+
+ let previous: any;
+
+ validate('123');
+ console.assert(validate.errors.length === 1);
+ previous = validate.errors;
+
+ validate('123');
+ console.assert(validate.errors.length === 1);
+
+ console.assert(validate.errors !== previous);
+ console.assert(JSON.stringify(validate.errors) === JSON.stringify(previous));
+ }
+
+ // error object
+ {
+ const schemas = [
+ {
+ type: 'number'
+ },
+
+ {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'string'
+ }
+ }
+ },
+
+ {
+ type: 'array',
+ uniqueItems: true
+ },
+
+ {
+ type: 'array',
+ items: {
+ maximum: 10
+ }
+ },
+
+ {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'array',
+ items: [{
+ type: 'object',
+ properties: {
+ b: {
+ multipleOf: 7
+ }
+ }
+ }]
+ }
+ }
+ },
+
+ {
+ allOf: [
+ { minimum: 5 },
+ { maximum: 10 }
+ ]
+ },
+
+ {
+ type: 'object',
+ properties: {
+ a: {
+ anyOf: [
+ { type: 'string' },
+ { type: 'number' }
+ ]
+ }
+ }
+ },
+
+ {
+ type: 'array',
+ items: [{
+ type: 'object',
+ properties: {
+ a: {
+ oneOf: [
+ { type: 'boolean' },
+ { type: 'null' }
+ ]
+ }
+ }
+ }]
+ },
+
+ {
+ type: 'object',
+ properties: {
+ a: {
+ not: {
+ type: 'string'
+ }
+ }
+ }
+ },
+
+ {
+ definitions: {
+ positiveInteger: {
+ type: 'integer',
+ minimum: 0,
+ exclusiveMinimum: true
+ }
+ },
+ type: 'object',
+ properties: {
+ a: {
+ type: 'object',
+ properties: {
+ b: {
+ type: 'object',
+ properties: {
+ c: {
+ $ref: '#/definitions/positiveInteger'
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ {
+ type: 'object',
+ required: ['a', 'b']
+ },
+
+ {
+ type: 'object',
+ dependencies: {
+ a: {
+ required: ['b']
+ }
+ }
+ },
+
+ {
+ type: 'object',
+ dependencies: {
+ a: ['b']
+ }
+ }
+ ];
+
+ const data = [
+ '123',
+ { a: 123 },
+ [7, 11, 7],
+ [10, 11, 9],
+ { a: [{ b: 8 }] },
+ 12,
+ { a: false },
+ [{ a: 123 }],
+ { a: 'abc' },
+ { a: { b: { c: 0 } } },
+ {},
+ { a: 123 },
+ { a: 123 }
+ ];
+
+ // property: path
+ {
+ const expectedPaths = [
+ [''],
+ ['a'],
+ [''],
+ ['1'],
+ ['a.0.b'],
+ [''],
+ ['a', 'a'],
+ ['0.a', '0.a', '0.a'],
+ ['a'],
+ ['a.b.c'],
+ ['a'],
+ ['b'],
+ ['b']
+ ];
+
+ let validate: IJsenValidator;
+ let valid: boolean;
+
+ schemas.forEach((schema, index) => {
+ validate = jsen(schema);
+ valid = validate(data[index]);
+
+ console.assert(!valid);
+
+ expectedPaths[index].forEach((path, pindex) => {
+ try {
+ console.assert(validate.errors[pindex].path === path);
+ }
+ catch (e) {
+ // console.log(index);
+ // console.log(validate.errors);
+ throw e;
+ }
+ });
+ });
+ }
+
+ // property: keyword
+ {
+ const expectedKeywords = [
+ ['type'],
+ ['type'],
+ ['uniqueItems'],
+ ['maximum'],
+ ['multipleOf'],
+ ['maximum'],
+ ['type', 'type', 'anyOf'],
+ ['type', 'type', 'oneOf'],
+ ['not'],
+ ['exclusiveMinimum'],
+ ['required'],
+ ['required'],
+ ['dependencies']
+ ];
+
+ let validate: IJsenValidator;
+ let valid: boolean;
+
+ schemas.forEach((schema, index) => {
+ validate = jsen(schema);
+ valid = validate(data[index]);
+
+ console.assert(!valid);
+
+ expectedKeywords[index].forEach((keyword, kindex) => {
+ try {
+ console.assert(validate.errors[kindex].keyword === keyword);
+ }
+ catch (e) {
+ // console.log(index);
+ // console.log(validate.errors);
+ throw e;
+ }
+ });
+ });
+ }
+
+ // adds required property name to path
+ {
+ let schema: any = { type: 'object', required: ['a'] };
+ let validate = jsen(schema);
+ let valid = validate({});
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].path === 'a');
+ console.assert(validate.errors[0].keyword === 'required');
+
+ schema = {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'array',
+ items: {
+ type: 'object',
+ required: ['b']
+ }
+ }
+ }
+ };
+
+ validate = jsen(schema);
+ valid = validate({ a: [{}] });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].path === 'a.0.b');
+ console.assert(validate.errors[0].keyword === 'required');
+ }
+
+ // adds required dependency property to path
+ {
+ let schema: any = {
+ type: 'object',
+ dependencies: {
+ a: ['b']
+ }
+ };
+
+ let validate = jsen(schema);
+ let valid = validate({ a: 123 });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].path === 'b');
+ console.assert(validate.errors[0].keyword === 'dependencies');
+
+ schema = {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'array',
+ items: {
+ type: 'object',
+ dependencies: {
+ a: ['b']
+ }
+ }
+ }
+ }
+ };
+
+ validate = jsen(schema);
+ valid = validate({ a: [{ a: 123 }] });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].path === 'a.0.b');
+ console.assert(validate.errors[0].keyword === 'dependencies');
+ }
+ }
+
+ // multiple errors
+ {
+ const schema = {
+ definitions: {
+ array: {
+ maxItems: 1
+ }
+ },
+ type: 'object',
+ properties: {
+ a: {
+ anyOf: [
+ { items: { type: 'integer' } },
+ { $ref: '#/definitions/array' },
+ { items: [{ maximum: 3 }] }
+ ]
+ }
+ }
+ };
+
+ const data = { a: [Math.PI, Math.E] };
+ const validate = jsen(schema);
+
+ // returns multiple errors
+ {
+ const valid = validate(data);
+
+ // console.log(validate.errors);
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 5);
+ }
+ }
+
+ // custom errors
+ {
+ const schemas = [
+ {
+ type: 'string',
+ invalidMessage: 'string is invalid',
+ requiredMessage: 'string is required'
+ },
+ {
+ type: 'object',
+ required: ['a'],
+ properties: {
+ a: {
+ invalidMessage: 'a is invalid',
+ requiredMessage: 'a is required'
+ }
+ }
+ },
+ {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'object',
+ properties: {
+ b: {
+ invalidMessage: 'b is invalid',
+ requiredMessage: 'b is required'
+ }
+ },
+ required: ['b']
+ }
+ }
+ }
+ },
+ {
+ type: 'object',
+ properties: {
+ a: {
+ type: 'object',
+ properties: {
+ c: {
+ type: 'string',
+ invalidMessage: 'c is invalid',
+ requiredMessage: 'c is required'
+ }
+ }
+ }
+ }
+ }
+ ];
+
+ const data = [
+ undefined,
+ {},
+ [{ a: {} }],
+ { a: { c: 123 } }
+ ];
+
+ const expectedMessages = [
+ 'string is invalid',
+ 'a is required',
+ 'b is required',
+ 'c is invalid'
+ ];
+
+ let validate: IJsenValidator;
+ let valid: boolean;
+
+ schemas.forEach((schema, index) => {
+ //it(expectedMessages[index], function () {
+ validate = jsen(schema);
+ valid = validate(data[index]);
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === expectedMessages[index]);
+ //});
+ });
+ }
+
+ // custom keyword messages
+ {
+ // uses custom messages on keywords
+ {
+ const schemas: any = [
+ {
+ type: 'string',
+ messages: { type: 'custom message for keyword "type"' }
+ },
+ {
+ enum: [1, 2, 3],
+ messages: { enum: 'custom message for keyword "enum"' }
+ },
+ {
+ minimum: 3,
+ messages: { minimum: 'custom message for keyword "minimum"' }
+ },
+ {
+ minimum: 3,
+ exclusiveMinimum: true,
+ messages: { exclusiveMinimum: 'custom message for keyword "exclusiveMinimum"' }
+ },
+ {
+ maximum: 10,
+ messages: { maximum: 'custom message for keyword "maximum"' }
+ },
+ {
+ maximum: 10,
+ exclusiveMaximum: true,
+ messages: { exclusiveMaximum: 'custom message for keyword "exclusiveMaximum"' }
+ },
+ {
+ multipleOf: 5,
+ messages: { multipleOf: 'custom message for keyword "multipleOf"' }
+ },
+ {
+ minLength: 3,
+ messages: { minLength: 'custom message for keyword "minLength"' }
+ },
+ {
+ maxLength: 5,
+ messages: { maxLength: 'custom message for keyword "maxLength"' }
+ },
+ {
+ pattern: '\\d+',
+ messages: { pattern: 'custom message for keyword "pattern"' }
+ },
+ {
+ format: 'email',
+ messages: { format: 'custom message for keyword "format"' }
+ },
+ {
+ minItems: 1,
+ messages: { minItems: 'custom message for keyword "minItems"' }
+ },
+ {
+ maxItems: 1,
+ messages: { maxItems: 'custom message for keyword "maxItems"' }
+ },
+ {
+ additionalItems: false,
+ items: [{ type: 'string' }],
+ messages: { additionalItems: 'custom message for keyword "additionalItems"' }
+ },
+ {
+ uniqueItems: true,
+ messages: { uniqueItems: 'custom message for keyword "uniqueItems"' }
+ },
+ {
+ minProperties: 1,
+ messages: { minProperties: 'custom message for keyword "minProperties"' }
+ },
+ {
+ maxProperties: 1,
+ messages: { maxProperties: 'custom message for keyword "maxProperties"' }
+ },
+ {
+ required: ['foo'],
+ messages: { required: 'custom message for keyword "required"' }
+ },
+ {
+ required: ['foo'],
+ properties: {
+ foo: {
+ messages: {
+ required: 'custom message for keyword "required"'
+ }
+ }
+ }
+ },
+ {
+ required: ['foo'],
+ properties: {
+ foo: {
+ messages: {
+ required: 'this custom message for keyword "required" is assigned'
+ }
+ }
+ },
+ messages: { required: 'this custom message for keyword "required" is NOT assigned' }
+ },
+ {
+ additionalProperties: false,
+ messages: { additionalProperties: 'custom message for keyword "additionalProperties"' }
+ },
+ {
+ dependencies: {
+ foo: ['bar']
+ },
+ messages: { dependencies: 'custom message for keyword "dependencies"' }
+ },
+ {
+ anyOf: [
+ { type: 'string' },
+ { type: 'integer' }
+ ],
+ messages: { anyOf: 'custom message for keyword "anyOf"' }
+ },
+ {
+ oneOf: [
+ { type: 'string' },
+ { type: 'integer' }
+ ],
+ messages: { oneOf: 'custom message for keyword "oneOf"' }
+ },
+ {
+ not: {
+ type: 'string'
+ },
+ messages: { not: 'custom message for keyword "not"' }
+ }
+ ];
+
+ const data = [
+ 123,
+ 5,
+ 1,
+ 3,
+ 11,
+ 10,
+ 12,
+ 'ab',
+ 'abcdef',
+ 'abc',
+ 'invalid email',
+ [],
+ [1, 2, 3],
+ ['abc', 'def'],
+ [1, 2, 2],
+ {},
+ { foo: 1, bar: 2 },
+ {},
+ {},
+ {},
+ { foo: 'bar' },
+ { foo: 'abc' },
+ null,
+ null,
+ 'abc'
+ ];
+
+ const expectedMessages = [
+ schemas[0].messages.type,
+ schemas[1].messages.enum,
+ schemas[2].messages.minimum,
+ schemas[3].messages.exclusiveMinimum,
+ schemas[4].messages.maximum,
+ schemas[5].messages.exclusiveMaximum,
+ schemas[6].messages.multipleOf,
+ schemas[7].messages.minLength,
+ schemas[8].messages.maxLength,
+ schemas[9].messages.pattern,
+ schemas[10].messages.format,
+ schemas[11].messages.minItems,
+ schemas[12].messages.maxItems,
+ schemas[13].messages.additionalItems,
+ schemas[14].messages.uniqueItems,
+ schemas[15].messages.minProperties,
+ schemas[16].messages.maxProperties,
+ schemas[17].messages.required,
+ schemas[18].properties.foo.messages.required,
+ schemas[19].properties.foo.messages.required,
+ schemas[20].messages.additionalProperties,
+ schemas[21].messages.dependencies,
+ schemas[22].messages.anyOf,
+ schemas[23].messages.oneOf,
+ schemas[24].messages.not
+ ];
+
+ let validate: IJsenValidator;
+ let valid: boolean;
+
+ schemas.forEach((schema: any, index: number) => {
+ validate = jsen(schema);
+
+ valid = validate(data[index]);
+
+ console.assert(!valid);
+ console.assert(validate.errors[validate.errors.length - 1].message === expectedMessages[index]);
+ });
+ }
+
+ // does not use custom messages on keyword: items (object)
+ {
+ const schema = {
+ items: {
+ type: 'string',
+ messages: {
+ type: 'will be assigned'
+ }
+ },
+ messages: {
+ items: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate([123]);
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+
+ // does not use custom messages on keyword: items (array)
+ {
+ const schema = {
+ items: [{
+ type: 'string',
+ messages: {
+ type: 'will be assigned'
+ }
+ }],
+ messages: {
+ items: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate([123, 123]);
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+
+ // does not use custom messages on keyword: properties
+ {
+ const schema = {
+ properties: {
+ foo: {
+ type: 'number',
+ messages: {
+ type: 'will be assigned'
+ }
+ }
+ },
+ messages: {
+ properties: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate({ foo: 'bar' });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+
+ // does not use custom messages on keyword: patternProperties
+ {
+ const schema = {
+ patternProperties: {
+ '^foo$': {
+ type: 'number',
+ messages: {
+ type: 'will be assigned'
+ }
+ }
+ },
+ messages: {
+ patternProperties: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate({ foo: 'bar' });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+
+ // does not use custom messages on keyword: dependencies (schema)
+ {
+ const schema = {
+ dependencies: {
+ foo: {
+ minProperties: 2,
+ messages: {
+ minProperties: 'will be assigned'
+ }
+ }
+ },
+ messages: {
+ dependencies: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate({ foo: 'bar' });
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+
+ // does not use custom messages on keyword: allOf
+ {
+ const schema = {
+ dependencies: {
+ foo: {
+ minProperties: 2,
+ messages: {
+ minProperties: 'will be assigned'
+ }
+ }
+ },
+ allOf: [
+ {
+ minimum: 2,
+ messages: {
+ minimum: 'will not be assigned'
+ }
+ },
+ {
+ maximum: 5,
+ messages: {
+ maximum: 'will be assigned'
+ }
+ }
+ ],
+ messages: {
+ allOf: 'will not be assigned'
+ }
+ };
+
+ const validate = jsen(schema);
+ const valid = validate(6);
+
+ console.assert(!valid);
+ console.assert(validate.errors.length === 1);
+ console.assert(validate.errors[0].message === 'will be assigned');
+ }
+ }
+}
+
+const doesThrow = (func: Function) => {
+ try {
+ func();
+ }
+ catch (e) {
+ return true;
+ }
+
+ return false;
+};
+
+const doesNotThrow = (func: Function) => {
+ return !doesThrow(func);
+};
+
+// fixes
+{
+ // Fix broken inlining of regular expressions containing slashes (#15, #25)
+ {
+ const schema = {
+ type: 'string',
+ pattern: '^/dev/[^/]+(/[^/]+)*$'
+ };
+
+ console.assert(doesNotThrow(jsen(schema)));
+ }
+
+ // Fix code generation breaks when object properties in schema are not valid identifiers (#16)
+ {
+ const schema = {
+ type: 'object',
+ properties: {
+ 123: {
+ type: 'boolean'
+ }
+ }
+ };
+
+ let validate: IJsenValidator;
+
+ console.assert(doesNotThrow(() => {
+ validate = jsen(schema);
+ })
+ );
+
+ console.assert(validate({ 123: true }));
+ }
+
+ // Fix cannot dereference schema when ids change resolution scope (#14)
+ {
+ let schema: any = {
+ $ref: '#child',
+ definitions: {
+ child: {
+ id: '#child',
+ type: 'string'
+ }
+ }
+ };
+
+ let validate: IJsenValidator;
+
+ console.assert(doesNotThrow(() => {
+ validate = jsen(schema);
+ })
+ );
+
+ console.assert(validate('abc'));
+ console.assert(!validate(123));
+
+ schema = {
+ $ref: '#child/definitions/subchild',
+ definitions: {
+ child: {
+ id: '#child',
+ definitions: {
+ subchild: {
+ type: 'number'
+ }
+ }
+ }
+ }
+ };
+
+ console.assert(doesThrow(() => {
+ validate = jsen(schema);
+ })
+ );
+ }
+
+ // Fix recursive calls to the same cached $ref validator resets the error object
+ {
+ const schema = {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ foo: { $ref: '#' }
+ },
+ required: ['foo']
+ }
+ };
+
+ const validate = jsen(schema);
+
+ console.assert(validate([{ foo: [] }]));
+ console.assert(validate([{ foo: [{ foo: [] }] }]));
+ console.assert(!validate([{ bar: [] }]));
+ console.assert(!validate([{ foo: [{ foo: [] }, { bar: [] }] }])); // Bug! False positive
+ }
+}
+
+// format
+{
+ // date-time
+ {
+ const schema = { format: 'date-time' };
+ const validate = jsen(schema);
+
+ console.assert(validate(new Date().toJSON()));
+
+ console.assert(!validate(''));
+ console.assert(!validate(new Date().toUTCString()));
+ console.assert(!validate(new Date().toLocaleDateString()));
+ console.assert(!validate(new Date().toTimeString()));
+ }
+
+ // uri
+ {
+ const schema = { format: 'uri' };
+ const validate = jsen(schema);
+
+ console.assert(validate('http://google.com'));
+ console.assert(validate('ftp://my-site'));
+ console.assert(validate('custom://my-site/long/$cr@mbl3d/u_r-l?with=query%20string'));
+ console.assert(validate('//no-scheme-here'));
+
+ console.assert(!validate(''));
+ console.assert(!validate('google'));
+ console.assert(!validate('/google'));
+ console.assert(!validate('://google'));
+ console.assert(!validate('http://google.com/no space allowed'));
+ }
+
+ // email
+ {
+ const schema = { format: 'email' };
+ const validate = jsen(schema);
+ const maxLongHostname1 = new Array(5).join('.' + new Array(64).join('a')).substr(1); // 255 chars (4 groups x 63 chars)
+ const maxLongHostname2 = new Array(9).join('.' + new Array(32).join('a')).substr(1); // 255 chars (8 groups x 31 chars)
+
+ console.assert(validate('me@domain'));
+ console.assert(validate('first.last+plus-dash#hash!bang$dollar%percent&\'quote*star/dash=equal?question^pow_under`backtick{brace}|bar~tilde@domain'));
+ console.assert(validate('me@domain.with.multiple.subdomains'));
+ console.assert(validate('me@domain-parts.may.contain-dashes'));
+ console.assert(validate('me@a-single-domain-part-can-be-up-to-sixty-three-characters-long63'));
+ console.assert(validate('me@' + maxLongHostname1));
+ console.assert(validate('me@' + maxLongHostname2));
+
+ console.assert(!validate(''));
+ console.assert(!validate('qu"ote\'s@domain'));
+ console.assert(!validate('me@no_underscores+or?special$chars'));
+ console.assert(!validate('me@ends-with-dash-'));
+ console.assert(!validate('me@-starts-with-dash'));
+ console.assert(!validate('me@asingle-domain-part-cannot-be-longer-than-sixty-three-characters'));
+
+ // These verify that a hostname cannot be longer than 255 chars in total. However,
+ // maximum string length verification cannot be performed in the same regex, so
+ // these test cases fail. Users must additionall use the `maxlength` keyword in this case.
+ // assert(!validate('me@' + maxLongHostname1 + '.a'));
+ // assert(!validate('me@' + maxLongHostname2 + '.a'));
+ }
+
+ // ipv4
+ {
+ const schema = { format: 'ipv4' };
+ const validate = jsen(schema);
+
+ console.assert(validate('0.0.0.0'));
+ console.assert(validate('255.255.255.255'));
+ console.assert(validate('127.0.0.1'));
+
+ console.assert(!validate(''));
+ console.assert(!validate('...'));
+ console.assert(!validate('0.0.0.-1'));
+ console.assert(!validate('0.0.-1.0'));
+ console.assert(!validate('0.-1.0.0'));
+ console.assert(!validate('-1.0.0.0'));
+ console.assert(!validate('256.0.0.0'));
+ console.assert(!validate('0.256.0.0'));
+ console.assert(!validate('0.0.256.0'));
+ console.assert(!validate('0.0.0.256'));
+ }
+
+ // ipv6
+ {
+ const schema = { format: 'ipv6' };
+ const validate = jsen(schema);
+
+ console.assert(validate('1:2:3:4:5:6:7:8'));
+ console.assert(validate('1::'));
+ console.assert(validate('1:2:3:4:5:6:7::'));
+ console.assert(validate('1::8'));
+ console.assert(validate('1:2:3:4:5:6::8'));
+ console.assert(validate('1::7:8'));
+ console.assert(validate('1:2:3:4:5::7:8'));
+ console.assert(validate('1:2:3:4:5::8'));
+ console.assert(validate('1::6:7:8'));
+ console.assert(validate('1:2:3:4::6:7:8'));
+ console.assert(validate('1:2:3:4::8'));
+ console.assert(validate('1::5:6:7:8'));
+ console.assert(validate('1:2:3::5:6:7:8'));
+ console.assert(validate('1:2:3::8'));
+ console.assert(validate('1::4:5:6:7:8'));
+ console.assert(validate('1:2::4:5:6:7:8'));
+ console.assert(validate('1:2::8'));
+ console.assert(validate('1::3:4:5:6:7:8'));
+ console.assert(validate('1::8'));
+ console.assert(validate('::2:3:4:5:6:7:8'));
+ console.assert(validate('::8'));
+ console.assert(validate('::'));
+
+ // link-local IPv6 addresses with zone index
+ console.assert(validate('fe80::7:8%eth0'));
+ console.assert(validate('fe80::7:8%1'));
+
+ // IPv4-mapped IPv6 addresses and IPv4-translated addresses
+ console.assert(validate('::255.255.255.255'));
+ console.assert(validate('::ffff:255.255.255.255'));
+ console.assert(validate('::ffff:0:255.255.255.255'));
+
+ // IPv4-Embedded IPv6 Address
+ console.assert(validate('2001:db8:3:4::192.0.2.33'));
+ console.assert(validate('64:ff9b::192.0.2.33'));
+
+ console.assert(!validate(''));
+ console.assert(!validate('::_'));
+
+ // TODO: we may need more invalid cases here
+ }
+
+ // hostname
+ {
+ const schema = { format: 'hostname' },
+ validate = jsen(schema),
+ maxLong1 = new Array(5).join('.' + new Array(64).join('a')).substr(1), // 255 chars (4 groups x 63 chars)
+ maxLong2 = new Array(9).join('.' + new Array(32).join('a')).substr(1); // 255 chars (8 groups x 31 chars)
+
+ console.assert(validate('my.host'));
+ console.assert(validate('host'));
+ console.assert(validate('domain.with.multiple.subdomains'));
+ console.assert(validate('domain-parts.may.contain-dashes'));
+ console.assert(validate('a-single-domain-part-can-be-up-to-sixty-three-characters-long63'));
+ console.assert(validate(maxLong1));
+ console.assert(validate(maxLong2));
+
+ console.assert(!validate(''));
+ console.assert(!validate('me@domain'));
+ console.assert(!validate('qu"ote\'s'));
+ console.assert(!validate('no_underscores+or?special$chars'));
+ console.assert(!validate('ends-with-dash-'));
+ console.assert(!validate('-starts-with-dash'));
+ console.assert(!validate('asingle-domain-part-cannot-be-longer-than-sixty-three-characters'));
+
+ // These verify that a hostname cannot be longer than 255 chars in total. However,
+ // maximum string length verification cannot be performed in the same regex, so
+ // these test cases fail. Users must additionall use the `maxlength` keyword in this case.
+ // assert(!validate(maxLong1 + '.a'));
+ // assert(!validate(maxLong2 + '.a'));
+ }
+
+ // custom format
+ {
+ // accepts string
+ {
+ const schema = { format: 'custom' },
+ custom = '^\\d+$',
+ validate = jsen(schema, {
+ formats: {
+ custom: custom
+ }
+ });
+
+ console.assert(validate('123'));
+ console.assert(!validate('a123'));
+ }
+
+ // accepts regex
+ {
+ const schema = { format: 'custom' },
+ custom = /^\d+$/,
+ validate = jsen(schema, {
+ formats: {
+ custom: custom
+ }
+ });
+
+ console.assert(validate('123'));
+ console.assert(!validate('a123'));
+ }
+
+ // accepts function
+ {
+ let schema = { format: 'custom' },
+ callCount = 0,
+ custom = (value: any, childSchema: any) => {
+ console.assert(value.indexOf('123') > -1);
+ console.assert(childSchema === schema);
+
+ callCount++;
+
+ return /^\d+$/.test(value);
+ },
+ validate = jsen(schema, {
+ formats: {
+ custom: custom
+ }
+ });
+
+ console.assert(validate('123'));
+ console.assert(!validate('a123'));
+ console.assert(callCount === 2);
+ }
+
+ // is run for all types
+ {
+ let schema = { format: 'custom' },
+ callCount = 0,
+ options = {
+ formats: {
+ custom: () => {
+ callCount++;
+ return true;
+ }
+ }
+ },
+ validate = jsen(schema, options),
+ data = [
+ undefined,
+ null,
+ 'abc',
+ 123,
+ Math.PI,
+ true,
+ false,
+ {},
+ [],
+ new Date()
+ ];
+
+ data.forEach((dataItem) => {
+ validate(dataItem);
+ console.assert(callCount === 1);
+ callCount = 0;
+ });
+ }
+
+ // is not run if a built-in keyword fails
+ {
+ let schema = {
+ format: 'custom',
+ type: 'number',
+ maximum: 10
+ },
+ callCount = 0,
+ options = {
+ formats: {
+ custom: () => {
+ callCount++;
+ return true;
+ }
+ }
+ },
+ validate = jsen(schema, options);
+
+ console.assert(!validate(123));
+ console.assert(callCount === 0);
+
+ console.assert(validate(7));
+ console.assert(callCount === 1);
+ }
+
+ // common scenarios
+ {
+ // verify passwords match
+ {
+ let schema = {
+ description: 'User account creation form',
+ type: 'object',
+ properties: {
+ password: {
+ type: 'string',
+ minLength: 8
+ },
+ password_confirm: {
+ type: 'string',
+ minLength: 8
+ }
+ },
+ format: 'passwordsMatch'
+ },
+ options = {
+ formats: {
+ passwordsMatch: (obj: any) => {
+ callCount++;
+ return obj.password === obj.password_confirm;
+ }
+ }
+ },
+ data = {
+ password: '1234567',
+ password_confirm: '1234567'
+ },
+ validate = jsen(schema, options),
+ callCount = 0;
+
+ console.assert(!validate(data)); // minLength validator failed
+ console.assert(callCount === 0);
+
+ data.password += '8';
+ data.password_confirm += '9';
+
+ console.assert(!validate(data)); // custom validator failed
+ console.assert(callCount === 1);
+
+ data.password_confirm = data.password;
+
+ console.assert(validate(data)); // OK
+ console.assert(callCount === 2);
+ }
+ }
+ }
+}
+
+// option: greedy
+{
+ // validates as many keywords as possible
+ {
+ let schema = {
+ type: 'object',
+ properties: {
+ test1: {
+ type: 'string'
+ },
+ test2: {
+ type: 'object',
+ properties: {
+ test21: {
+ type: 'number'
+ }
+ }
+ },
+ test3: {
+ type: 'number'
+ },
+ test4: { $ref: '#external' }
+ },
+ additionalProperties: false
+ },
+ options = {
+ greedy: true,
+ schemas: {
+ external: {
+ type: 'string'
+ }
+ }
+ },
+ validate = jsen(schema, options),
+ invalidTest = {
+ test1: 1,
+ test2: '2',
+ test3: 'j',
+ test4: 4
+ },
+ ret = validate(invalidTest);
+
+ console.assert(!ret); // false
+ console.assert(JSON.stringify(validate.errors)
+ === JSON.stringify([
+ { path: 'test1', keyword: 'type' },
+ { path: 'test2', keyword: 'type' },
+ { path: 'test3', keyword: 'type' },
+ { path: 'test4', keyword: 'type' }
+ ])
+ );
+
+ delete options.greedy;
+
+ validate = jsen(schema, options);
+
+ ret = validate(invalidTest);
+
+ console.assert(!ret); // false
+ console.assert(JSON.stringify(validate.errors)
+ === JSON.stringify([{ path: 'test1', keyword: 'type' }])
+ );
+ }
+
+ // does not descend into invalid objects
+ {
+ let schema = {
+ type: 'object',
+ properties: {
+ test1: { type: 'object' },
+ test2: {
+ required: ['foo']
+ },
+ test3: {
+ properties: {
+ foo: { type: 'string' }
+ }
+ },
+ test4: {
+ type: 'array',
+ items: {
+ type: 'object',
+ required: ['foo']
+ }
+ }
+ }
+ },
+ options = { greedy: true },
+ data = {
+ test1: 123,
+ test2: {},
+ test3: 123,
+ test4: [{}, { foo: 123 }, null]
+ },
+ validate = jsen(schema, options),
+ ret = validate(data);
+
+ console.assert(!ret);
+ console.assert(JSON.stringify(validate.errors)
+ === JSON.stringify([
+ { path: 'test1', keyword: 'type' },
+ { path: 'test2.foo', keyword: 'required' },
+ { path: 'test4.0.foo', keyword: 'required' },
+ { path: 'test4.2', keyword: 'type' }
+ ])
+ );
+ }
+}
+
+// jsen
+{
+ // is a function
+ {
+ console.assert(typeof jsen === "function");
+ }
+
+ // throws if schema is not an object
+ {
+ console.assert(doesThrow(jsen()));
+ console.assert(doesThrow(jsen(null)));
+ console.assert(doesThrow(jsen(false)));
+ console.assert(doesThrow(jsen(123)));
+ console.assert(doesThrow(jsen("abc")));
+ console.assert(doesThrow(jsen([])));
+ console.assert(doesNotThrow(jsen({})));
+ }
+
+ // produces a function
+ {
+ const validate = jsen({});
+ console.assert(typeof validate === 'function');
+ console.assert(validate() === true);
+ // console.assert(validate.error === null);
+ }
+}
+
+// type: integer
+{
+ // required
+ {
+ const schema = { type: 'integer' },
+ validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+ console.assert(validate(123));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['integer', 'null'] },
+ validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(null));
+ console.assert(validate(123));
+ }
+
+ // type
+ {
+ const schema = { type: 'integer' },
+ validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate(true));
+ console.assert(!validate(false));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(13));
+ }
+
+ // enum
+ {
+ const schema = { type: 'integer', enum: [1, 3, 5, 7] },
+ validate = jsen(schema);
+
+ console.assert(!validate(4));
+ console.assert(validate(5));
+ }
+
+ // minimum
+ {
+ const schema = { type: 'integer', minimum: 7 },
+ validate = jsen(schema);
+
+ console.assert(!validate(6));
+
+ console.assert(validate(7));
+ console.assert(validate(999));
+ }
+
+ // exclusiveMinimum
+ {
+ const schema = {
+ type: 'integer',
+ minimum: 7,
+ exclusiveMinimum: true
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(6));
+ console.assert(!validate(7));
+
+ console.assert(validate(8));
+ console.assert(validate(999));
+ }
+
+ // maximum
+ {
+ const schema = { type: 'integer', maximum: 77 },
+ validate = jsen(schema);
+
+ console.assert(!validate(78));
+
+ console.assert(validate(-12));
+ console.assert(validate(76));
+ console.assert(validate(77));
+ }
+
+ // exclusiveMaximum
+ {
+ const schema = {
+ type: 'integer',
+ maximum: 77,
+ exclusiveMaximum: true
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(77));
+ console.assert(!validate(78));
+
+ console.assert(validate(-12));
+ console.assert(validate(75));
+ console.assert(validate(76));
+ }
+
+ // multipleOf
+ {
+ const schema = { type: 'integer', multipleOf: 7 },
+ validate = jsen(schema);
+
+ console.assert(!validate(8));
+
+ console.assert(validate(14));
+ console.assert(validate(-49));
+ console.assert(validate(77));
+ }
+}
+
+// missing $ref
+{
+ // passes validation with ignore missing $ref
+ {
+ let schema = {
+ type: 'object',
+ properties: {
+ test1: { $ref: '#external1' },
+ test2: {
+ type: 'number'
+ },
+ test3: { $ref: '#external3' } //missing
+ },
+ additionalProperties: false
+ },
+ external1 = {
+ type: 'object',
+ properties: {
+ test11: { $ref: '#external11' }, //missing
+ test12: {
+ type: 'number'
+ },
+ test13: { $ref: '#external11' } //duplicate
+ }
+ },
+ validate = jsen(schema, {
+ schemas: {
+ external1: external1
+ },
+ missing$Ref: true
+ }),
+ missingTest = {
+ test1: {
+ test11: 'missing',
+ test12: 5,
+ test13: 'missing too'
+ },
+ test2: 2,
+ test3: 3
+ },
+ invalidTest = {
+ test1: {
+ test11: 'missing',
+ test12: 5,
+ test13: 'missing too'
+ },
+ test2: 'fail',
+ test3: 3
+ },
+ ret: boolean;
+
+ ret = validate(missingTest);
+ console.assert(ret); // true
+
+ ret = validate(invalidTest);
+ console.assert(!ret); // !false
+ }
+}
+
+// multi schema
+{
+ // allOf
+ {
+ const schema = {
+ allOf: [
+ { type: 'number' },
+ { type: 'integer' }
+ ]
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(null));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(0));
+ console.assert(validate(777));
+ console.assert(validate(-9));
+ }
+
+ // anyOf
+ {
+ const schema = {
+ anyOf: [
+ { type: 'string' },
+ { type: 'number' }
+ ]
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(null));
+ console.assert(!validate(true));
+ console.assert(!validate({}));
+ console.assert(!validate([]));
+
+ console.assert(validate('abc'));
+ console.assert(validate(123));
+ console.assert(validate(''));
+ console.assert(validate(0));
+ }
+
+ // oneOf
+ {
+ const schema = {
+ oneOf: [
+ { type: 'number', maximum: 5 },
+ { type: 'number', minimum: 3 }
+ ]
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(null));
+ console.assert(!validate(true));
+ console.assert(!validate({}));
+ console.assert(!validate([]));
+ // matches both validators
+ console.assert(!validate(3));
+
+ console.assert(validate(0));
+ console.assert(validate(1));
+ console.assert(validate(2));
+ console.assert(validate(6));
+ console.assert(validate(17));
+ }
+
+ // not
+ {
+ const schema = {
+ not: {
+ type: 'array'
+ }
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate([]));
+
+ console.assert(validate(0));
+ console.assert(validate(false));
+ console.assert(validate('abc'));
+ console.assert(validate({}));
+ console.assert(validate(null));
+ console.assert(validate());
+ }
+}
+
+// type: null
+{
+ // required
+ {
+ const schema = { type: 'null' },
+ validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+ console.assert(validate(null));
+ }
+
+ // type
+ {
+ const schema = { type: 'null' },
+ validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(null));
+ }
+}
+
+// type: number
+{
+ // required
+ {
+ const schema = { type: 'number' },
+ validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate(Math.PI));
+ console.assert(validate(123));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['number', 'null'] },
+ validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(null));
+ console.assert(validate(Math.PI));
+ }
+
+ // type
+ {
+ const schema = { type: 'number' },
+ validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate(true));
+ console.assert(!validate(false));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+
+ console.assert(validate(13));
+ console.assert(validate(17.8));
+ console.assert(validate(Math.PI));
+ }
+
+ // enum
+ {
+ const schema = {
+ type: 'number',
+ enum: [1, Math.E, 3, 5, 7]
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(4));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(5));
+ console.assert(validate(Math.E));
+ }
+
+ // minimum
+ {
+ const schema = { type: 'number', minimum: 7 },
+ validate = jsen(schema);
+
+ console.assert(!validate(6));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(7));
+ console.assert(validate(999));
+ }
+
+ // exclusiveMinimum
+ {
+ const schema = {
+ type: 'number',
+ minimum: 7,
+ exclusiveMinimum: true
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(6));
+ console.assert(!validate(7));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate(8));
+ console.assert(validate(999));
+ }
+
+ // maximum
+ {
+ const schema = { type: 'number', maximum: 77 },
+ validate = jsen(schema);
+
+ console.assert(!validate(77.000001));
+ console.assert(!validate(78));
+
+ console.assert(validate(-12));
+ console.assert(validate(76));
+ console.assert(validate(77));
+ console.assert(validate(Math.PI));
+ }
+
+ // exclusiveMaximum
+ {
+ const schema = {
+ type: 'number',
+ maximum: 77,
+ exclusiveMaximum: true
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate(77));
+ console.assert(!validate(78));
+
+ console.assert(validate(-12));
+ console.assert(validate(75));
+ console.assert(validate(76));
+ console.assert(validate(76.99999));
+ }
+
+ // multipleOf
+ {
+ let schema = { type: 'number', multipleOf: 7 },
+ validate = jsen(schema);
+
+ console.assert(!validate(8));
+
+ console.assert(validate(14));
+ console.assert(validate(-49));
+ console.assert(validate(77));
+
+ schema = {
+ type: 'number',
+ multipleOf: 3.14 // Math.PI
+ };
+
+ validate = jsen(schema);
+
+ console.assert(!validate(2.5));
+
+ console.assert(validate(9.42)); // 3 * Math.PI
+ }
+
+ // fix multipleOf doesn't validate data for decimal point (#1)
+ {
+ const schema = { type: 'number', multipleOf: 0.01 },
+ validate = jsen(schema);
+
+ console.assert(validate(18.15));
+ }
+}
+
+// type: object
+{
+ // required
+ {
+ const schema = { type: 'object' },
+ validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate({}));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['object', 'null'] },
+ validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(null));
+ console.assert(validate({}));
+ }
+
+ // type
+ {
+ const schema = { type: 'object' },
+ validate = jsen(schema);
+
+ console.assert(!validate('123'));
+ console.assert(!validate(false));
+ console.assert(!validate([]));
+ console.assert(!validate(Math.PI));
+
+ console.assert(validate({}));
+ console.assert(jsen({ type: 'object', properties: {} }, {})());
+ }
+
+ // maxProperties
+ {
+ const schema = { type: 'object', maxProperties: 3 },
+ validate = jsen(schema);
+
+ console.assert(!validate({ a: 1, b: 2, c: 3, d: 4 }));
+
+ console.assert(validate({}));
+ console.assert(validate({ a: 1 }));
+ console.assert(validate({ a: 1, b: 2 }));
+ console.assert(validate({ a: 1, b: 2, c: 3 }));
+ }
+
+ // minProperties
+ {
+ const schema = { type: 'object', minProperties: 2 },
+ validate = jsen(schema);
+
+ console.assert(!validate({}));
+ console.assert(!validate({ a: 1 }));
+
+ console.assert(validate({ a: 1, b: 2 }));
+ console.assert(validate({ a: 1, b: 2, c: 3 }));
+ }
+
+ // required properties
+ {
+ const schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' },
+ c: { type: 'boolean' }
+ },
+ required: ['a', 'b']
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate({}));
+ console.assert(!validate({ c: true }));
+ console.assert(!validate({ a: 'abc', c: true }));
+ console.assert(!validate({ b: 123, c: true }));
+ console.assert(!validate({ a: 'abc', b: undefined }));
+
+ console.assert(validate({ a: 'abc', b: 123 }));
+ console.assert(validate({ a: 'abc', b: 123, c: true }));
+ }
+
+ // additionalProperties
+ {
+ let schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' }
+ },
+ additionalProperties: true
+ },
+ validate = jsen(schema);
+
+ console.assert(validate({ a: 'abc' }));
+ console.assert(validate({ b: 123 }));
+ console.assert(validate({ a: 'abc', b: 123 }));
+ console.assert(validate({ a: 'abc', b: 123, c: true }));
+
+ schema.additionalProperties = false;
+ validate = jsen(schema);
+
+ console.assert(!validate({ c: true }));
+ console.assert(!validate({ a: 'abc', b: 123, c: true }));
+
+ console.assert(validate({ a: 'abc', b: 123 }));
+ console.assert(jsen({ type: 'object', additionalProperties: false })({}));
+ }
+
+ // additionalProperties as schema
+ {
+ const schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' }
+ },
+ additionalProperties: {
+ type: 'boolean'
+ }
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate({ a: 'abc', b: 123, c: 123 }));
+
+ console.assert(validate({ a: 'abc', b: 123, c: false }));
+ }
+
+ // additionalProperties with patternProperties
+ {
+ let schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' }
+ },
+ patternProperties: {
+ '^b': { type: 'number' }
+ },
+ additionalProperties: true
+ },
+ validate = jsen(schema);
+
+ console.assert(validate({ a: 'abc' }));
+ console.assert(validate({ b: 123 }));
+ console.assert(validate({ a: 'abc', b: 123, bar: Math.E, baz: Math.PI }));
+ console.assert(validate({ a: 'abc', baz: 123, c: true }));
+
+ schema.additionalProperties = false;
+ validate = jsen(schema);
+
+ console.assert(!validate({ c: true }));
+ console.assert(!validate({ a: 'abc', bar: 123, c: true }));
+
+ console.assert(validate({ a: 'abc', baz: 123 }));
+ console.assert(jsen({ type: 'object', additionalProperties: false })({}));
+ }
+
+ // patternProperties
+ {
+ const schema = {
+ type: 'object',
+ patternProperties: {
+ '^a': { type: 'string' },
+ '^b': { type: 'number' }
+ }
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate({ a: 123 }));
+ console.assert(!validate({ b: 'abc' }));
+
+ console.assert(validate({}));
+ console.assert(validate({ a: 'abc' }));
+ console.assert(validate({ b: 123 }));
+ console.assert(validate({ a: 'abc', b: 123 }));
+ }
+
+ // dependencies: schema
+ {
+ const schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' }
+ },
+ dependencies: {
+ a: {
+ type: 'object',
+ required: ['c'],
+ properties: {
+ c: { type: 'boolean' }
+ }
+ },
+ b: {
+ type: 'object',
+ required: ['f'],
+ properties: {
+ f: { type: 'null' }
+ }
+ },
+ g: {
+ type: 'object',
+ required: ['b'],
+ properties: {
+ b: {
+ type: 'integer'
+ }
+ }
+ }
+ }
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate({ a: 'abc' }));
+ console.assert(!validate({ a: 'abc', c: 123 }));
+ console.assert(!validate({ b: Math.PI, f: false }));
+ console.assert(!validate({ b: Math.PI, g: null }));
+
+ console.assert(validate({}));
+ console.assert(validate({ a: 'abc', c: false }));
+ console.assert(validate({ b: Math.PI, f: null }));
+ console.assert(validate({ b: 123, g: 'any value', f: null }));
+ }
+
+ // dependencies: property
+ {
+ const schema = {
+ type: 'object',
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' },
+ c: { type: 'boolean' }
+ },
+ dependencies: {
+ a: ['b', 'c']
+ }
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate({ a: 'abc' }));
+ console.assert(!validate({ a: 'abc', b: 123 }));
+
+ console.assert(validate({}));
+ console.assert(validate({ a: 'abc', b: 123, c: false }));
+ }
+
+ // nested graph
+ {
+ const schema = {
+ type: ['object', 'null'],
+ properties: {
+ a: { type: 'string' },
+ b: { type: 'number' },
+ c: {
+ type: 'array',
+ items: { type: 'boolean' }
+ }
+ },
+ required: ['a']
+ },
+ validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate({}));
+ console.assert(!validate({ a: 123 }));
+ console.assert(!validate({ a: 'abc', b: false }));
+ console.assert(!validate({ a: 'abc', c: [null] }));
+
+ console.assert(validate(null));
+ console.assert(validate({ a: 'abc', b: 123.4, c: [true, false] }));
+ console.assert(validate({ a: 'abc', b: 0 }));
+ console.assert(validate({ a: 'abc', c: [true, false] }));
+ console.assert(validate({ a: 'abc', c: [] }));
+ }
+}
+
+// $ref
+{
+ // throws if string is not in correct format
+ {
+ console.assert(doesThrow(
+ jsen({ $ref: '' })
+ ));
+
+ console.assert(doesThrow(
+ jsen({ $ref: '#double//slash' })
+ ));
+
+ console.assert(doesThrow(
+ jsen({ $ref: '#ends/with/slash/' })
+ ));
+
+ console.assert(doesThrow(
+ // invalid reference, non-existent schema properties
+ jsen({ $ref: '#a/b/c' })
+ ));
+
+ console.assert(doesNotThrow(
+ // schema resolves to itself
+ jsen({ $ref: '#' })
+ ));
+
+ console.assert(doesNotThrow(
+ jsen({
+ a: {
+ b: {
+ c: {
+ type: 'any'
+ }
+ }
+ },
+ $ref: '#/a/b/c'
+ })
+ ));
+
+ console.assert(doesNotThrow(
+ jsen({
+ arr: [
+ { value: { type: 'string' } },
+ { value: { type: 'number' } },
+ { value: { type: 'boolean' } }
+ ],
+ type: 'object',
+ properties: {
+ a: { $ref: '#arr/2/value' }
+ }
+ })
+ ));
+ }
+
+ // external schema
+ {
+ // finds external schema with a hash
+ {
+ const external = { type: 'string' },
+ schema = { $ref: '#external' },
+ validate = jsen(schema, {
+ schemas: {
+ external: external
+ }
+ });
+
+ console.assert(validate('abc'));
+ console.assert(!validate(123));
+ }
+
+ // finds external schema without a hash
+ {
+ const external = { type: 'string' },
+ schema = { $ref: 'external' },
+ validate = jsen(schema, {
+ schemas: {
+ external: external
+ }
+ });
+
+ console.assert(validate('abc'));
+ console.assert(!validate(123));
+ }
+
+ // throws when no external schema found
+ {
+ const schema = { $ref: '#external' };
+
+ console.assert(doesNotThrow(
+ jsen(schema)
+ ));
+ }
+
+ // own property takes precendence over external schema
+ {
+ const external = { type: 'string' },
+ schema = {
+ external: { type: 'number' },
+ $ref: '#external'
+ },
+ validate = jsen(schema, {
+ schemas: {
+ external: external
+ }
+ });
+
+ console.assert(!validate('abc'));
+ console.assert(validate(123));
+ }
+
+ // external schemas have their own dereferencing scope
+ {
+ const external = {
+ inner: { type: 'string' },
+ $ref: '#inner'
+ },
+ schema = {
+ inner: { type: 'number' },
+ $ref: '#external'
+ },
+ validate = jsen(schema, {
+ schemas: {
+ external: external
+ }
+ });
+
+ console.assert(validate('abc'));
+ console.assert(!validate(123));
+ }
+ }
+}
+
+// type: string
+{
+ // required
+ {
+ const schema = { type: 'string' },
+ validate = jsen(schema);
+
+ console.assert(!validate());
+ console.assert(!validate(null));
+
+ console.assert(validate('abc'));
+ }
+
+ // nullable
+ {
+ const schema = { type: ['string', 'null'] },
+ validate = jsen(schema);
+
+ console.assert(!validate(undefined));
+
+ console.assert(validate(null));
+ console.assert(validate(''));
+ }
+
+ // type
+ {
+ const schema = { type: 'string' },
+ validate = jsen(schema);
+
+ console.assert(!validate(123));
+ console.assert(!validate(true));
+ console.assert(!validate(false));
+ console.assert(!validate(0));
+ console.assert(!validate([]));
+ console.assert(!validate({}));
+
+ console.assert(validate('abc'));
+ }
+
+ // enum
+ {
+ const schema = { type: 'string', enum: ['a', 'b', 'c'] },
+ validate = jsen(schema);
+
+ console.assert(!validate('not in enum'));
+ console.assert(validate('b'));
+ }
+
+ // minLength
+ {
+ const schema = { type: 'string', minLength: 10 },
+ validate = jsen(schema);
+
+ console.assert(!validate('too short'));
+ console.assert(validate('just long enough'));
+ }
+
+ // maxLength
+ {
+ const schema = { type: 'string', maxLength: 12 },
+ validate = jsen(schema);
+
+ console.assert(!validate('this string is too long'));
+ console.assert(validate('short enough'));
+ }
+
+ // pattern
+ {
+ let schema: any = { type: 'string', pattern: '\\d' },
+ validate = jsen(schema);
+
+ console.assert(!validate('a'));
+ console.assert(validate('1'));
+
+ schema = { type: 'string', pattern: /\d/ };
+ validate = jsen(schema);
+
+ console.assert(!validate('a'));
+ console.assert(validate('1'));
+ }
+}
+
+// unique
+{
+ // filters unique values
+ {
+ const inputs = [
+ [1, 'a', 3, false, null, undefined],
+ ['abc', 123, true, 123, false, Math.PI, 'abc', true, null, null]
+ ],
+ expected = [
+ [1, 'a', 3, false, null, undefined],
+ ['abc', 123, true, false, Math.PI, null]
+ ];
+
+ for (let i = 0; i < inputs.length; i++) {
+ console.assert(JSON.stringify(jsen.unique(inputs[i])) === JSON.stringify(expected[i]));
+ }
+ }
+
+ // performs deep equality checks
+ {
+ const input: any = [
+ {},
+ { a: 1 },
+ { b: { c: { d: 123, f: null }, e: 'abc' } },
+ [1, 2, 3],
+ [{ a: 213 }],
+ { b: 1 },
+ { a: 1, b: undefined },
+ { b: { e: 'abc', c: { f: null, d: 123 } } },
+ [1, 2, 3],
+ [{ a: 213 }]
+ ],
+ expected: any = [
+ {},
+ { a: 1 },
+ { b: { c: { d: 123, f: null }, e: 'abc' } },
+ [1, 2, 3],
+ [{ a: 213 }],
+ { b: 1 },
+ { a: 1, b: undefined }
+ ];
+
+ console.assert(JSON.stringify(jsen.unique(input)) === JSON.stringify(expected));
+ }
+
+ // unique.findIndex
+ {
+ // finds an item index with comparator
+ {
+ const arr = [{}, { a: 1 }, { a: 1, b: 2 }],
+ expected = 2,
+ comparator = (obj1: any, obj2: any) => {
+ return obj1.a === obj2.a && obj1.b === obj2.b;
+ };
+
+ console.assert(jsen.unique.findIndex(arr, { a: 1, b: 2 }, comparator) === expected);
+ }
+
+ // returns -1 when item cannot be found
+ {
+ const arr = [{}, { a: 1 }, { a: 1, b: 2 }],
+ expected = -1,
+ comparator = (obj1: any, obj2: any) => {
+ return obj1.a === obj2.a && obj1.b === obj2.b;
+ };
+
+ console.assert(jsen.unique.findIndex(arr, { a: 1, b: null }, comparator) === expected);
+ }
+ }
+}
diff --git a/jsen/jsen.d.ts b/jsen/jsen.d.ts
new file mode 100644
index 000000000..709ebe1ea
--- /dev/null
+++ b/jsen/jsen.d.ts
@@ -0,0 +1,42 @@
+// Type definitions for jsen (JSON Sentinel)
+// Project: https://github.com/bugventure/jsen
+// Definitions by: Vladimir Đokić
+// Definitions: https://github.com/borisyankov/DefinitelyTyped
+
+interface IJsenFormats {
+ [key: string]: string | RegExp | Function;
+}
+
+interface IJSenSettings {
+ missing$Ref?: boolean;
+ greedy?: boolean;
+ copy?: boolean;
+ additionalProperties?: boolean;
+ formats?: IJsenFormats;
+ schemas?: any;
+}
+
+interface IJsenValidator {
+ (data?: any): boolean;
+ build(initial?: any, options?: any): any;
+ errors?: IValidateError[];
+}
+
+interface IValidateError {
+ path: string;
+ keyword: string;
+ message?: string;
+}
+
+interface IJsenUnique {
+ (array: any[]): boolean;
+ findIndex(array: any[], value: any, comparator: (obj1: any, obj2: any) => boolean): number;
+}
+interface IJsen {
+ (schema?: any, options?: IJSenSettings): IJsenValidator;
+ clone(data: any): any;
+ equal(a: any, b: any): boolean;
+ unique: IJsenUnique;
+}
+
+declare var jsen: IJsen;