From ed673f6e649967f0f387b0d2a52ceeffb164902f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20=C4=90oki=C4=87?= Date: Wed, 23 Mar 2016 14:19:01 +0100 Subject: [PATCH] Initial commit of jsen (JSON Sentinel) typescript definition. --- jsen/jsen-tests.ts | 3503 ++++++++++++++++++++++++++++++++++++++++++++ jsen/jsen.d.ts | 42 + 2 files changed, 3545 insertions(+) create mode 100644 jsen/jsen-tests.ts create mode 100644 jsen/jsen.d.ts 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;