import {expect} from 'chai'; import {DataType} from './data-type.js'; import {format} from '@e22m4u/js-format'; import {DataParser} from './data-parser.js'; import {DataSchemaRegistry} from './data-schema-registry.js'; import { arrayTypeParser, numberTypeParser, objectTypeParser, stringTypeParser, booleanTypeParser, } from './data-parsers/index.js'; describe('DataParser', function () { describe('getParsers', function () { it('should return a default parser list', function () { const S = new DataParser(); const res = S.getParsers(); expect(res).to.be.eql([ stringTypeParser, booleanTypeParser, numberTypeParser, arrayTypeParser, objectTypeParser, ]); }); it('should return a modified parser list', function () { const S = new DataParser(); const parser1 = () => undefined; const parser2 = () => undefined; S.setParsers([parser1, parser2]); const res = S.getParsers(); expect(res).to.be.eql([parser1, parser2]); }); }); describe('setParsers', function () { it('should require a given value to be an array', function () { const S = new DataParser(); const throwable = v => () => S.setParsers(v); const error = s => format('Data parsers must be an Array, but %s was given.', s); expect(throwable('str')).to.throw(error('"str"')); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable(true)).to.throw(error('true')); expect(throwable(false)).to.throw(error('false')); expect(throwable({})).to.throw(error('Object')); expect(throwable(null)).to.throw(error('null')); expect(throwable(undefined)).to.throw(error('undefined')); throwable([])(); }); it('should require given parsers to be a function', function () { const S = new DataParser(); const throwable = v => () => S.setParsers([v]); const error = s => format('Data parser must be a Function, but %s was given.', s); expect(throwable('str')).to.throw(error('"str"')); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable(true)).to.throw(error('true')); expect(throwable(false)).to.throw(error('false')); expect(throwable([])).to.throw(error('Array')); expect(throwable({})).to.throw(error('Object')); expect(throwable(null)).to.throw(error('null')); expect(throwable(undefined)).to.throw(error('undefined')); throwable(() => undefined)(); }); it('should set the parsers list', function () { const S = new DataParser(); const parser1 = () => undefined; const parser2 = () => undefined; S.setParsers([parser1, parser2]); const res1 = S.getParsers(); expect(res1).to.be.eql([parser1, parser2]); }); it('should able to clean the parser list', function () { const S = new DataParser(); const parser1 = () => undefined; const parser2 = () => undefined; S.setParsers([parser1, parser2]); const res1 = S.getParsers(); expect(res1).to.be.eql([parser1, parser2]); S.setParsers([]); const res2 = S.getParsers(); expect(res2).to.be.eql([]); }); }); describe('defineSchema', function () { it('should pass a given schema definition to the registry', function () { const S = new DataParser(); const registry = S.getService(DataSchemaRegistry); const schemaDef = {name: 'mySchema', schema: {}}; S.defineSchema(schemaDef); const res = registry.getDefinition(schemaDef.name); expect(res).to.be.eql(schemaDef); }); it('should return a current instance', function () { const S = new DataParser(); const schemaDef = {name: 'mySchema', schema: {}}; const res = S.defineSchema(schemaDef); expect(res).to.be.eq(S); }); }); describe('hasSchema', function () { it('should return true if a given name is registered', function () { const S = new DataParser(); const schemaDef = {name: 'mySchema', schema: {}}; expect(S.hasSchema(schemaDef.name)).to.be.false; S.defineSchema(schemaDef); expect(S.hasSchema(schemaDef.name)).to.be.true; }); }); describe('getSchema', function () { it('should return a register schema for a given name', function () { const S = new DataParser(); const schemaDef = {name: 'mySchema', schema: {}}; S.defineSchema(schemaDef); const res = S.getSchema(schemaDef.name); expect(res).to.be.eql(schemaDef.schema); }); it('should throw an error if a given name is not registered', function () { const S = new DataParser(); const throwable = () => S.getSchema('mySchema'); expect(throwable).to.throw('Data schema "mySchema" is not found.'); }); }); describe('parse', function () { it('should require the "options" argument to be an object', function () { const S = new DataParser(); const throwable = v => () => S.parse(10, {}, v); const error = s => format('Parsing options must be an Object, but %s was given.', s); expect(throwable('str')).to.throw(error('"str"')); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable(true)).to.throw(error('true')); expect(throwable(false)).to.throw(error('false')); expect(throwable([])).to.throw(error('Array')); expect(throwable(null)).to.throw(error('null')); expect(throwable(() => undefined)).to.throw(error('Function')); throwable({})(); throwable(undefined)(); }); it('should require the "sourcePath" argument to be a non-empty string', function () { const S = new DataParser(); const throwable = v => () => S.parse(10, {}, {sourcePath: v}); const error = s => format( 'Option "sourcePath" must be a non-empty String, but %s was given.', s, ); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable(true)).to.throw(error('true')); expect(throwable(false)).to.throw(error('false')); expect(throwable([])).to.throw(error('Array')); expect(throwable({})).to.throw(error('Object')); expect(throwable(null)).to.throw(error('null')); expect(throwable(() => undefined)).to.throw(error('Function')); throwable('str')(); throwable(undefined)(); }); it('should require the "shallowMode" argument to be a boolean', function () { const S = new DataParser(); const throwable = v => () => S.parse(10, {}, {shallowMode: v}); const error = s => format('Option "shallowMode" must be a Boolean, but %s was given.', s); expect(throwable('str')).to.throw(error('"str"')); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable([])).to.throw(error('Array')); expect(throwable({})).to.throw(error('Object')); expect(throwable(null)).to.throw(error('null')); expect(throwable(() => undefined)).to.throw(error('Function')); throwable(true)(); throwable(false)(); throwable(undefined)(); }); it('should require the "noParsingErrors" argument to be a boolean', function () { const S = new DataParser(); const throwable = v => () => S.parse(10, {}, {noParsingErrors: v}); const error = s => format( 'Option "noParsingErrors" must be a Boolean, but %s was given.', s, ); expect(throwable('str')).to.throw(error('"str"')); expect(throwable('')).to.throw(error('""')); expect(throwable(10)).to.throw(error('10')); expect(throwable(0)).to.throw(error('0')); expect(throwable([])).to.throw(error('Array')); expect(throwable({})).to.throw(error('Object')); expect(throwable(null)).to.throw(error('null')); expect(throwable(() => undefined)).to.throw(error('Function')); throwable(true)(); throwable(false)(); throwable(undefined)(); }); it('should validate a given schema in the shallow mode', function () { const S = new DataParser(); const throwable = () => S.parse(10, {required: 10}); expect(throwable).to.throw( 'Schema option "required" must be a Boolean, but 10 was given.', ); S.parse([], {type: DataType.ARRAY, items: {type: 10}}); }); it('should resolve the data schema from a given factory', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const res = S.parse(10, () => ({type: DataType.STRING})); expect(res).to.be.eq('10'); }); it('should resolve the data schema from a schema name', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); S.getService(DataSchemaRegistry).defineSchema({ name: 'mySchema', schema: {type: DataType.STRING}, }); const res = S.parse(10, 'mySchema'); expect(res).to.be.eq('10'); }); it('should resolve a schema name from a given factory', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); S.getService(DataSchemaRegistry).defineSchema({ name: 'mySchema', schema: {type: DataType.STRING}, }); const res = S.parse(10, () => 'mySchema'); expect(res).to.be.eq('10'); }); it('should resolve a schema factory from a named schema', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); S.getService(DataSchemaRegistry).defineSchema({ name: 'mySchema', schema: () => ({type: DataType.STRING}), }); const res = S.parse(10, 'mySchema'); expect(res).to.be.eq('10'); }); it('should pass specific arguments to data parsers', function () { const S = new DataParser(); const value = 10; const schema = {type: DataType.NUMBER}; const options = {sourcePath: 'mySource'}; let invoked = 0; const parser = (...args) => { invoked++; expect(args).to.be.eql([value, schema, options, S.container]); return args[0]; }; S.setParsers([parser]); const res = S.parse(value, schema, options); expect(res).to.be.eq(value); expect(invoked).to.be.eq(1); }); it('should propagate an error from the data parser', function () { const S = new DataParser(); const parser = () => { throw new Error('Caught!'); }; S.setParsers([parser]); const throwable = () => S.parse(10, {}); expect(throwable).to.throw('Caught!'); }); it('should apply parsers sequentially to a given value', function () { const S = new DataParser(); const value = 'a'; S.setParsers([v => v + 'b', v => v + 'c']); const res = S.parse(value, {}); expect(res).to.be.eq('abc'); }); it('should not parse array items in the shallow mode even if the items schema is provided', function () { const S = new DataParser(); const value = [1, 2, 3]; const schema = { type: DataType.ARRAY, items: {type: DataType.STRING}, }; S.setParsers([stringTypeParser]); const res = S.parse(value, schema, {shallowMode: true}); expect(res).to.be.eql(value); }); it('should not parse array items when the items schema is not provided', function () { const S = new DataParser(); const value = [1, 2, 3]; const schema = {type: DataType.ARRAY}; let invoked = 0; const parser = (...args) => { invoked++; expect(args[0]).to.be.eql(value); return args[0]; }; S.setParsers([parser]); const res = S.parse(value, schema); expect(res).to.be.eql(value); expect(invoked).to.be.eq(1); }); it('should parse array items when the array schema is provided', function () { const S = new DataParser(); const value = [1, 2, 3]; const schema = { type: DataType.ARRAY, items: {type: DataType.STRING}, }; const expectedCalls = [ [value, schema, undefined, S.container], [1, schema.items, {sourcePath: 'array[0]'}, S.container], [2, schema.items, {sourcePath: 'array[1]'}, S.container], [3, schema.items, {sourcePath: 'array[2]'}, S.container], ]; const calls = []; const parser = (...args) => { calls.push(args); return Array.isArray(args[0]) ? args[0] : String(args[0]); }; S.setParsers([parser]); const res = S.parse(value, schema); expect(res).to.be.eql(['1', '2', '3']); expect(expectedCalls).to.be.eql(calls); }); it('should add an array index to a provided source path', function () { const S = new DataParser(); const value = [1, 2, 3]; const schema = { type: DataType.ARRAY, items: {type: DataType.STRING}, }; const options = {sourcePath: 'mySource'}; const expectedCalls = [ [value, schema, options, S.container], [1, schema.items, {sourcePath: 'mySource[0]'}, S.container], [2, schema.items, {sourcePath: 'mySource[1]'}, S.container], [3, schema.items, {sourcePath: 'mySource[2]'}, S.container], ]; const calls = []; const parser = (...args) => { calls.push(args); return Array.isArray(args[0]) ? args[0] : String(args[0]); }; S.setParsers([parser]); const res = S.parse(value, schema, options); expect(res).to.be.eql(['1', '2', '3']); expect(expectedCalls).to.be.eql(calls); }); it('should resolve a schema factory from the "items" option', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const factory = () => ({type: DataType.STRING}); const schema = {type: DataType.ARRAY, items: factory}; const res = S.parse([10], schema); expect(res).to.be.eql(['10']); }); it('should resolve a schema name from the "items" option', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const schemaA = { type: DataType.ARRAY, items: 'schemaB', }; S.getService(DataSchemaRegistry).defineSchema({ name: 'schemaB', schema: {type: DataType.STRING}, }); const res = S.parse([10], schemaA); expect(res).to.be.eql(['10']); }); it('should not parse object properties in the shallow mode even if the properties schema is provided', function () { const S = new DataParser(); const value = {p1: 1, p2: 2, p3: 3}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.STRING}, p2: {type: DataType.STRING}, p3: {type: DataType.STRING}, }, }; S.setParsers([stringTypeParser]); const res = S.parse(value, schema, {shallowMode: true}); expect(res).to.be.eql(value); }); it('should not parse object properties when the properties schema is not provided', function () { const S = new DataParser(); const value = {p1: 1, p2: 2, p3: 3}; const schema = {type: DataType.OBJECT}; let invoked = 0; const parser = (...args) => { invoked++; expect(args).to.be.eql([value, schema, undefined, S.container]); return args[0]; }; S.setParsers([parser]); const res = S.parse(value, schema); expect(res).to.be.eql(value); expect(invoked).to.be.eq(1); }); it('should parse object properties when the properties schema is provided', function () { const S = new DataParser(); const value = {p1: 1, p2: 2, p3: 3}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.STRING}, p2: {type: DataType.STRING}, p3: {type: DataType.STRING}, }, }; const expectedCalls = [ [value, schema, undefined, S.container], [1, schema.properties.p1, {sourcePath: 'p1'}, S.container], [2, schema.properties.p2, {sourcePath: 'p2'}, S.container], [3, schema.properties.p3, {sourcePath: 'p3'}, S.container], ]; const calls = []; const parser = (...args) => { calls.push(args); return typeof args[0] === 'object' ? args[0] : String(args[0]); }; S.setParsers([parser]); const res = S.parse(value, schema); expect(res).to.be.eql({p1: '1', p2: '2', p3: '3'}); expect(expectedCalls).to.be.eql(calls); }); it('should apply parsers sequentially to a given object and its properties', function () { const S = new DataParser(); const value = {p1: 'a', p2: 'b', p3: 'c'}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.STRING}, p2: {type: DataType.STRING}, p3: {type: DataType.STRING}, }, }; const parser = suffix => value => { return typeof value === 'string' ? value + suffix : value; }; S.setParsers([parser('b'), parser('c')]); const res = S.parse(value, schema); expect(res).to.be.eql({p1: 'abc', p2: 'bbc', p3: 'cbc'}); }); it('should not parse object properties without a specified schema', function () { const S = new DataParser(); const value = {p1: 1, p2: 2, p3: 3}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.NUMBER}, }, }; const expectedCalls = [ [value, schema, undefined, S.container], [value.p1, schema.properties.p1, {sourcePath: 'p1'}, S.container], ]; const calls = []; const parser = (...args) => { calls.push(args); return args[0]; }; S.setParsers([parser]); const res = S.parse(value, schema); expect(res).to.be.eql(value); expect(expectedCalls).to.be.eql(calls); }); it('should add property name to a provided source path', function () { const S = new DataParser(); const value = {p1: 1, p2: 2, p3: 3}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.STRING}, p2: {type: DataType.STRING}, p3: {type: DataType.STRING}, }, }; const options = {sourcePath: 'mySource'}; const expectedCalls = [ [value, schema, options, S.container], [1, schema.properties.p1, {sourcePath: 'mySource.p1'}, S.container], [2, schema.properties.p2, {sourcePath: 'mySource.p2'}, S.container], [3, schema.properties.p3, {sourcePath: 'mySource.p3'}, S.container], ]; const calls = []; const parser = (...args) => { calls.push(args); return typeof args[0] === 'object' ? args[0] : String(args[0]); }; S.setParsers([parser]); const res = S.parse(value, schema, options); expect(res).to.be.eql({p1: '1', p2: '2', p3: '3'}); expect(expectedCalls).to.be.eql(calls); }); it('should resolve a schema factory from the "properties" option', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const factory = () => ({ type: DataType.OBJECT, properties: { prop: {type: DataType.STRING}, }, }); const schema = { type: DataType.OBJECT, properties: factory, }; const res = S.parse({prop: 10}, schema); expect(res).to.be.eql({prop: '10'}); }); it('should resolve a schema name from the "properties" option', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const schemaA = { type: DataType.OBJECT, properties: 'schemaB', }; S.getService(DataSchemaRegistry).defineSchema({ name: 'schemaB', schema: { type: DataType.OBJECT, properties: { prop: {type: DataType.STRING}, }, }, }); const res = S.parse({prop: 10}, schemaA); expect(res).to.be.eql({prop: '10'}); }); it('should throw an error if a properties schema from the schema factory is a non-array schema', function () { const S = new DataParser(); const factory = () => ({type: DataType.BOOLEAN}); const schema = { type: DataType.OBJECT, properties: factory, }; const throwable = () => S.parse({prop: 10}, schema); expect(throwable).to.throw( 'Unable to get the "properties" option ' + 'from the data schema of "boolean" type.', ); }); it('should throw an error if a properties schema from the schema name is a non-array schema', function () { const S = new DataParser(); const schemaA = { type: DataType.OBJECT, properties: 'schemaB', }; S.getService(DataSchemaRegistry).defineSchema({ name: 'schemaB', schema: {type: DataType.BOOLEAN}, }); const throwable = () => S.parse({prop: 10}, schemaA); expect(throwable).to.throw( 'Unable to get the "properties" option ' + 'from the data schema of "boolean" type.', ); }); it('should resolve a schema factory from the property schema', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const factory = () => ({type: DataType.STRING}); const schema = { type: DataType.OBJECT, properties: {prop: factory}, }; const res = S.parse({prop: 10}, schema); expect(res).to.be.eql({prop: '10'}); }); it('should resolve a schema name from the property schema', function () { const S = new DataParser(); S.setParsers([stringTypeParser]); const schemaA = { type: DataType.OBJECT, properties: {prop: 'schemaB'}, }; S.getService(DataSchemaRegistry).defineSchema({ name: 'schemaB', schema: {type: DataType.STRING}, }); const res = S.parse({prop: 10}, schemaA); expect(res).to.be.eql({prop: '10'}); }); it('should not set undefined value to a non existent property', function () { const S = new DataParser(); const parser = value => value; S.setParsers([parser]); const value = {p1: 'str'}; const schema = { type: DataType.OBJECT, properties: { p1: {type: DataType.ANY}, p2: {type: DataType.ANY}, }, }; const res = S.parse(value, schema); expect(res).to.be.eql(value); }); }); });