diff --git a/experiments/config.mjs b/experiments/config.mjs index ed2c988..8a93cd5 100644 --- a/experiments/config.mjs +++ b/experiments/config.mjs @@ -1,4 +1,4 @@ -import { Field_Configuration } from '@efforting.tech/data/field-configuration'; +import { Schema, Field_Configuration } from '@efforting.tech/data/field-configuration'; function mandatory_anything(value) { @@ -15,7 +15,8 @@ function string_coercion_function(value) { -const fc = new Field_Configuration('Some_Field', null, string_coercion_function, 'Anything that could be a string'); +const fc = new Field_Configuration(null, string_coercion_function, null, 'Anything that could be a string'); +const fd = new Field_Configuration(null, string_coercion_function, () => 'baz', 'A string. Defaults to \'baz\''); console.log(fc.check_validation(undefined)); console.log(fc.check_validation(true)); @@ -23,9 +24,27 @@ console.log(fc.check_validation(true)); console.log([ fc.coerce(123) ]); console.log([ fc.coerce(true) ]); -console.log([ fc.load(undefined, 'Some configuration') ]); -console.log([ fc.load(true) ]); +// console.log([ fc.load(undefined, 'Some configuration') ]); +// console.log([ fc.load(true) ]); //fc.validate(undefined); -console.log(fc.load(undefined, 'New thingamabob object')); +//console.log(fc.load(undefined, 'New thingamabob object')); + + +const s = new Schema({ + foo: fc, + bar: fd, +}); + +const s2 = new Schema({ + thing: fc, + stuff: s, +}); + +console.log(s2.load({ + stuff: { foo: 'Hello World' }, + thing: 123, +})); + +// { thing: '123', stuff: { foo: 'Hello World', bar: 'baz' } } diff --git a/source/data/field-configuration.mjs b/source/data/field-configuration.mjs index 219a99b..10c78fc 100644 --- a/source/data/field-configuration.mjs +++ b/source/data/field-configuration.mjs @@ -1,8 +1,8 @@ -import { Data_Validation_Failed, Data_Coercion_Failed } from '@efforting.tech/errors'; +import { Data_Validation_Failed, Data_Coercion_Failed, Superfluous_Data_Field } from '@efforting.tech/errors'; export class Field_Configuration { - constructor(name, validation_function=null, coercion_function=null, expected_description=undefined) { - Object.assign(this, { name, validation_function, coercion_function, expected_description }); + constructor(validation_function=null, coercion_function=null, factory_function=null, expected_description=undefined) { + Object.assign(this, { validation_function, coercion_function, factory_function, expected_description }); } check_validation(value) { @@ -11,29 +11,36 @@ export class Field_Configuration { } validate(value, target=undefined) { - const { validation_function, name, expected_description } = this; + const { validation_function, expected_description } = this; if (!this.check_validation(value)) { throw new Data_Validation_Failed({ - name, validation_function, value, + validation_function, value, target, expected_description, }); } } coerce(value, target=undefined) { - const { coercion_function, name, expected_description } = this; + const { coercion_function, expected_description } = this; try { return coercion_function ? coercion_function(value) : value; } catch (e) { throw new Data_Coercion_Failed({ - name, coercion_function, value, + coercion_function, value, target, expected_description, upstream_error: e, }) } } - load(value, target=undefined) { + load(value=undefined, target=undefined) { + + const { factory_function } = this; + + if ((value === undefined) && factory_function) { + value = factory_function(target); + } + const coerced_value = this.coerce(value, target); this.validate(coerced_value, target); return coerced_value; @@ -41,3 +48,32 @@ export class Field_Configuration { } + + +export class Schema { + + constructor(field_schema) { + Object.assign(this, { field_schema }); + } + + load(value={}, target=undefined) { + const { field_schema } = this; + + for (const [value_name, value_value] of Object.entries(value)) { + if (!field_schema[value_name]) { + throw new Superfluous_Data_Field({ + field_value: value_value, field_name: value_name, target, + }); + } + } + + const result = {}; + for (const [field_name, field_config] of Object.entries(field_schema)) { + const sub_target = { schema: this, field_name, field_config, parent_target: target }; + result[field_name] = field_config.load(value[field_name], sub_target); + } + + return result; + } + +} \ No newline at end of file diff --git a/source/errors.mjs b/source/errors.mjs index aeed2c6..18a9e63 100644 --- a/source/errors.mjs +++ b/source/errors.mjs @@ -4,24 +4,43 @@ import { inspect } from 'node:util'; export class Data_Validation_Failed extends Error { constructor(data) { - const { value, target, expected_description, validation_function, name, upstream_error } = data; + const { value, target, expected_description, validation_function, upstream_error } = data; const type = value === null ? 'null' : typeof value; - const target_info = target ? inspect(target) : 'unknown target'; + + const target_info = target?.info ? inspect(target.info) : 'unknown target'; + const field_ref = target?.name ? `field "${target.name}"` : 'unknown field'; + const expected_desc = expected_description ?? `data that would pass validation using ${validation_function}`; const upstream_error_description = upstream_error ? ` Upstream error was: ${upstream_error}` : ''; - super(`Data validation failed for field "${name}" of ${target_info}. Encountered data of type "${type}" while expecting "${expected_desc}".${upstream_error_description}`); + super(`Data validation failed for ${field_ref} of ${target_info}. Encountered data of type "${type}" while expecting "${expected_desc}".${upstream_error_description}`); + this.data = data; + } +} + +export class Superfluous_Data_Field extends Error { + constructor(data) { + const { field_value, field_name, target, upstream_error } = data; + const type = field_value === null ? 'null' : typeof field_value; + + const target_info = target?.info ? inspect(target.info) : 'unknown target'; + + const upstream_error_description = upstream_error ? ` Upstream error was: ${upstream_error}` : ''; + super(`Data validation failed for ${target_info}. Superfluous field "${field_name}" with value of type "${type}" encountered.${upstream_error_description}`); this.data = data; } } export class Data_Coercion_Failed extends Error { constructor(data) { - const { value, target, expected_description, coercion_function, name, upstream_error } = data; + const { value, target, expected_description, coercion_function, upstream_error } = data; const type = value === null ? 'null' : typeof value; - const target_info = target ? inspect(target) : 'unknown target'; + + const target_info = target?.info ? inspect(target.info) : 'unknown target'; + const field_ref = target?.name ? `field "${target.name}"` : 'unknown field'; + const expected_desc = expected_description ?? `data that would be coerced using ${coercion_function}`; const upstream_error_description = upstream_error ? ` Upstream error was: ${upstream_error}` : ''; - super(`Data coercion failed for field "${name}" of ${target_info}. Encountered data of type "${type}" while expecting "${expected_desc}".${upstream_error_description}`); + super(`Data coercion failed for ${field_ref} of ${target_info}. Encountered data of type "${type}" while expecting "${expected_desc}".${upstream_error_description}`); this.data = data; } }