diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2269628..e3ca60f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,19 +2,22 @@
## [Unreleased][unreleased]
-## [1.0.0][] - 2023-10-1\*
+
-## [0.3.0][] - 2023-10-18
+## [0.3.0][] - 2023-10-19
-- Plan generation from sample [issue](https://github.com/astrohelm/astroplan/issues/10)
-- DOCS & Typings & JSDoc [issue](https://github.com/astrohelm/astroplan/issues/11)
- Schema child method, now you can update only schema or only options for new schemas
-- Getter & Setter for metadata, now you can gather all metadata by schema.meta
+- Metadata fetcher, now you can gather all metadata by schema.meta
- Fixed case when schema returns only methods without metadata, now it returns prototypes key with
all metadata
+- Preprocess & postprocess for objects properties mutations
+- New prototype enum
+- Code cleanup & tests
## [0.2.0][] - 2023-10-18
diff --git a/lib/builder/index.js b/lib/builder/index.js
index 57663c4..674c5e8 100644
--- a/lib/builder/index.js
+++ b/lib/builder/index.js
@@ -1,29 +1,21 @@
'use strict';
const preprocess = require('./preprocess');
+
+//? [Preprocess] Purpose: find, join and build prototype by plan
module.exports = (types, tools, plan) => {
const prototypes = preprocess(types, tools, plan);
if (prototypes.length === 1) return prototypes[0];
if (!prototypes.length) return { test: () => [], ts: () => 'unknown', debug: 'Check warnings' };
- const { condition = 'anyof' } = plan;
+ const { condition: conditionName = 'anyof' } = plan;
return {
prototypes,
type: () => 'object',
test: (sample, path = 'root') => {
- const handler = tools.conditions(condition, prototypes.length - 1);
- const errors = [];
- for (let i = 0; i < prototypes.length; ++i) {
- const result = prototypes[i].test(sample, path);
- const [toDo, err] = handler(result, i);
- if (err) {
- if (result.length) errors.push(...result);
- else errors.push(`[${path}] => ${err}: ${JSON.stringify(sample)}`);
- }
- if (toDo === 'skip' || toDo === 'break') break;
- if (toDo === 'continue') continue;
- }
- return errors;
+ const createError = cause => new tools.Error({ cause, path, sample });
+ const condition = tools.getCondition(conditionName, prototypes.length - 1);
+ return tools.runCondition(condition, { path, sample, createError, prototypes });
},
};
};
diff --git a/lib/builder/preprocess.js b/lib/builder/preprocess.js
index 08b26a9..605000e 100644
--- a/lib/builder/preprocess.js
+++ b/lib/builder/preprocess.js
@@ -1,26 +1,26 @@
'use strict';
const path = 'PREPROCESS';
-const { string } = require('astropack');
+const { string: astropack } = require('astropack');
const { typeOf, isShorthand } = require('../schema/tools');
+const ERR_SHORTHAND = 'Shorthand usage with non-scalar schema';
+const ERR_MISSING_PROTO = 'Missing prototype';
+const ERR_MISSING_SCHEMA = 'Missing or wrong schema at namespace';
module.exports = ({ types, namespace }, tools, schema) => {
const [prototypes, { warn }] = [[], tools];
const signal = (cause, sample, sampleType) => warn({ sample, sampleType, path, cause });
for (const type of typeOf(schema)) {
- if (string.case.isFirstUpper(type)) {
+ if (astropack.case.isFirstUpper(type)) {
const prototype = namespace.get(type);
if (prototype && prototype.test) prototypes.push(prototype);
- else signal('Missing or wrong schema at namespace', namespace, type);
+ else signal(ERR_MISSING_SCHEMA, namespace, type);
continue;
}
const Type = types.get(type);
- if (!Type) {
- signal('Missing prototype', schema, type);
- continue;
- }
- if (Type.kind !== 'scalar' && isShorthand(schema)) {
- signal('Shorthand usage with non-scalar schema', schema, type);
+ if (!Type || ((Type.kind !== 'scalar' || type === 'enum') && isShorthand(schema))) {
+ if (!Type) signal(ERR_MISSING_PROTO, schema, type);
+ else signal(ERR_SHORTHAND, schema, type);
continue;
}
const prototype = new Type(schema, tools);
diff --git a/lib/parser/store.js b/lib/parser/store.js
index 9eeb79d..5fdb037 100644
--- a/lib/parser/store.js
+++ b/lib/parser/store.js
@@ -32,7 +32,7 @@ module.exports = function ParseStorage(argTypes) {
for (const [name, Type] of types.entries()) {
if (!Type.parse) continue;
- for (const type of Type.parse.targets) store[name][type] = Type.parse;
+ for (const type of Type.parse.targets) store[type][name] = Type.parse;
}
return types.clear(), Object.freeze(Object.assign(this, store));
diff --git a/lib/proto/arrays.js b/lib/proto/arrays.js
deleted file mode 100644
index e589a49..0000000
--- a/lib/proto/arrays.js
+++ /dev/null
@@ -1,58 +0,0 @@
-'use strict';
-
-const astropack = require('astropack');
-const MISS = 'Data type missconfiguration, expexted: ';
-const EMPTY = 'Empty list reviced, but required';
-const struct = isInstance => ({
- meta: { kind: 'struct', subtype: 'array' },
- construct(plan, tools) {
- const { Error, build, conditions } = tools;
- this.required = plan.required ?? true;
- this.items = plan.items.map(v => build(v));
- this.condition = plan.condition ?? 'anyof';
-
- this.ts = () => 'object';
- this.test = (sample, path) => {
- const err = cause => new Error({ path, sample, plan, cause });
- if (!isInstance(sample)) return [err(MISS + this.type)];
- const entries = [...sample];
- if (!entries.length && this.required) return [err(EMPTY)];
- const errors = [];
- point: for (let i = 0; i < entries.length; ++i) {
- const handler = conditions(this.condition, this.items.length - 1);
- const err = cause => new Error({ path: `[${path}[${i}]]`, sample, plan, cause });
- for (let j = 0; j < this.items.length; ++j) {
- const result = this.items[j].test(entries[i], `${path}[${i}]`);
- const [toDo, error] = handler(result, j);
- if (error) {
- if (result.length) errors.push(...result);
- else errors.push(err(error + `: ${JSON.stringify(entries[i])}`));
- }
- if (toDo === 'break') break;
- if (toDo === 'continue') continue;
- if (toDo === 'skip') continue point;
- }
- }
- return errors;
- };
- },
-});
-
-module.exports = {
- array: struct(value => Array.isArray(value)),
- set: struct(value => value?.constructor?.name === 'Set'),
-};
-
-const parse = (sample, parse) => {
- if (!Array.isArray(sample)) return null;
- const items = sample.reduce((acc, sample) => {
- const plan = parse(sample);
- for (const saved of acc) if (astropack.utils.equals(saved, plan)) return acc;
- return acc.push(plan), acc;
- }, []);
- if (items.length === 0) return { type: 'array', items: ['unknown'] };
- if (items.length === 1) return { type: 'array', items: [items[0]] };
- return { type: 'array', items };
-};
-
-module.exports.array.parse = Object.assign(parse, { targets: ['object'] });
diff --git a/lib/proto/arrays/index.js b/lib/proto/arrays/index.js
new file mode 100644
index 0000000..9a163e0
--- /dev/null
+++ b/lib/proto/arrays/index.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const astropack = require('astropack');
+const struct = require('./struct');
+
+const parse = (sample, parse) => {
+ if (!Array.isArray(sample)) return null;
+ const items = sample.reduce((acc, sample) => {
+ const plan = parse(sample);
+ for (const saved of acc) if (astropack.utils.equals(saved, plan)) return acc;
+ return acc.push(plan), acc;
+ }, []);
+ if (items.length === 0) return { type: 'array', items: ['unknown'] };
+ if (items.length === 1) return { type: 'array', items: [items[0]] };
+ return { type: 'array', items };
+};
+
+module.exports = {
+ array: struct(value => Array.isArray(value)),
+ set: struct(value => value?.constructor?.name === 'Set'),
+};
+module.exports.array.parse = Object.assign(parse, { targets: ['object'] });
diff --git a/lib/proto/arrays/struct.js b/lib/proto/arrays/struct.js
new file mode 100644
index 0000000..93f26af
--- /dev/null
+++ b/lib/proto/arrays/struct.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const ERR_MISS = 'Data type missconfiguration, expected: ';
+const ERR_EMPTY = 'Empty list recived, but required';
+module.exports = isInstance => ({
+ meta: { kind: 'struct', origin: 'default' },
+ construct(plan, tools) {
+ const { Error, build, getCondition } = tools;
+ this.required = plan.required ?? true;
+ this.items = plan.items.map(v => build(v));
+ this.condition = plan.condition ?? 'anyof';
+
+ this.ts = () => 'object';
+ this.test = (sample, path) => {
+ const createError = cause => new Error({ path, sample, plan, cause });
+ if (!isInstance(sample)) return [createError(ERR_MISS + this.type)];
+ const entries = [...sample];
+ if (!entries.length && this.required) return [createError(ERR_EMPTY)];
+ const errors = [];
+ for (let i = 0; i < entries.length; ++i) {
+ const condition = getCondition(this.condition, this.items.length - 1);
+ const suboption = { path: `${path}[${i}]`, sample: entries[i] };
+ const createError = cause => new tools.Error({ cause, plan, ...suboption });
+ const option = { createError, prototypes: this.items, ...suboption };
+ const result = tools.runCondition(condition, option);
+ if (result.length) errors.push(...result);
+ }
+ return errors;
+ };
+ },
+});
diff --git a/lib/proto/constructor.js b/lib/proto/constructor.js
deleted file mode 100644
index 656ad38..0000000
--- a/lib/proto/constructor.js
+++ /dev/null
@@ -1,50 +0,0 @@
-'use strict';
-
-const defaultTest = () => [];
-const INVALID = 'Invalid prototype, missing construct method';
-
-const parse = (res, options, cause) => {
- const { sample, path, plan, tools } = options;
- if (typeof res === 'object' && Array.isArray(res)) {
- if (!res.length || res[0] instanceof tools.Error) return res;
- return res.map(v => parse(v, options)).flat(2);
- }
- if (typeof res === 'boolean' && !res) return [new tools.Error({ sample, path, plan, cause })];
- if (typeof res === 'string') return [new tools.Error({ sample, path, plan, cause: res })];
- if (res instanceof tools.Error) return [res];
- return [];
-};
-
-const constructor = (name, proto, defaultMeta) => {
- if (!proto?.construct || typeof proto.construct !== 'function') throw new Error(INVALID);
- const meta = { type: name };
- if (proto.meta) Object.assign(meta, proto.meta);
- if (defaultMeta) Object.assign(meta, defaultMeta);
- function Type(plan, tools) {
- Object.assign(this, meta);
- if (plan.meta) Object.assign(this, plan.meta);
- proto.construct.call(this, plan, tools);
- if (!this.type) this.type = 'unknown';
- if (!('required' in this)) this.required = tools.isRequired(plan);
- const test = this.test ?? defaultTest;
- const rules = plan.rules ?? [];
- this.test = (sample, path = 'root') => {
- const options = { sample, path, tools, plan };
- const errors = parse(test(sample, path), options, `Didn't pass test`);
- if (rules.length === 0) return errors;
- for (let i = 0; i < rules.length; ++i) {
- const result = parse(rules[i](sample), options, `Didn't pass rule[${i}]`);
- if (result.length > 0) {
- const err = result.map(cause => new tools.Error({ sample, path, cause, plan }));
- errors.push(...err);
- }
- }
- return errors;
- };
- return Object.freeze(this);
- }
- if (proto.parse && Array.isArray(proto.parse.targets)) Type.parse = proto.parse;
- return Object.assign(Type, meta);
-};
-
-module.exports = constructor;
diff --git a/lib/proto/constructor/index.js b/lib/proto/constructor/index.js
new file mode 100644
index 0000000..7d543e1
--- /dev/null
+++ b/lib/proto/constructor/index.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const defaultTest = () => [];
+const INVALID = 'Invalid prototype, missing construct method';
+const TEST_FAIL = `Didn't pass test`;
+const RULE_FAIL = `Didn't pass rule: `;
+const { parseResult: parse, setDefault } = require('./utils');
+
+//? [Pre-preprocess] Purpose: Prototype wrapper and tester
+module.exports = (name, proto, defaultMeta) => {
+ if (!proto?.construct || typeof proto.construct !== 'function') throw new Error(INVALID);
+ const meta = { type: name };
+ function Type(plan, tools) {
+ Object.assign(this, meta);
+ if (plan.meta) Object.assign(this, plan.meta);
+ proto.construct.call(this, plan, tools), setDefault(this, plan, tools);
+
+ const [test, rules] = [this.test ?? defaultTest, plan.rules ?? []];
+ this.test = (sample, path = 'root') => {
+ const options = { sample, path, tools, plan };
+ const errors = parse(test(sample, path), options, TEST_FAIL);
+ if (rules.length === 0) return errors;
+ for (let i = 0; i < rules.length; ++i) {
+ const result = parse(rules[i](sample), options, RULE_FAIL + i);
+ if (result.length > 0) errors.push(...result);
+ }
+ return errors;
+ };
+ return Object.freeze(this);
+ }
+ if (proto.meta) Object.assign(meta, proto.meta);
+ if (defaultMeta) Object.assign(meta, defaultMeta);
+ if (proto.parse && Array.isArray(proto.parse.targets)) Type.parse = proto.parse;
+ return Object.assign(Type, meta);
+};
diff --git a/lib/proto/constructor/utils.js b/lib/proto/constructor/utils.js
new file mode 100644
index 0000000..f560f8e
--- /dev/null
+++ b/lib/proto/constructor/utils.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const parseResult = (res, options, cause) => {
+ const { sample, path, plan, tools } = options;
+ if (typeof res === 'object' && Array.isArray(res)) {
+ if (!res.length || res[0] instanceof tools.Error) return res;
+ return res.map(v => parseResult(v, options)).flat(2);
+ }
+ if (typeof res === 'boolean' && !res) return [new tools.Error({ sample, path, plan, cause })];
+ if (typeof res === 'string') return [new tools.Error({ sample, path, plan, cause: res })];
+ if (res instanceof tools.Error) return [res];
+ return [];
+};
+
+const setDefault = (ctx, plan, tools) => {
+ if (typeof plan.preprocess === 'function') ctx.preprocess = plan.preprocess;
+ if (typeof plan.postprocess === 'function') ctx.postprocess = plan.postprocess;
+ if (!('required' in ctx)) ctx.required = tools.isRequired(plan);
+ if (!ctx.origin) ctx.origin = 'custom';
+ if (!ctx.type) ctx.type = 'unknown';
+};
+
+module.exports = { parseResult, setDefault };
diff --git a/lib/proto/exotic.js b/lib/proto/exotic.js
index ce7e591..f4721c4 100644
--- a/lib/proto/exotic.js
+++ b/lib/proto/exotic.js
@@ -1,31 +1,31 @@
'use strict';
-const REQUIRED = 'Value is required';
+const ERR_REQUIRED = 'Value is required';
+const ERR_MISS = 'Not of expected type: object';
+const META = { kind: 'scalar', origin: 'default' };
const any = {
- meta: { kind: 'scalar', subtype: 'exotic' },
+ meta: META,
construct(plan, tools) {
const { isRequired, Error } = tools;
this.required = isRequired(plan);
this.ts = () => this.type;
this.test = (sample, path) => {
if (!(!this.required && sample === undefined)) return [];
- return [new Error({ path, sample, plan, cause: REQUIRED })];
+ return [new Error({ path, sample, plan, cause: ERR_REQUIRED })];
};
},
};
const json = {
- meta: { kind: 'struct', subtype: 'exotic' },
+ meta: { kind: 'struct', origin: 'default' },
construct(plan, tools) {
const { isRequired, Error } = tools;
this.required = isRequired(plan);
this.ts = () => 'object';
this.test = (sample, path) => {
- if (!this.required && sample === undefined) return [];
if (typeof sample === 'object' && sample) return [];
- const err = cause => new Error({ path, sample, plan, cause });
- if (this.required && sample === undefined) return [err(REQUIRED)];
- return [err('Not of expected type: object')];
+ if (!this.required && sample === undefined) return [];
+ return [new Error({ path, sample, plan, cause: ERR_MISS })];
};
},
};
diff --git a/lib/proto/index.js b/lib/proto/index.js
index 240ec36..8873a30 100644
--- a/lib/proto/index.js
+++ b/lib/proto/index.js
@@ -1,10 +1,9 @@
'use strict';
const createType = require('./constructor');
-
const prototypes = Object.entries({
...require('./scalars'),
- ...require('./objects'),
+ ...require('./objects/index.js'),
...require('./arrays'),
...require('./exotic'),
});
diff --git a/lib/proto/objects.js b/lib/proto/objects.js
deleted file mode 100644
index ac12e9a..0000000
--- a/lib/proto/objects.js
+++ /dev/null
@@ -1,81 +0,0 @@
-'use strict';
-
-const MISS = 'Data type missconfiguration, expexted: ';
-const EMPTY = 'Empty object reviced, but required';
-const search = (schema, prop) => {
- if (!schema.properties) return [null, null];
- if (prop in schema.properties) return [schema.properties[prop], null];
- if (!schema.patternProperties) return [null, null];
- const entries = Object.entries(schema.patternProperties);
- if (typeof prop !== 'string') prop = String(prop);
- for (const [pattern, value] of entries) if (prop.match(pattern)) return [value, pattern];
- return [null, null];
-};
-
-const struct = ({ isInstance, getEntries }) => ({
- meta: { kind: 'struct', subtype: 'hashmap' },
- construct(plan, tools) {
- const { Error, isRequired, build } = tools;
- const allowExotic = plan.allowExotic ?? false;
-
- const requires = new Map();
- const builded = { properties: {}, patternProperties: {} };
- for (const propType in builded) {
- if (!plan[propType]) continue;
- const entries = Object.entries(plan[propType]);
- for (const [key, value] of entries) {
- builded[propType][key] = build(value);
- const required = isRequired(value);
- if (required) requires.set(key);
- }
- this[propType] = Object.assign({}, builded[propType]);
- }
-
- this.required = plan.required ?? true;
- this.ts = () => 'object';
- this.test = (sample, path) => {
- const err = cause => new Error({ path, sample, plan, cause });
- if (!this.required && sample === undefined) return [];
- if (this.required && !sample) return [err(`Is required`)];
- if (!isInstance(sample)) return [err(MISS + this.type)];
- const entries = getEntries(sample);
- if (!entries.length && this.required) return [err(EMPTY)];
- const errors = [];
- for (const [prop, value] of entries) {
- const err = cause => new Error({ path: `${path}.${prop}`, sample, plan, cause });
- const [prototype, schemaProp] = search(this, prop);
- if (!prototype) {
- if (!allowExotic) errors.push(err(`Exotic property`));
- continue;
- }
- requires.delete(schemaProp ?? prop);
- const result = prototype.test(value, `${path}.${prop}`);
- if (result.length) errors.push(...result);
- }
- if (requires.size) errors.push(err(`Missing fields "${[...requires.keys()].join()}"`));
- return errors;
- };
- },
-});
-
-module.exports = {
- object: struct({
- isInstance: v => typeof v === 'object',
- getEntries: v => Object.entries(v),
- }),
- map: struct({
- isInstance: v => v?.constructor?.name === 'Map',
- getEntries: v => v.entries(),
- }),
-};
-
-const parse = (sample, parse) => {
- if (typeof sample !== 'object' || !sample) return null;
- const properties = Object.entries(sample).reduce((acc, [key, prop]) => {
- acc[key] = parse(prop);
- return acc;
- }, {});
- return { type: 'object', properties };
-};
-
-module.exports.object.parse = Object.assign(parse, { targets: ['object'] });
diff --git a/lib/proto/objects/index.js b/lib/proto/objects/index.js
new file mode 100644
index 0000000..f077d58
--- /dev/null
+++ b/lib/proto/objects/index.js
@@ -0,0 +1,18 @@
+'use strict';
+
+const struct = require('./struct');
+
+module.exports = {
+ object: struct({
+ isInstance: v => typeof v === 'object',
+ getKeys: v => Object.keys(v),
+ getValue: (k, target) => target[k],
+ setValue: (k, v, target) => (target[k] = v),
+ }),
+ map: struct({
+ isInstance: v => v?.constructor?.name === 'Map',
+ getKeys: v => [...v.keys()],
+ getValue: (k, target) => target.get(k),
+ setValue: (k, v, target) => target.set(k, v),
+ }),
+};
diff --git a/lib/proto/objects/struct.js b/lib/proto/objects/struct.js
new file mode 100644
index 0000000..b9daa7d
--- /dev/null
+++ b/lib/proto/objects/struct.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const { process, search, parser, buildProps } = require('./utils');
+const ERR_MISS = 'Data type missconfiguration, expexted: ';
+const ERR_EMPTY = 'Empty object reviced, but required';
+const ERR_REQUIRED = `Properties and value required`;
+
+module.exports = protoTools => ({
+ meta: { kind: 'struct', origin: 'default' },
+ parse: Object.assign(parser(protoTools), { targets: ['object'] }),
+ construct(plan, tools) {
+ const { Error } = tools;
+ const allowExotic = plan.allowExotic ?? false;
+ const requires = buildProps(this, plan, tools);
+
+ this.required = plan.required ?? true;
+ this.ts = () => 'object';
+ this.test = (sample, path) => {
+ const createError = cause => new Error({ path, sample, plan, cause });
+ if (!this.required && sample === undefined) return [];
+ if (this.required && sample === undefined) return [createError(ERR_REQUIRED)];
+ if (!protoTools.isInstance(sample)) return [createError(ERR_MISS + this.type)];
+ const keys = protoTools.getKeys(sample);
+ if (!keys.length && this.required) return [createError(ERR_EMPTY)];
+ const [errors, test] = [[], process(protoTools.getValue, protoTools.setValue, sample)];
+ for (const prop of keys) {
+ const createError = cause => new Error({ path: `${path}.${prop}`, sample, plan, cause });
+ const [prototype, schemaProp] = search(this, prop);
+ if (!prototype) {
+ if (!allowExotic) errors.push(createError(`Exotic property`));
+ continue;
+ }
+ requires.delete(schemaProp ?? prop);
+ const result = test(prototype, prop, `${path}.${prop}`);
+ if (result.length) errors.push(...result);
+ }
+ if (requires.size) {
+ for (const prop of requires.keys()) {
+ const prototype = this.properties[prop] ?? this.patternProperties[prop];
+ if (!prototype.preprocess) continue;
+ const result = (requires.delete(prop), test(prototype, prop, `${path}.${prop}`));
+ if (result.length) errors.push(...result);
+ }
+ requires.size && errors.push(createError(`Missing "${[...requires.keys()].join()}"`));
+ }
+ return errors;
+ };
+ },
+});
diff --git a/lib/proto/objects/utils.js b/lib/proto/objects/utils.js
new file mode 100644
index 0000000..b18bc12
--- /dev/null
+++ b/lib/proto/objects/utils.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const parser = tools => {
+ const parse = (sample, parse) => {
+ if (!tools.isInstance(sample) || !sample) return null;
+ const properties = tools.getKeys(sample).reduce((acc, key) => {
+ acc[key] = parse(tools.getValue(key, sample), parse);
+ return acc;
+ }, {});
+ return { type: 'object', properties };
+ };
+ return parse;
+};
+
+const search = (schema, prop) => {
+ if (!schema.properties) return [null, null];
+ if (prop in schema.properties) return [schema.properties[prop], null];
+ if (!schema.patternProperties) return [null, null];
+ const entries = Object.entries(schema.patternProperties);
+ if (typeof prop !== 'string') prop = String(prop);
+ for (const [pattern, value] of entries) if (prop.match(pattern)) return [value, pattern];
+ return [null, null];
+};
+
+const process = (get, set, target) => (proto, prop, path) => {
+ const { preprocess, postprocess } = proto;
+ if (preprocess) set(prop, preprocess(get(prop, target)), target);
+ const result = proto.test(get(prop, target), path);
+ if (postprocess) set(prop, postprocess(get(prop, target), result), target);
+ return result;
+};
+
+const buildProps = (ctx, plan, tools) => {
+ const requires = new Map();
+ const builded = { properties: {}, patternProperties: {} };
+ for (const propType in builded) {
+ if (!plan[propType]) continue;
+ const entries = Object.entries(plan[propType]);
+ for (const [key, value] of entries) {
+ builded[propType][key] = tools.build(value);
+ const required = tools.isRequired(value);
+ if (required) requires.set(key);
+ }
+ ctx[propType] = Object.assign({}, builded[propType]);
+ }
+ return requires;
+};
+
+module.exports = { process, parser, search, buildProps };
diff --git a/lib/proto/scalars.js b/lib/proto/scalars.js
index 266d6bd..75985ab 100644
--- a/lib/proto/scalars.js
+++ b/lib/proto/scalars.js
@@ -1,24 +1,41 @@
'use strict';
-const MISS = 'Type missconfiguration, expected type: ';
+const ACCEPTED = ['string', 'boolean', 'number', 'bigint'];
+const META = { kind: 'scalar', origin: 'default' };
+const ERR_ENUM = `Enum doesn't contain this value, enum: `;
+const ERR_MISS = 'Type missconfiguration, expected type: ';
+
const scalar = type => ({
- meta: { kind: 'scalar', subtype: 'default' },
+ meta: META,
parse: Object.assign(() => type, { targets: [type] }),
- construct(plan, tools) {
- const { isRequired, Error } = tools;
+ construct(plan, { isRequired, Error }) {
this.required = isRequired(plan);
this.ts = () => type;
this.test = (sample, path) => {
if (typeof sample === type) return [];
if (sample === undefined && !this.required) return [];
- return [new Error({ path, sample, plan, cause: MISS + type })];
+ return [new Error({ path, sample, plan, cause: ERR_MISS + type })];
};
},
});
+const enumerable = {
+ meta: META,
+ construct(plan, { isRequired, Error }) {
+ this.required = isRequired(plan);
+ this.enum = plan.enum?.filter(el => ACCEPTED.includes(typeof el));
+ this.ts = () => 'unknown';
+ this.test = (sample, path) => {
+ if (this.enum.includes(sample)) return [];
+ return new Error({ path, sample, plan, cause: ERR_ENUM + this.enum.join(', ') });
+ };
+ },
+};
+
module.exports = {
string: scalar('string'),
bigint: scalar('bigint'),
number: scalar('number'),
boolean: scalar('boolean'),
+ enum: enumerable,
};
diff --git a/lib/schema/error.js b/lib/schema/error.js
index 0f5992f..7a7df1b 100644
--- a/lib/schema/error.js
+++ b/lib/schema/error.js
@@ -1,30 +1,23 @@
'use strict';
-const MAP = { s: 'sample', c: 'cause', p: 'path' };
-module.exports = (options = {}) => {
- const { pattern = '[P] => C' } = options;
- const format = dict => pattern.toLowerCase().replace(/[scp]/g, char => dict[MAP[char]]);
+const define = (ctx, name, value) => Object.defineProperty(ctx, name, { enumerable: false, value });
+const defaultFormat = dict => `[${dict.path}] => ${dict.cause}`;
+module.exports = (options = {}) => {
+ const { format = defaultFormat } = options;
return function SchemaError({ path, sample, plan, cause, sampleType }) {
[this.count, this.path, this.cause] = [0, path, ''];
if (sample) [this.sample, this.sampleType] = [sample, sampleType ?? typeof sample];
- if (cause) [this.cause, this.count] = [cause, this.count + 1];
+ if (cause) this.cause = (++this.count, cause);
if (plan) this.plan = plan;
- const toString = () => this.message;
- const toJSON = () => ({ sample, path, cause: this.cause, count: this.count });
- const add = cause => {
- cause = this.cause ? `${this.cause}, ${cause}` : cause;
- [this.count, this.cause] = [this.count + 1, cause];
- this.message = format(this);
+ this.message = format(this);
+ define(this, 'toString', () => this.message);
+ define(this, 'toJSON', () => ({ sample, path, cause: this.cause, count: this.count }));
+ define(this, 'message', format(this));
+ define(this, 'add', cause => {
+ this.cause = this.cause ? `${this.cause}, ${cause}` : cause;
+ this.message = (++this.count, format(this));
return this;
- };
- Object.defineProperty(this, 'add', { enumerable: false, value: add });
- Object.defineProperty(this, 'toJSON', { enumerable: false, value: toJSON });
- Object.defineProperty(this, 'toString', { enumerable: false, value: toString });
- Object.defineProperty(this, 'message', {
- enumerable: false,
- writable: true,
- value: format(this),
});
};
};
diff --git a/lib/schema/index.js b/lib/schema/index.js
index 639b31e..6614755 100644
--- a/lib/schema/index.js
+++ b/lib/schema/index.js
@@ -7,19 +7,16 @@ const { ParseStorage, createParser } = require('../parser');
function Schema(plan, params = {}) {
if (plan instanceof Schema) return plan.child(null, params); //? Just updating parameters
- const { errorPattern, types: custom, namespace = new Map() } = params;
+ const { errorFormat, types: custom, namespace = new Map() } = params;
const tools = { ...tooling, build: null, warn: null };
const types = new Map([...defaultTypes]); //? Copy default types links
-
- //? Building tools for type constructors, for example method build used at struct kind
tools.build = build.bind(null, { types, namespace }, tools);
tools.warn = options => {
const err = new tooling.Error(options);
return this.warnings.push(err), err;
};
- if (errorPattern) tools.Error = createError({ pattern: errorPattern });
- //? Working with custom types
+ if (errorFormat) tools.Error = createError({ format: errorFormat });
if (custom && custom instanceof Map) {
const entries = custom.entries();
for (const [name, proto] of entries) {
@@ -31,7 +28,7 @@ function Schema(plan, params = {}) {
this.warnings = [];
Object.freeze(tools);
Object.defineProperty(this, 'meta', { get: tools.exportMeta.bind(null, this) });
- this.parse = createParser(new ParseStorage(types)); //? Plan from sample
+ this.parse = createParser(new ParseStorage(types));
this.child = (p, o) => new Schema(p || plan, o ? { ...params, ...o } : params);
if (plan) Object.assign(this, tools.build(plan)); //? Gathering test, ts methods and metadata
return Object.freeze(this);
diff --git a/lib/schema/tools.js b/lib/schema/tools.js
index 27dc3b3..0aa433e 100644
--- a/lib/schema/tools.js
+++ b/lib/schema/tools.js
@@ -1,15 +1,15 @@
'use strict';
-const conditions = (condition, max) => {
+const getCondition = (name, max) => {
let flag = false;
- if (condition === 'allof') {
+ if (name === 'allof') {
return (result, i) => {
if (result.length > 0) return ['break', 'Item did not pass one of schema'];
if (i === max) return ['skip'];
return ['continue'];
};
}
- if (condition === 'oneof') {
+ if (name === 'oneof') {
return (result, i) => {
if (flag && result.length === 0) return ['break', 'Item passed more than one schema'];
if (result.length === 0) flag = true;
@@ -25,6 +25,21 @@ const conditions = (condition, max) => {
};
};
+const runCondition = (handler, { path, sample, createError, prototypes }) => {
+ const errors = [];
+ for (let i = 0; i < prototypes.length; ++i) {
+ const result = prototypes[i].test(sample, path);
+ const [toDo, cause] = handler(result, i);
+ if (cause) {
+ if (result.length) errors.push(...result);
+ else errors.push(createError(cause));
+ }
+ if (toDo === 'skip' || toDo === 'break') break;
+ if (toDo === 'continue') continue;
+ }
+ return errors;
+};
+
const isShorthand = schema => typeof schema === 'string';
const isRequired = s => (isShorthand(s) ? !s.startsWith('?') : s.required ?? true);
const typeOf = s => {
@@ -48,4 +63,4 @@ const exportMeta = schema => {
return meta;
};
-module.exports = { conditions, isRequired, typeOf, isShorthand, exportMeta };
+module.exports = { getCondition, isRequired, typeOf, isShorthand, exportMeta, runCondition };
diff --git a/package.json b/package.json
index 6914856..d569c0a 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"browser": {},
"files": ["/lib", "/types"],
"scripts": {
- "test": "node --test",
+ "test": "clear && node --test",
"dev": "node index.js",
"prettier:fix": "prettier --write \"**/*.{js,ts,json,html,cjs,md,yaml}\"",
"eslint:fix": "eslint --fix \"**/*.{js,ts}\""
diff --git a/tests/basic.test.js b/tests/basic.test.js
index 9dcbac1..88684a8 100644
--- a/tests/basic.test.js
+++ b/tests/basic.test.js
@@ -12,6 +12,9 @@ test('Schema with errors & warnings', () => {
c: { type: 'object', properties: { z: 'string' } },
z: 'string',
z2: '?string',
+ season: { type: 'enum', enum: ['winter', 'spring', 'autumn', 'summer'] },
+ room: { type: 'enum', enum: [1, 2, 3, 4] },
+ room2: 'enum', //? +1 [Warning] Shorthand enum (enum is scalar)
},
patternProperties: {
'^[a-z]+': 'string',
@@ -22,21 +25,23 @@ test('Schema with errors & warnings', () => {
b: new Set(['a', 'b', 'c']), //? +1 [Warning] Shorthand non-scalar
c: { z: 'string', a: true }, //? +1 [Error] Exotic
hello: 'world',
+ season: 'today', //? +1 [Error] Not at enum
+ room: 5, //? +1 [Error] Not at enum
123: 'test', //? +2 [Error] Exoitic, Missing field "z"
};
const schema = new Schema(plan);
- assert.strictEqual(schema.warnings.length, 1);
+ assert.strictEqual(schema.warnings.length, 2);
const { cause, message, path } = schema.warnings[0];
assert.strictEqual(path, 'PREPROCESS');
assert.strictEqual(cause, 'Shorthand usage with non-scalar schema');
assert.strictEqual(message, '[PREPROCESS] => Shorthand usage with non-scalar schema');
const errors = schema.test(sample);
- assert.strictEqual(errors.length, 4);
+ assert.strictEqual(errors.length, 6);
});
test('Schema without errors & warnings', () => {
const plan = {
- type: 'object',
+ type: 'map',
properties: {
a: ['number', 'string'], //? anyof
b: { type: 'set', items: ['?string', 'any', 'unknown'], condition: 'allof' },
@@ -51,20 +56,31 @@ test('Schema without errors & warnings', () => {
z: 'string',
z2: '?string', //? not required
z3: { type: 'string', required: false },
+ z4: { type: 'string', preprocess: () => 'Required' }, //? Default value
+ z5: {
+ type: 'array',
+ items: [{ type: 'number', postprocess: v => v * 2 }], //? This process wont work
+ preprocess: () => [1, 2, 3, 4],
+ },
+ season: { type: 'enum', enum: ['winter', 'spring', 'autumn', 'summer'] },
},
patternProperties: {
- '^[a-z]+': 'string',
+ '^[a-z]+': { type: 'string', postprocess: v => v + ' !' },
},
};
- const sample = {
- a: 'test',
- b: new Set(['a', 'b', 'c']),
- c: { z: 'string', d: [1, 'test'] },
- hello: 'world',
- z: 'test',
- };
+ const sample = new Map(
+ Object.entries({
+ a: 'test',
+ b: new Set(['a', 'b', 'c']),
+ c: { z: 'string', d: [1, 'test'] },
+ hello: 'world',
+ z: 'test',
+ season: 'winter',
+ }),
+ );
const schema = new Schema(plan);
assert.strictEqual(schema.warnings.length, 0);
const errors = schema.test(sample);
+ console.log(errors, sample);
assert.strictEqual(errors.length, 0);
});
diff --git a/tests/parser.test.js b/tests/parser.test.js
index a29a0a2..42fe9ed 100644
--- a/tests/parser.test.js
+++ b/tests/parser.test.js
@@ -71,5 +71,8 @@ test('Generate schema from array of objects', () => {
],
};
const plan = Schema.parse(sample);
+ const schema = new Schema(plan);
+ const errors = schema.test(sample);
assert.deepStrictEqual(plan, expected);
+ assert.strictEqual(errors.length + schema.warnings.length, 0);
});
diff --git a/types/index.d.ts b/types/index.d.ts
index 65c1ffa..d66b405 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1,4 +1,4 @@
-//TODO: #1 Parser for samples
+//TODO: #1 enum, tuple, date prototypes / Code cleanup
//TODO: #2 Typescript code generation
//TODO: #3 FS UTilities
//TODO: #4 Types