diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86d2b3f..e3ca60f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,24 @@
## [Unreleased][unreleased]
-
+- FS utilities [issue](https://github.com/astrohelm/astroplan/issues/9)
+- Plan generation from sample [issue](https://github.com/astrohelm/astroplan/issues/10)
+- DOCS & Typings & JSDoc [issue](https://github.com/astrohelm/astroplan/issues/11) -->
+
+## [0.3.0][] - 2023-10-19
+
+- Schema child method, now you can update only schema or only options for new schemas
+- 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-1\*
+## [0.2.0][] - 2023-10-18
- Renamed astroplan -> metaforge
- Meta information & Builded tree export [issue](https://github.com/astrohelm/astroplan/issues/8)
@@ -29,6 +38,7 @@
- Default exotic types: Any, Undefined, JSON
- Custom Errors
-[unreleased]: https://github.com/astrohelm/workspace/compare/v0.2.0...HEAD
-[0.2.0]: https://github.com/astrohelm/workspace/releases/tag/v0.2.0
-[0.1.0]: https://github.com/astrohelm/workspace/releases/tag/release
+[unreleased]: https://github.com/astrohelm/metaforge/compare/v0.2.0...HEAD
+[0.3.0]: https://github.com/astrohelm/metaforge/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/astrohelm/metaforge/compare/v0.1.0...v0.2.0
+[0.1.0]: https://github.com/astrohelm/metaforge/releases/tag/v0.1.0
diff --git a/README.md b/README.md
index c414915..885b72a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
Copyright © 2023 Astrohelm contributors.
diff --git a/lib/builder/index.js b/lib/builder/index.js
index 9e25ff1..674c5e8 100644
--- a/lib/builder/index.js
+++ b/lib/builder/index.js
@@ -1,28 +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;
- // TODO: assign Meta information
+ 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 f5c6719..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);
@@ -28,5 +28,3 @@ module.exports = ({ types, namespace }, tools, schema) => {
}
return prototypes;
};
-
-// sql, input, runtime check, core, rpc protocol
diff --git a/lib/parser/index.js b/lib/parser/index.js
new file mode 100644
index 0000000..e25ebe0
--- /dev/null
+++ b/lib/parser/index.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const SKIP = ['undefined', 'function', 'symbol'];
+const ParseStorage = require('./store');
+
+const createParser = store => {
+ const parser = sample => {
+ const type = Array.isArray(sample) ? 'array' : typeof sample;
+ if (SKIP.includes(type) || (type === 'object' && !sample)) return '?unknown';
+ for (const [, parse] of store[type]) {
+ const result = parse(sample, parser);
+ if (result) return result;
+ }
+ return '?unknown';
+ };
+ return parser;
+};
+
+module.exports = { createParser, ParseStorage };
diff --git a/lib/parser/store.js b/lib/parser/store.js
new file mode 100644
index 0000000..5fdb037
--- /dev/null
+++ b/lib/parser/store.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const STANDARD = ['string', 'number', 'bigint', 'boolean', 'object', 'array'];
+function TypedParseStorage(name, prototype) {
+ const handlers = new Map();
+ this.toJSON = () => handlers;
+ this.toString = () => JSON.stringify(handlers);
+ this[Symbol.iterator] = function* () {
+ yield* handlers.entries();
+ yield [name, prototype];
+ };
+
+ return new Proxy(this, {
+ ownKeys: () => handlers.keys(),
+ has: (_, prop) => handlers.has(prop),
+ set: (_, prop, value) => handlers.set(prop, value),
+ deleteProperty: (_, prop) => handlers.delete(prop),
+ get: (target, prop) => {
+ if (prop === Symbol.iterator) return this[Symbol.iterator].bind(target);
+ return handlers.get(prop);
+ },
+ });
+}
+
+module.exports = function ParseStorage(argTypes) {
+ const types = new Map([...argTypes]);
+ const store = STANDARD.reduce((acc, name) => {
+ acc[name] = new TypedParseStorage(name, types.get(name).parse);
+ types.delete(name);
+ return acc;
+ }, {});
+
+ for (const [name, Type] of types.entries()) {
+ if (!Type.parse) continue;
+ 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 dd888d5..0000000
--- a/lib/proto/arrays.js
+++ /dev/null
@@ -1,44 +0,0 @@
-'use strict';
-
-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.type = plan.type;
-
- 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'),
-};
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/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 8085e94..f4721c4 100644
--- a/lib/proto/exotic.js
+++ b/lib/proto/exotic.js
@@ -1,33 +1,33 @@
'use strict';
-const REQUIRED = 'Value is required';
-const any = type => ({
- meta: { kind: 'scalar', subtype: 'exotic' },
+const ERR_REQUIRED = 'Value is required';
+const ERR_MISS = 'Not of expected type: object';
+const META = { kind: 'scalar', origin: 'default' };
+const any = {
+ meta: META,
construct(plan, tools) {
const { isRequired, Error } = tools;
- [this.type, this.required] = [type, isRequired(plan)];
+ 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.type, this.required] = ['json', isRequired(plan)];
+ 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 })];
};
},
};
-module.exports = { any: any('any'), unknown: any('unknown'), json };
+module.exports = { any, unknown: any, json };
diff --git a/lib/proto/index.js b/lib/proto/index.js
index f927c9b..8873a30 100644
--- a/lib/proto/index.js
+++ b/lib/proto/index.js
@@ -1,58 +1,15 @@
'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 createType = (proto, defaultMeta) => {
- if (!proto?.construct || typeof proto.construct !== 'function') throw new Error(INVALID);
- const meta = {};
- if (proto.meta) Object.assign(meta, proto.meta);
- if (defaultMeta) Object.assign(meta, defaultMeta);
- function Type(plan, tools) {
- proto.construct.call(this, plan, tools);
- Object.assign(this, meta);
- if (plan.meta) Object.assign(this, plan.meta);
- if (!this.type) this.type = 'unknown';
- 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);
- }
- return Object.assign(Type, meta);
-};
+const createType = require('./constructor');
+const prototypes = Object.entries({
+ ...require('./scalars'),
+ ...require('./objects/index.js'),
+ ...require('./arrays'),
+ ...require('./exotic'),
+});
module.exports = {
createType,
- proto: new Map(
- Object.entries({
- ...require('./scalars'),
- ...require('./objects'),
- ...require('./arrays'),
- ...require('./exotic'),
- }),
- ),
+ deffaultPrototypes: new Map(prototypes),
+ defaultTypes: new Map(prototypes.map(([k, v]) => [k, createType(k, v)])),
};
diff --git a/lib/proto/objects.js b/lib/proto/objects.js
deleted file mode 100644
index 47cc496..0000000
--- a/lib/proto/objects.js
+++ /dev/null
@@ -1,71 +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.type = plan.type;
- 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(),
- }),
-};
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 72a2939..75985ab 100644
--- a/lib/proto/scalars.js
+++ b/lib/proto/scalars.js
@@ -1,23 +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' },
- construct(plan, tools) {
- const { isRequired, Error } = tools;
- [this.type, this.required] = [type, isRequired(plan)];
+ meta: META,
+ parse: Object.assign(() => type, { targets: [type] }),
+ 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 c40e7f8..6614755 100644
--- a/lib/schema/index.js
+++ b/lib/schema/index.js
@@ -1,32 +1,39 @@
'use strict';
const [build, createError] = [require('../builder'), require('./error')];
-const { proto: prepared, createType } = require('../proto');
+const { defaultTypes, deffaultPrototypes, createType } = require('../proto');
const tooling = { Error: createError(), ...require('./tools') };
+const { ParseStorage, createParser } = require('../parser');
-module.exports = function Schema(schema, params = {}) {
- const { errorPattern, types: custom = new Map(), ...options } = params;
- const [types, customPrototypes] = [new Map(), custom.entries()];
+function Schema(plan, params = {}) {
+ if (plan instanceof Schema) return plan.child(null, params); //? Just updating parameters
+ const { errorFormat, types: custom, namespace = new Map() } = params;
const tools = { ...tooling, build: null, warn: null };
- tools.build = build.bind(null, { types, ...options }, tools);
+ const types = new Map([...defaultTypes]); //? Copy default types links
+ 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 });
- for (const [name, proto] of customPrototypes) {
- const prototype = prepared.get(name) ?? proto;
- types.set(name, createType(prototype, proto.meta));
- }
- for (const [name, proto] of prepared) {
- if (types.has(name)) continue;
- types.set(name, createType(proto));
+ if (errorFormat) tools.Error = createError({ format: errorFormat });
+ if (custom && custom instanceof Map) {
+ const entries = custom.entries();
+ for (const [name, proto] of entries) {
+ const prototype = deffaultPrototypes.get(name) ?? proto;
+ types.set(name, createType(name, prototype, proto.meta));
+ }
}
this.warnings = [];
Object.freeze(tools);
- Object.assign(this, tools.build(schema));
+ Object.defineProperty(this, 'meta', { get: tools.exportMeta.bind(null, this) });
+ 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);
-};
-// this.update = schema => new Schema(schema,)
+}
+
+Schema.parse = createParser(new ParseStorage(defaultTypes));
+Schema.from = (plan, params) => new Schema(plan, params);
+module.exports = Schema;
diff --git a/lib/schema/tools.js b/lib/schema/tools.js
index d201983..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 => {
@@ -34,4 +49,18 @@ const typeOf = s => {
return [s.type];
};
-module.exports = { conditions, isRequired, typeOf, isShorthand };
+const exportMeta = schema => {
+ const [entries, meta] = [Object.entries(schema), new Map()];
+ for (const [k, v] of entries) {
+ if (typeof v === 'function' || k === 'warnings') continue;
+ if (typeof v === 'object') {
+ if (Array.isArray(v)) meta.set(k, v.map(exportMeta));
+ else meta.set(k, exportMeta(v));
+ continue;
+ }
+ meta.set(k, v);
+ }
+ return meta;
+};
+
+module.exports = { getCondition, isRequired, typeOf, isShorthand, exportMeta, runCondition };
diff --git a/package.json b/package.json
index d3d47d8..d569c0a 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"license": "MIT",
- "version": "0.2.0",
+ "version": "0.3.0",
"type": "commonjs",
"name": "metaforge",
"homepage": "https://astrohelm.ru",
- "description": "Library for runtime data validation by metadata",
+ "description": "Library that allow you to describe data structures by subset of JavaScript syntax and validate them at runtime",
"author": "Alexander Ivanov