diff --git a/CHANGELOG.md b/CHANGELOG.md index 428f035..2494997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,50 @@ ## [Unreleased][unreleased] - +- Support latest:21 node version +- Removed parser (maybe temporary) +- TypeScript .d.ts support +- Schema field with multiple type variants now works only with special type union +- Modular mechanism (internal rework): **How it works** + + ```js + const schema = new Schema(); + schema.register((schema, options, plan) => {}); //? Register new module + ``` + + By default registrated modules are: + + - Metatest module (Adds tests for prototypes) + - Metatype module (Typescript parser) + - Handyman (Quality of life module) + + But you also remove default modules: + + ```js + Schema.modules.delete('metatest'); + ``` + +- New shorthands for: + - enum example: ['winter', 'spring', 'summer, 'autumn'] + - tuple example: ['string', 'number'] + - object example: { a: string, b: number } + - string example: 'string' + - schema example: 'MyGlobalSchema' + - schema#2 example: new Schema('string') +- Removed preprocessor mechanism +- Schemas now can be part of plan +- Performance improvements (by removing unnecessary modules) +- Lightweight inheritance +- Removed type JSON (temporary) +- Prototype chaining +- Partial testing +- New prototypes: + - Tuple + - Record + - Schema + - Union ## [0.3.0][] - 2023-10-19 diff --git a/README.md b/README.md index 1dd23a0..1e8fc7b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,52 @@

MetaForge v0.4.0 🕵️

+## Usage example + +```js +const userSchema = new Schema({ + $id: 'userSchema', + phone: { $type: 'union', types: ['number', 'string'] }, //? anyof tyupe + name: { $type: 'set', items: ['string', '?string'] }, //? set tuple + mask: { $type: 'array', items: 'string' }, //? array + ip: { + $type: 'array', + $required: false, + $rules: [ip => ip[0] === '192'], //? custom rules + items: { $type: 'union', types: ['string', '?number'], condition: 'oneof', $required: false }, + }, + type: ['elite', 'member', 'guest'], //? enum + adress: 'string', + secondAdress: '?string', + options: { notifications: 'boolean', lvls: ['number', 'string'] }, +}); + +const systemSchema = new Schema({ $type: 'array', items: userSchema }); + +const sample = [ + { + phone: '7(***)...', + ip: ['192', 168, '1', null], + type: 'elite', + mask: ['255', '255', '255', '0'], + name: new Set(['Alexander', null]), + options: { notifications: true, lvls: [2, '["admin", "user"]'] }, + adress: 'Pushkin street', + }, + { + phone: 79999999999, + type: 'guest', + mask: ['255', '255', '255', '0'], + name: new Set(['Alexander', 'Ivanov']), + options: { notifications: false, lvls: [2, '["admin", "user"]'] }, + adress: 'Pushkin street', + }, +]; + +systemSchema.test(sample); // Shema validation +systemSchema.pull('userSchema').test(sample[0]); // Subschema validation +systemSchema.pull('userSchema').test({ phone: 123 }, 'root', true); // Partial validation +``` +

Copyright © 2023 Astrohelm contributors. This library is MIT licensed.
diff --git a/index.js b/index.js index dc03568..94eea91 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,35 @@ 'use strict'; -module.exports = require('./lib/schema'); +const Forge = require('./lib/forge'); +const createError = require('./lib/error'); +Schema.modules = require('./modules'); +module.exports = Schema; + +const MODULE_ERROR = 'Module already exists: '; +function Schema(plan, options = {}) { + const { modules = Schema.modules, errorFormat, prototypes } = options; + const [SchemaError, warnings] = [createError({ format: errorFormat }), []]; + [this.tools, this.modules] = [{ Error: SchemaError, build, warn }, new Map()]; + const forge = new Forge(this, prototypes); + this.child = (a = plan, b = options) => new Schema(a, b === options ? b : { ...b, ...options }); + this.register = (name, module) => { + if (this.modules.has(module)) warn({ cause: MODULE_ERROR, plan: this.modules, sample: module }); + module(this, options, plan), this.modules.set(name); + return this; + }; + + [this.forge, this.warnings] = [forge, warnings]; + for (const [name, plugin] of modules.entries()) this.register(name, plugin); + return Object.assign(this, this.tools.build(plan)); + + function build(plan) { + const Type = forge.get(plan.$type); + if (Type) return new Type(plan); + throw new Error('Building error: recieved wrong plan:\n' + JSON.stringify(plan)); + } + + function warn(options) { + const warn = new SchemaError({ path: 'BUILD', ...options }); + return warnings.push(warn), warn; + } +} diff --git a/lib/builder/index.js b/lib/builder/index.js deleted file mode 100644 index 674c5e8..0000000 --- a/lib/builder/index.js +++ /dev/null @@ -1,21 +0,0 @@ -'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: conditionName = 'anyof' } = plan; - - return { - prototypes, - type: () => 'object', - test: (sample, path = 'root') => { - 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 deleted file mode 100644 index fa6756d..0000000 --- a/lib/builder/preprocess.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const path = 'PREPROCESS'; -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, rules }, tools, schema) => { - const [prototypes, { warn }] = [[], tools]; - const signal = (cause, sample, sampleType) => warn({ sample, sampleType, path, cause }); - for (const type of typeOf(schema)) { - if (astropack.case.isFirstUpper(type)) { - const prototype = namespace?.get(type); - if (prototype && prototype.test) prototypes.push(prototype); - else signal(ERR_MISSING_SCHEMA, namespace, type); - continue; - } - const Type = types.get(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, rules); - prototypes.push(prototype); - } - return prototypes; -}; diff --git a/lib/schema/error.js b/lib/error.js similarity index 79% rename from lib/schema/error.js rename to lib/error.js index 7a7df1b..7caa4c6 100644 --- a/lib/schema/error.js +++ b/lib/error.js @@ -3,9 +3,9 @@ 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 }) { +module.exports = options => { + const format = options?.format ?? defaultFormat; + return function SchemaError({ path = 'unknown', 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); @@ -13,7 +13,6 @@ module.exports = (options = {}) => { 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)); diff --git a/lib/forge.js b/lib/forge.js new file mode 100644 index 0000000..276d471 --- /dev/null +++ b/lib/forge.js @@ -0,0 +1,48 @@ +'use strict'; + +const { utils } = require('astropack'); +const core = require('./proto'); + +module.exports = function Forge(schema, custom = {}) { + const [chains, wrappers] = [new Map(), { before: [], after: [] }]; + const { before, after } = wrappers; + this.has = name => chains.has(name); + this.attach = (name, ...prototypes) => { + const protos = prototypes.map(unifyProto); + if (name in wrappers) return void wrappers[name].push(...protos); + const chain = chains.get(name) ?? []; + chain.push(...protos), chains.set(name, chain); + return void 0; + }; + + this.get = name => { + const chain = chains.get(name); + if (!chain) return null; + return function ForgePrototype(plan) { + const meta = plan.$meta; + if (plan.$id) this.$id = plan.$id; + [this.$required, this.$type, this.$plan] = [plan.$required ?? true, name, plan]; + if (meta && typeof meta === 'object' && !Array.isArray(meta)) Object.assign(this, meta); + [...before, ...chain, ...after].forEach(proto => proto.call(this, plan, schema.tools)); + if (!this.$kind) this.$kind = 'unknown'; + }; + }; + + for (const [name, proto] of [...entries(core), ...entries(custom)]) this.attach(name, proto); + return Object.freeze(this); +}; + +function unifyProto(Proto) { + const type = utils.isFunction(Proto); + if (type === 'function' || type === 'arrow') return Proto; + return function Prototype(plan, tools) { + if (type === 'class') Object.assign(this, new Proto(plan, tools)); + if (typeof Proto.construct !== 'function') Object.assign(this, Proto); + else Object.assign(this, Proto.construct(plan, tools)); + }; +} + +function entries(protos) { + if (Array.isArray(protos) && protos[0]?.length === 2) return protos; + return protos?.constructor.name === 'Map' ? protos.entries() : Object.entries(protos); +} diff --git a/lib/parser/index.js b/lib/parser/index.js deleted file mode 100644 index e25ebe0..0000000 --- a/lib/parser/index.js +++ /dev/null @@ -1,19 +0,0 @@ -'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 deleted file mode 100644 index 5fdb037..0000000 --- a/lib/parser/store.js +++ /dev/null @@ -1,39 +0,0 @@ -'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.js b/lib/proto.js new file mode 100644 index 0000000..b9cda3d --- /dev/null +++ b/lib/proto.js @@ -0,0 +1,65 @@ +'use strict'; + +const create = type => ({ $kind: type }); +module.exports = new Map( + Object.entries({ + unknown: create('unknown'), + boolean: create('scalar'), + string: create('scalar'), + number: create('scalar'), + bigint: create('scalar'), + any: create('any'), + schema: Schema, + union: Union, + array: List, + tuple: List, + set: List, + record: Struct, + object: Struct, + map: Struct, + enum: Enum, + }), +); + +const ENUM_WARN = 'Recieved incorrect enumerable'; +function Enum(plan, { warn }) { + this.$kind = 'enum'; + const filter = el => typeof el === 'string' || typeof el === 'number'; + this.$enum = Array.isArray(plan.enum) ? [...new Set(plan.enum)].filter(filter) : []; + const isFiltered = this.$enum.length !== plan.enum?.length; + isFiltered && warn({ cause: ENUM_WARN, plan, sample: plan.enum }); +} + +const ITEMS_ERROR = 'Plan items are invalid or empty'; +function List(plan, { warn, build }) { + this.$kind = 'struct'; + const isArray = Array.isArray(plan.items); + this.$isTuple = this.$type === 'tuple' || isArray; + this.$items = (isArray ? plan.items : [plan.items]).map(build); + !this.$items.length && warn({ plan, cause: ITEMS_ERROR, sample: plan.items }); +} + +const PLANS_ERROR = 'Revievd plan without properties'; +function Struct(plan, { build, warn }) { + [this.$kind, this.$properties, this.$patterns] = ['struct', new Map(), new Map()]; + this.$isRecord = this.$type === 'record' || plan.isRecord; + this.$requires = []; + !plan.properties && warn({ plan, sample: plan.properties, cause: PLANS_ERROR }); + for (const [key, value] of Object.entries(plan.properties ?? {})) { + const builded = build(value); + builded.$required && this.$requires.push(key); + (value.isPattern ? this.$patterns : this.$properties).set(key, builded); + } +} + +function Union(plan, { build }) { + this.$kind = 'union'; + this.$condition = plan.condition ?? 'anyof'; + this.$types = (Array.isArray(plan.types) ? plan.types : [plan]).map(build); +} + +function Schema(plan) { + Object.assign(this, plan.schema); + if (plan.$id) this.$id = plan.$id; + this.$required = plan.$required; +} diff --git a/lib/proto/arrays/index.js b/lib/proto/arrays/index.js deleted file mode 100644 index 9a163e0..0000000 --- a/lib/proto/arrays/index.js +++ /dev/null @@ -1,22 +0,0 @@ -'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 deleted file mode 100644 index a43a3cd..0000000 --- a/lib/proto/arrays/struct.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const ERR_MISS = 'Data type misconfiguration, expected: '; -const ERR_EMPTY = 'Empty list received, 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 = Array.isArray(plan.items) ? plan.items.map(v => build(v)) : [build(plan.items)]; - 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 deleted file mode 100644 index f5230fa..0000000 --- a/lib/proto/constructor/index.js +++ /dev/null @@ -1,46 +0,0 @@ -'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 RULE_NOT_EXIST = `Missing 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, defaultRules) { - 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; - if (rules.length === 0 || (!this.required && sample === undefined)) return errors; - for (let i = 0; i < rules.length; ++i) { - let rule = rules[i]; - if (typeof rule === 'string') { - const name = rule; - rule = defaultRules?.get(rule); - if (!rule) { - errors.push(new tools.Error({ cause: RULE_NOT_EXIST + name, sample, path, plan })); - continue; - } - } - const result = parse(rule(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 deleted file mode 100644 index f560f8e..0000000 --- a/lib/proto/constructor/utils.js +++ /dev/null @@ -1,23 +0,0 @@ -'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 deleted file mode 100644 index f4721c4..0000000 --- a/lib/proto/exotic.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -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.required = isRequired(plan); - this.ts = () => this.type; - this.test = (sample, path) => { - if (!(!this.required && sample === undefined)) return []; - return [new Error({ path, sample, plan, cause: ERR_REQUIRED })]; - }; - }, -}; - -const json = { - meta: { kind: 'struct', origin: 'default' }, - construct(plan, tools) { - const { isRequired, Error } = tools; - this.required = isRequired(plan); - this.ts = () => 'object'; - this.test = (sample, path) => { - if (typeof sample === 'object' && sample) return []; - if (!this.required && sample === undefined) return []; - return [new Error({ path, sample, plan, cause: ERR_MISS })]; - }; - }, -}; - -module.exports = { any, unknown: any, json }; diff --git a/lib/proto/index.js b/lib/proto/index.js deleted file mode 100644 index e7efeb5..0000000 --- a/lib/proto/index.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -const createType = require('./constructor'); -const prototypes = Object.entries({ - ...require('./scalars'), - ...require('./objects/index.js'), - ...require('./arrays'), - ...require('./exotic'), -}); - -module.exports = { - createType, - defaultPrototypes: new Map(prototypes), - defaultTypes: new Map(prototypes.map(([k, v]) => [k, createType(k, v)])), -}; diff --git a/lib/proto/objects/index.js b/lib/proto/objects/index.js deleted file mode 100644 index f077d58..0000000 --- a/lib/proto/objects/index.js +++ /dev/null @@ -1,18 +0,0 @@ -'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 deleted file mode 100644 index e2117f7..0000000 --- a/lib/proto/objects/struct.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -const { process, search, parser, buildProps } = require('./utils'); -const ERR_MISS = 'Data type misconfiguration, expected: '; -const ERR_EMPTY = 'Empty object received, 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 deleted file mode 100644 index b18bc12..0000000 --- a/lib/proto/objects/utils.js +++ /dev/null @@ -1,49 +0,0 @@ -'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 deleted file mode 100644 index 75985ab..0000000 --- a/lib/proto/scalars.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -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: 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: 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/index.js b/lib/schema/index.js deleted file mode 100644 index c9a7525..0000000 --- a/lib/schema/index.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const [build, createError] = [require('../builder'), require('./error')]; -const { defaultTypes, defaultPrototypes, createType } = require('../proto'); -const tooling = { Error: createError(), ...require('./tools') }; -const { ParseStorage, createParser } = require('../parser'); - -const path = 'PREPROCESS'; -const MISS = 'Parameter type misconfiguration: '; -function Schema(plan, params = {}) { - if (plan instanceof Schema) return plan.child(null, params); //? Just updating parameters - const { errorFormat, types: custom, namespace = new Map(), rules = new Map() } = params; - const tools = { ...tooling, build: null, warn: null }; - const types = new Map([...defaultTypes]); //? Copy default types links - tools.build = build.bind(null, { types, namespace, rules }, tools); - tools.warn = options => { - const err = new tooling.Error(options); - return this.warnings.push(err), err; - }; - - if (errorFormat) tools.Error = createError({ format: errorFormat }); - if (custom && custom instanceof Map) { - const entries = custom.entries(); - for (const [name, proto] of entries) { - const prototype = defaultPrototypes.get(name) ?? proto; - types.set(name, createType(name, prototype, proto.meta)); - } - } - - this.warnings = []; - if (!(rules instanceof Map)) tools.warn({ cause: MISS + 'Rules not a Map', path }); - if (!(namespace instanceof Map)) tools.warn({ cause: MISS + 'Namespace not a Map', path }); - Object.freeze(tools); - 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); -} - -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 deleted file mode 100644 index 0aa433e..0000000 --- a/lib/schema/tools.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -const getCondition = (name, max) => { - let flag = false; - 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 (name === 'oneof') { - return (result, i) => { - if (flag && result.length === 0) return ['break', 'Item passed more than one schema']; - if (result.length === 0) flag = true; - if (!flag && i === max) return ['break', 'Item did not pass all schemas']; - if (flag && i === max) return ['skip']; - return ['continue']; - }; - } - return (result, i) => { - if (result.length !== 0 && i >= max) return ['break', 'Item did not pass all schemas']; - if (result.length === 0) return ['skip']; - return ['continue']; - }; -}; - -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 => { - if (isShorthand(s)) return [!s.startsWith('?') ? s : s.substring(1)]; - if (Array.isArray(s)) return s; - if (Array.isArray(s.type)) return s.type; - return [s.type]; -}; - -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/modules/handyman/index.js b/modules/handyman/index.js new file mode 100644 index 0000000..b1e36e3 --- /dev/null +++ b/modules/handyman/index.js @@ -0,0 +1,34 @@ +'use strict'; + +const RepairKit = require('./repairkit'); +const MISSING_MODULE = 'Missing plugin: handyman'; +const MODULES_ERROR = 'Recieved sub schema with different modules, may be incompatible: '; +const entries = v => (v?.constructor?.name === 'Map' ? v.entries() : Object.entries(v)); + +module.exports = (schema, options) => { + const namespace = options.namespace ? new Map(entries(options.namespace)) : new Map(); + const [{ tools, modules }, { build, warn }] = [schema, schema.tools]; + const repair = new RepairKit(schema, namespace); + const unnamedSchemas = new Set(); + schema.pull = name => { + const schema = namespace.get(name); + if (schema) return schema; + for (const [, schema] of [...namespace.entries(), ...unnamedSchemas.entries()]) { + if (!schema.pull) throw new Error(MISSING_MODULE); + const found = schema.pull(name); + if (found) return found; + } + return null; + }; + + tools.build = plan => { + const fixed = repair(plan); + const builded = build(fixed); + const { $id } = builded; + if (fixed.$type !== 'schema') return builded; + const [a, b] = [[...modules.keys()], [...builded.modules.keys()]]; + !a.every(key => b.includes(key)) && warn({ cause: MODULES_ERROR + $id, plan, sample: b }); + if (!$id) return unnamedSchemas.add(builded), builded; + return namespace.set($id, builded), builded; + }; +}; diff --git a/modules/handyman/repairkit.js b/modules/handyman/repairkit.js new file mode 100644 index 0000000..2a3c227 --- /dev/null +++ b/modules/handyman/repairkit.js @@ -0,0 +1,61 @@ +'use strict'; + +const { string: astropack } = require('astropack'); +const unknown = { $type: 'unknown', $required: false }; +const SCHEMA_NOT_FOUND = 'Schema not found: '; +const TYPE_NOT_FOUND = 'Recieved unknown type: '; +const ARRAY_TYPE_NOT_FOUND = 'Cant parse type of recieved array: '; +module.exports = function RepairKit(schema, namespace) { + const shorthands = { string, object, array }; + const { tools, forge, child } = schema; + return repair; + + function repair(plan, warn = tools.warn) { + const type = Array.isArray(plan) ? 'array' : typeof plan; + return shorthands[type]?.(plan, warn) ?? unknown; + } + + function string(plan, warn) { + const $required = plan[0] !== '?'; + const $type = $required ? plan : plan.slice(1); + if (!astropack.case.isFirstUpper(plan)) { + if (forge.has($type)) return { $type, $required }; + warn({ cause: TYPE_NOT_FOUND + $type, plan, sample: $type }); + return unknown; + } + const schema = namespace.get($type); //? Schema wrapper #1 + if (schema) return { $type: 'schema', schema, $id: $type, $required }; + warn({ cause: SCHEMA_NOT_FOUND + $type, plan, sample: $type }); + return unknown; + } + + function array(plan, warn) { + let isArray = true; + const stub = () => (isArray = false); + for (let i = 0; i < plan.length && isArray; ++i) { + const fixed = repair(plan[i], stub); + isArray = isArray && forge.has(fixed.$type); + if (!isArray) break; + } + if (isArray) return { $type: 'array', items: plan, $required: true }; + const isEnum = plan.every(item => typeof item === 'string' || typeof item === 'number'); + if (isEnum) return { $type: 'enum', enum: plan, $required: true }; + warn({ cause: ARRAY_TYPE_NOT_FOUND, sample: plan, plan }); + return unknown; + } + + function object(plan, warn) { + const { $required = true, $id, ...fields } = plan; //? Schema wrapper #2 + if (plan.constructor.name === 'Schema') return { $type: 'schema', schema: plan, $required }; + if (!plan || plan.constructor.name !== 'Object') return unknown; + if ($id) return { $type: 'schema', $id, schema: child(fields), $required }; + if ('$type' in plan) { + if (forge.has(plan.$type)) return '$required' in plan ? plan : { ...plan, $required }; + warn({ cause: TYPE_NOT_FOUND + plan.$type, sample: plan.$type, plan }); + return unknown; + } + const result = { $type: 'object', properties: { ...fields }, $required }; + if (plan.$meta) result.$meta = plan.$meta; + return result; + } +}; diff --git a/modules/index.js b/modules/index.js new file mode 100644 index 0000000..83778c6 --- /dev/null +++ b/modules/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = new Map( + Object.entries({ + handyman: require('./handyman'), + metatype: require('./types'), + metatest: require('./test'), + }), +); diff --git a/modules/test/index.js b/modules/test/index.js new file mode 100644 index 0000000..0ffa671 --- /dev/null +++ b/modules/test/index.js @@ -0,0 +1,45 @@ +'use strict'; + +const { objectEntries, unifyResult } = require('./utils'); +const prototypes = require('./tests'); + +const DID_NOT_PASSED = 'Test failed: '; +const RULE_NOT_EXIST = `Missing rule: `; +const REQUIRE_SAMPLE = 'Recieved empty sample'; +module.exports = (schema, options) => { + schema.forge.attach('after', TestWrapper); + for (const [name, proto] of prototypes.entries()) schema.forge.attach(name, proto); + const schemaRules = options.rules ? new Map(objectEntries('any', options.rules)) : new Map(); + const Error = schema.tools.Error; + schema.test = (sample, path = 'root', isPartial = false) => { + const err = (def, cause = def) => new Error({ cause, path, plan: schema.$plan, sample }); + const result = schema.test(sample, path, isPartial); + const flat = unifyResult(result, err.bind(null, DID_NOT_PASSED)); + flat.valid = flat.length === 0; + return flat; + }; + + function TestWrapper(plan) { + if (plan.$type === 'schema') return this.test.bind(this); + const planRules = plan?.$rules; + const rules = Array.isArray(planRules) ? planRules : [planRules]; + const tests = rules.filter(test => typeof test === 'string' || typeof test === 'function'); + typeof this.test === 'function' && tests.unshift(this.test.bind(this)); + this.test = (sample, path, isPartial) => { + if (sample === undefined || sample === null) return !this.$required ? [] : [REQUIRE_SAMPLE]; + const err = (def, cause = def) => new Error({ cause, path, plan, sample }); + const errors = []; + for (let i = 0; i < tests.length; i++) { + let [rule, name] = [tests[i], i - 1 < 0 ? 'Prototype test' : 'Rule №' + i]; + if (typeof rule === 'string') [rule, name] = [schemaRules.get(rule), rule]; + if (rule) { + const result = rule(sample, path, isPartial); + errors.push(...unifyResult(result, err.bind(null, DID_NOT_PASSED + name))); + continue; + } + errors.push(err(RULE_NOT_EXIST + name)); + } + return errors; + }; + } +}; diff --git a/modules/test/tests.js b/modules/test/tests.js new file mode 100644 index 0000000..e978dcb --- /dev/null +++ b/modules/test/tests.js @@ -0,0 +1,99 @@ +'use strict'; + +module.exports = new Map( + Object.entries({ + union: Union, + set: Iterable, + array: Iterable, + tuple: Iterable, + boolean: Scalar, + string: Scalar, + number: Scalar, + bigint: Scalar, + object: Struct, + record: Struct, + map: Struct, + enum: Enum, + }), +); + +const { unionHandler, instanceOfArray, objectEntries } = require('./utils'); + +const WRONG_TYPE = 'Type misconfiguration, expected type: '; +function Scalar() { + this.test = sample => { + if (typeof sample === this.$type) return null; + return WRONG_TYPE + this.$type; + }; +} + +const NOT_AT_ENUM = `Enum doesn't contain this value, enum: `; +function Enum() { + this.test = sample => { + if (this.$enum.includes(sample)) return null; + return NOT_AT_ENUM + this.$enum.join(', '); + }; +} + +const EMPTY_ERROR = 'Empty data recieved'; +const TUPLE_ERROR = 'Recieved items length does not match expected length'; +const INCOR_ERROR = 'Data type misconfiguration, expected: '; +function Iterable() { + this.test = (sample, path, isPartial) => { + if (!instanceOfArray(this.$type, sample)) return [INCOR_ERROR + this.$type]; + const entries = [...sample]; + if (!entries.length && this.$required) return [EMPTY_ERROR]; + if (this.$isTuple && entries.length !== this.$items.length) return [TUPLE_ERROR]; + const errors = []; + for (let i = 0; i < entries.length; i++) { + const test = this.$isTuple ? this.$items[i].test : this.$items[0].test; + const result = test(entries[i], `${path}[${i}]`, isPartial); + result.length && errors.push(...result); + } + return errors; + }; +} + +const EXOTC_ERROR = 'Exotic propertie: '; +const RELIC_ERROR = 'Missing properties: '; +function Struct() { + const pull = prop => { + const patterns = this.$patterns.entries(); + const temp = this.$properties.get(prop); + if (temp) return [prop, temp]; + if (typeof prop !== 'string') prop = String(prop); + for (const [pattern, value] of patterns) if (prop.match(pattern)) return [pattern, value]; + return [null, null]; + }; + + this.test = (sample, path, isPartial) => { + const requires = this.$requires.reduce((acc, prop) => acc.set(prop), new Map()); + const entries = objectEntries(this.$type, sample); + if (!entries) return [INCOR_ERROR + this.$type]; + if (!entries.length && this.$required) return [EMPTY_ERROR]; + const errors = []; + for (const [prop, sample] of entries) { + const [key, prototype] = pull(prop); + if (!key && this.$isRecord) errors.push(EXOTC_ERROR); + if (!key) continue; + const result = prototype.test(sample, `${path}.${prop}`, isPartial); + requires.delete(key), result.length && errors.push(...result); + } + !isPartial && requires.size && errors.push(RELIC_ERROR + [...requires.keys()].join(', ')); + return errors; + }; +} + +function Union() { + this.test = (sample, path, isPartial) => { + const [errors, handler] = [[], unionHandler(this.$condition, this.$types.length - 1)]; + for (let i = 0; i < this.$types.length; ++i) { + const result = this.$types[i].test(sample, path, isPartial); + const [message, deepErrors] = handler(result, i); + if (message === 'ok') return []; + if (deepErrors && deepErrors.length > 0) errors.push(...deepErrors); + if (message !== 'continue') return errors.push(message), errors; + } + return errors; + }; +} diff --git a/modules/test/utils.js b/modules/test/utils.js new file mode 100644 index 0000000..b040146 --- /dev/null +++ b/modules/test/utils.js @@ -0,0 +1,57 @@ +'use strict'; + +module.exports = { unionHandler, instanceOfArray, objectEntries, unifyResult }; + +function objectEntries(t, s) { + if ((t === 'map' || t === 'any') && s?.constructor?.name === 'Map') return s.entries(); + if (typeof s === 'object') return Object.entries(s); + return null; +} + +function instanceOfArray(type, sample) { + if (type === 'array') return Array.isArray(sample); + return sample?.constructor?.name === 'Set'; +} + +function unifyResult(result, createError) { + const flatResult = []; + const unify = v => { + if (typeof v === 'boolean' && !v) return void flatResult.push(createError()); + if (!v) return void 0; + if (Array.isArray(v)) return void v.forEach(unify); + if (typeof v === 'string') return void flatResult.push(createError(v)); + if (typeof v === 'object') return void flatResult.push(v); + return void 0; + }; + return unify(result), flatResult; +} + +function unionHandler(name, max) { + let flag = false; + if (name === 'allof') { + return (result, i) => { + if (result.length > 0) return ['Item did not pass one of schema', result]; + if (i === max) return ['ok']; + return ['continue']; + }; + } + const errors = []; + if (name === 'oneof') { + return (result, i) => { + if (flag && result.length === 0) return ['Item passed more than one schema']; + if (result.length === 0) flag = true; + else if (!flag) errors.push(...result); + if (!flag && i === max) return ['Item did not pass all schemas', errors]; + if (flag && i === max) return ['ok']; + return ['continue']; + }; + } + return (result, i) => { + if (result.length !== 0) { + if (i >= max) return ['Item did not pass all schemas', errors]; + errors.push(...result); + } + if (result.length === 0) return ['ok']; + return ['continue']; + }; +} diff --git a/modules/types/index.js b/modules/types/index.js new file mode 100644 index 0000000..787fa93 --- /dev/null +++ b/modules/types/index.js @@ -0,0 +1,11 @@ +'use strict'; + +const types = require('./types'); + +module.exports = schema => { + function TypescriptWrapper() { + this.toTypescript = () => 'unknown'; + } + for (const [name, proto] of types.entries()) schema.forge.attach(name, proto); + schema.forge.attach('before', TypescriptWrapper); +}; diff --git a/modules/types/types.js b/modules/types/types.js new file mode 100644 index 0000000..891e704 --- /dev/null +++ b/modules/types/types.js @@ -0,0 +1,54 @@ +'use strict'; + +const create = type => ({ toTypescript: () => type }); +module.exports = new Map( + Object.entries({ + unknown: create('unknown'), + boolean: create('boolean'), + string: create('string'), + number: create('number'), + bigint: create('bigint'), + any: create('any'), + enum: Enumerable, + union: Union, + array: Iterable, + tuple: Iterable, + set: Iterable, + object: Struct, + record: Struct, + map: Struct, + }), +); + +function Enumerable() { + this.toTypescript = () => `(${this.$enum.join(' | ')})`; +} + +function Iterable() { + this.toTypescipt = () => { + const builded = this.$items.map(item => item.toTypescript()); + if (this.$type === 'set') return `Set<${builded.join(' | ')}>`; + if (this.$isTuple) return `[${builded.join(', ')}]`; + return `(${builded.join(' | ')})[]`; + }; +} + +function Struct() { + const patterns = this.$patterns.entries(); + this.toTypescript = () => { + let result = '{ '; + for (const [key, value] in this.$properties.entries()) { + // eslint-disable-next-line quotes, no-extra-parens + const sep = key.includes('`') ? (key.includes('"') ? "'" : '"') : '`'; + result += `${sep + key + sep}${value.$required ? '?' : ''}: ${value.toTypescript()}, `; + } + if (!patterns.length) return result + '}'; + const types = patterns.map((_, value) => value.toTypescript()); + return result + `[key: string]?: ${types.join(' | ')}, }`; + }; +} + +function Union() { + const sep = this.$condition === 'allof' ? ' & ' : ' | '; + this.toTypescript = () => this.$types.map(type => type.toTypescript()).join(sep); +} diff --git a/package-lock.json b/package-lock.json index a8ceb99..621b3be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "0.4.0", "license": "MIT", "dependencies": { - "astropack": "^0.4.1" + "astropack": "^0.4.2" }, "devDependencies": { - "@types/node": "^20.5.3", - "eslint": "^8.40.0", + "@types/node": "^20.8.10", + "eslint": "^8.52.0", "eslint-config-astrohelm": "^1.1.1", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.2", - "typescript": "^5.0.2" + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.0.3", + "typescript": "^5.2.2" }, "engines": { "node": "18 || 19 || 20" @@ -82,21 +82,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", - "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -118,9 +118,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@nodelib/fs.scandir": { @@ -185,9 +185,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.5.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.3.tgz", - "integrity": "sha512-ITI7rbWczR8a/S6qjAW7DMqxqFMjjTo61qZVWJ1ubPvbIQsL5D/TvwjYEalM8Kthpe3hTzOGrF2TGbAu2uyqeA==", + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, "node_modules/acorn": { @@ -271,15 +280,15 @@ } }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -290,16 +299,16 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", - "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -309,14 +318,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -327,14 +336,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -345,14 +354,15 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", - "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "is-array-buffer": "^3.0.2", "is-shared-array-buffer": "^1.0.2" @@ -365,9 +375,9 @@ } }, "node_modules/astropack": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/astropack/-/astropack-0.4.1.tgz", - "integrity": "sha512-QjCh7ogS1rUz26NS9DCI6ofGMLjewFfE57Wd7ZuZaru9SNqeDyqS8eSjpwGddLfi3nM0uvhuz+bNA5PlSKZ2QA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/astropack/-/astropack-0.4.2.tgz", + "integrity": "sha512-KT4+RAMEWzeqoCKHGoG9CzskomwTZnWQITpdbRrvWQ/dpaqlElcCkKpIXwThmGVoTkfMe2fpJfO5fzx1Jsic6g==", "engines": { "node": "18 || 19 || 20" } @@ -449,13 +459,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -581,6 +592,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -594,11 +619,12 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -622,26 +648,26 @@ } }, "node_modules/es-abstract": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", - "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -649,23 +675,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "safe-array-concat": "^1.0.0", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", "typed-array-buffer": "^1.0.0", "typed-array-byte-length": "^1.0.0", "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.10" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -675,26 +701,26 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -727,18 +753,19 @@ } }, "node_modules/eslint": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", - "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "^8.47.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -852,26 +879,26 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz", - "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.findlastindex": "^1.2.2", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", + "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.8.0", - "has": "^1.0.3", - "is-core-module": "^2.13.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.6", - "object.groupby": "^1.0.0", - "object.values": "^1.1.6", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", "semver": "^6.3.1", "tsconfig-paths": "^3.14.2" }, @@ -904,9 +931,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", - "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -1178,21 +1205,24 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -1211,15 +1241,15 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1333,18 +1363,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -1364,12 +1382,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1414,6 +1432,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -1474,13 +1504,13 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -1542,12 +1572,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1974,9 +2004,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2010,14 +2040,14 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -2027,26 +2057,26 @@ } }, "node_modules/object.groupby": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", - "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "es-abstract": "^1.21.2", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -2217,9 +2247,9 @@ } }, "node_modules/prettier": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", - "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -2273,14 +2303,14 @@ ] }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -2468,13 +2498,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", - "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -2508,6 +2538,35 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2550,14 +2609,14 @@ "dev": true }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -2567,28 +2626,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2817,9 +2876,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2844,6 +2903,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -2894,13 +2959,13 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" diff --git a/package.json b/package.json index 0513cce..f9e2f7c 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,17 @@ "runtime-verification", "zero-dependencies", "type-generator", - "astrohelm" + "astrohelm", + "lightweight", + "modules", + "modular" ], "main": "index.js", "types": "types/index.d.ts", "packageManager": "npm@9.6.4", "readmeFilename": "README.md", "engines": { - "node": "18 || 19 || 20" + "node": ">= 18" }, "browser": {}, "files": ["/lib", "/types"], @@ -47,16 +50,16 @@ "email": "sashapop101@gmail.com" }, "devDependencies": { - "@types/node": "^20.5.3", - "eslint": "^8.40.0", + "@types/node": "^20.8.10", + "eslint": "^8.52.0", "eslint-config-astrohelm": "^1.1.1", "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.2", - "typescript": "^5.0.2" + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.0.3", + "typescript": "^5.2.2" }, "dependencies": { - "astropack": "^0.4.1" + "astropack": "^0.4.2" } } diff --git a/tests/basic.test.js b/tests/basic.test.js index ff64ee6..1faebd9 100644 --- a/tests/basic.test.js +++ b/tests/basic.test.js @@ -3,86 +3,60 @@ const [test, assert] = [require('node:test'), require('node:assert')]; const Schema = require('..'); -test('Schema with errors & warnings', () => { - const plan = { - type: 'object', - properties: { - a: ['number'], - b: { type: 'set', items: ['array'], condition: 'anyof' }, - 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', +test('Schema without errors & warnings', () => { + const userSchema = new Schema({ + $id: 'UserSchema', + phone: { $type: 'union', types: ['number', 'string'] }, //? anyof tyupe + name: { $type: 'set', items: ['string', '?string'] }, //? set tuple + mask: { $type: 'array', items: 'string' }, //? array + ip: { + $id: 'IpSchema', + $type: 'array', + $required: false, + $rules: [ip => ip[0] === '192'], //? custom rules + items: { $type: 'union', types: ['string', '?number'], condition: 'oneof', $required: false }, }, - }; - const sample = { - a: 'test', //? +1 [Error] Type missmatch - 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, 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, 6); -}); + type: ['elite', 'member', 'guest'], //? enum + adress: 'string', + secondAdress: '?string', + associations: new Schema({ + $id: 'UserID', + '[a-z]+Id': { $type: 'number', isPattern: true }, + }), + options: { notifications: 'boolean', lvls: ['number', 'string'] }, + }); -test('Schema without errors & warnings', () => { - const plan = { - type: 'map', - preprocess: v => ({ ...v, test: 123 }), //? This process wont work - properties: { - a: ['number', 'string'], //? anyof - b: { type: 'set', items: ['?string', 'any', 'unknown'], condition: 'allof' }, - c: { - type: 'object', - properties: { - z: 'string', - //? Required shorthand don't work at array items - d: { type: 'array', items: ['?number', '?string'], condition: 'oneof' }, - }, - }, - 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'] }, - users: { type: 'array', items: 'string' }, + const sample = [ + { + phone: '7(***)...', + ip: ['192', 168, '1', null], + type: 'elite', + mask: ['255', '255', '255', '0'], + name: new Set(['Alexander', null]), + options: { notifications: true, lvls: [2, '["admin", "user"]'] }, + associations: { userId: 1, recordId: 1, dbId: 1 }, + adress: 'Pushkin street', }, - patternProperties: { - '^[a-z]+': { type: 'string', postprocess: v => v + ' !' }, + { + phone: 79999999999, + type: 'guest', + mask: ['255', '255', '255', '0'], + name: new Set(['Alexander', 'Ivanov']), + options: { notifications: false, lvls: [2, '["admin", "user"]'] }, + associations: { userId: 2, recordId: 2, dbId: 2 }, + adress: 'Pushkin street', }, - }; - 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', - users: ['sashapop10', 'expertrix', 'alexander'], - }), - ); - const schema = new Schema(plan); - assert.strictEqual(schema.warnings.length, 0); - const errors = schema.test(sample); - assert.strictEqual(errors.length, 0); + ]; + + const systemSchema = new Schema({ $type: 'array', items: userSchema }); + const ipSchema = systemSchema.pull('IpSchema'); + const usSchema = systemSchema.pull('UserSchema'); + const UserID = systemSchema.pull('UserID'); + assert.strictEqual(systemSchema.warnings.length, 0); + assert.strictEqual(userSchema.warnings.length, 0); + assert.strictEqual(UserID.test(sample[0].associations).valid, true); + assert.strictEqual(ipSchema.test(sample[0].ip).valid, true); + assert.strictEqual(usSchema.test(sample[0]).valid, true); + assert.strictEqual(userSchema.test(sample[0]).valid, true); + assert.strictEqual(systemSchema.test(sample).valid, true); }); diff --git a/tests/namespace.test.js b/tests/namespace.test.js index a5f3272..7eb011b 100644 --- a/tests/namespace.test.js +++ b/tests/namespace.test.js @@ -4,41 +4,10 @@ const [test, assert] = [require('node:test'), require('node:assert')]; const Schema = require('..'); test('Schema with namespace', () => { - const plan = { - type: 'object', - allowExotic: true, - properties: { - a: ['number', 'string'], //? anyof - b: { type: 'set', items: ['?string', 'any', 'unknown'], condition: 'allof' }, - c: { - type: 'object', - properties: { - z: 'string', - //? Required shorthand don't work at array items - d: { type: 'array', items: ['?string', '?number'], condition: 'oneof' }, - }, - }, - z: 'string', - z2: '?string', //? not required - z3: { type: 'string', required: false }, - }, - patternProperties: { - '^[a-z]+': 'string', - }, - }; - const sample = { - a: 'test', - b: new Set(['a', 'b', 'c']), - c: { z: 'string', d: [1, 'test'] }, - hello: 'world', - z: 'test', - 123: 'test', - }; - const schemaA = new Schema(plan); - const namespace = new Map(); - namespace.set('IntegratedSchema', schemaA); - const schemaB = new Schema({ type: 'IntegratedSchema' }, { namespace }); - assert.strictEqual(schemaB.warnings.length + schemaB.warnings.length, 0); - const errors = schemaB.test(sample); - assert.strictEqual(errors.length, 0); + const namespace = { User: new Schema('string') }; + const schema = new Schema(['User', 'User'], { namespace }); + const sample = ['Alexander', 'Ivanov']; + + assert.strictEqual(namespace.User.warnings.length + schema.warnings.length, 0); + assert.strictEqual(schema.test(sample).length, 0); }); diff --git a/tests/parser.test.js b/tests/parser.test.js deleted file mode 100644 index 42fe9ed..0000000 --- a/tests/parser.test.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const test = require('node:test'); -const assert = require('node:assert'); -const Schema = require('..'); - -test('Generate schema from sample', () => { - const sample = { - lines: ['Pushkin streen', 'Kalatushkin home', 8], - zip: '123103', - city: 'Moscow', - country: 'Russia', - }; - const expected = { - type: 'object', - properties: { - lines: { type: 'array', items: ['string', 'number'] }, - zip: 'string', - city: 'string', - country: 'string', - }, - }; - const plan = Schema.parse(sample); - assert.deepStrictEqual(plan, expected); -}); - -test('Generate schema from array of objects', () => { - const sample = [ - { - country: 'Russia', - lines: ['Pushkin street', 'Kalatushkin home', 8], - zip: '123103', - city: 'Moscow', - }, - { - lines: ['Ivanov street', 25], - city: 'Moscow', - zip: '123103', - country: 'Russia', - }, - { - lines: ['Brodway street'], - zip: '123103', - city: 'New York', - phone: '+1 111...', - country: 'USA', - }, - ]; - const expected = { - type: 'array', - items: [ - { - type: 'object', - properties: { - lines: { type: 'array', items: ['string', 'number'] }, - zip: 'string', - city: 'string', - country: 'string', - }, - }, - { - type: 'object', - properties: { - lines: { type: 'array', items: ['string'] }, - zip: 'string', - city: 'string', - phone: 'string', - country: 'string', - }, - }, - ], - }; - 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/tests/rules.test.js b/tests/rules.test.js index a6f1e7f..519f7cf 100644 --- a/tests/rules.test.js +++ b/tests/rules.test.js @@ -6,10 +6,10 @@ const Schema = require('..'); test('Rules', () => { const rule1 = sample => sample?.length > 5; const rule2 = sample => sample?.length < 100; - const plan = { type: 'string', rules: [rule1, rule2] }; + const plan = { $type: 'string', $rules: [rule1, rule2] }; const schema = new Schema(plan); assert.strictEqual(schema.warnings.length, 0); - assert.strictEqual(schema.test().length, 3); //? Required + two rules + assert.strictEqual(schema.test().length, 1); //? Required assert.strictEqual(schema.test('Test').length, 1); //? One rule assert.strictEqual(schema.test('Hello world').length, 0); }); @@ -19,11 +19,10 @@ test('String type of rules', () => { const mailRule = sample => sample?.includes('@'); const isGmail = sample => sample?.includes('gmail.com'); const rules = new Map().set('length', lengthRule).set('mail', mailRule); - const plan = { type: 'string', rules: ['length', 'mail', isGmail], required: false }; + const plan = { $type: 'string', $rules: ['length', 'mail', isGmail], $required: false }; const schema = new Schema(plan, { rules }); assert.strictEqual(schema.warnings.length, 0); - assert.strictEqual(schema.test().length, 0); - console.log(schema.test('Alexander Ivanov')); - assert.strictEqual(schema.test('Alexander Ivanov').length, 2); //? Not a mail + assert.strictEqual(schema.test().length, 0); //? Not required + assert.strictEqual(schema.test('Alexander Ivanov').length, 2); //? Not a mail / gmail assert.strictEqual(schema.test('somemail@gmail.com').length, 0); }); diff --git a/tests/types.test.js b/tests/types.test.js index 923f51b..8cd400a 100644 --- a/tests/types.test.js +++ b/tests/types.test.js @@ -3,46 +3,36 @@ const [test, assert] = [require('node:test'), require('node:assert')]; const Schema = require('..'); -test('Custom types', () => { - const types = new Map(); +test('Custom prototypes', () => { + function MyDate() { + this.$kind = 'scalar'; + this.test = sample => { + if (!isNaN(new Date(sample))) return null; + return 'Invalid sample type'; + }; + } - const datePrototype = { - meta: { kind: 'scalar', type: 'date' }, - construct(plan, { isRequired, Error }) { - this.required = isRequired(plan); - this.test = (sample, path) => { - if (!this.required && sample === undefined) return []; - if (this.required && !sample) { - return [new Error({ plan, sample, path, cause: 'Value is required' })]; - } - if (typeof sample !== 'object' && typeof sample !== 'string') { - return [new Error({ plan, sample, path, cause: 'Invalid sample type' })]; - } - if (isNaN(new Date(sample))) { - return [new Error({ plan, sample, path, cause: 'Invalid sample type' })]; - } - return []; - }; - }, - }; - types.set('myDate', datePrototype); - const plan = '?myDate'; - const schema = new Schema(plan, { types }); + const schema = new Schema('?date', { prototypes: { date: MyDate } }); assert.strictEqual(schema.warnings.length, 0); assert.strictEqual(schema.test().length, 0); assert.strictEqual(schema.test(new Date()).length, 0); assert.strictEqual(schema.test(new Date('Invalid param')).length, 1); }); -test('Custom types with meta replacement for old ones', () => { - const types = new Map(); - types.set('string', { meta: { newMeta: 'This String is awsome' } }); - types.set('number', { meta: { newMeta: 'This Number is awsome' } }); - const stringSchema = new Schema('string', { types }); - const numberSchema = new Schema('number', { types }); +test('Custom prototypes with meta replacement for old ones', () => { + const prototypes = new Map(); + prototypes.set('string', { about: 'This String is awsome' }); + prototypes.set('number', { about: 'This Number is awsome' }); + const prototypeOnject = Object.fromEntries(prototypes.entries()); + const stringSchema = new Schema('string', { prototypes }); + const numberSchema = new Schema( + { $type: 'number', $meta: { desc: 'age' } }, + { prototypes: prototypeOnject }, + ); assert.strictEqual(stringSchema.warnings.length + numberSchema.warnings.length, 0); assert.strictEqual(numberSchema.test(1).length, 0); assert.strictEqual(stringSchema.test('test').length, 0); - assert.strictEqual(stringSchema.newMeta, 'This String is awsome'); - assert.strictEqual(numberSchema.newMeta, 'This Number is awsome'); + assert.strictEqual(stringSchema.about, 'This String is awsome'); + assert.strictEqual(numberSchema.about, 'This Number is awsome'); + assert.strictEqual(numberSchema.desc, 'age'); }); diff --git a/types/index.d.ts b/types/index.d.ts index 42bc6c1..a3d9d2c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,54 +1,81 @@ -//TODO: Root link, root path, wrap value in object to update it (possible to do calculated fields) / Or remove that processing -//TODO: String rules, pass realization through Schema constructor, rename rules -> validate, possisble to pass only one method / string -//TODO: String for preprocess, postprocess -//TODO: Tuple, Date -//TODO: Custom prototype inheritance -//TODO: Plans join ? - -//TODO: #1 Typescript code generation -//TODO: #2 Types & README +class SchemaError { + constructor(options: { pattern: (dict: { [key: string]: string }) => string }) {} + plan: unknown; + message: string; + path: string; + sample: unknown; + sampleType: string; + cause: string; +} -type Condition = 'allof' | 'anyof' | 'oneof'; -type TypeField = { type: Type | Type[]; condition?: Condition }; -type Rule = (sample: unknown, tools: Tools) => SchemaError[]; -type Type = string | TypeObject; +type Rule = 'string' | ((sample: string, path?: string, partial?: boolean) => unknown); +interface PlanCore { + $meta?: { [key: string]: unknown }; + $rules?: Array | Rule; + $required?: boolean; + $type?: string; + $id?: string; + [key: string]: unknown; +} -interface Tools { - Error: SchemaError; - isRequired: (plan: string) => boolean; - isShorthand: (plan: ) => boolean; - typeOf: (plan: ) => Type[]; +interface Options { + errorPattern?: (dict: { [key: string]: string }) => string; + rules?: Map | { [key: string]: Rule }; + namespace?: Map | { [key: string]: Rule }; } -interface TypeObject { - type: string; - rules?: []; - meta?: { [key: string]: unknown }; +interface Tools { + Error: typeof SchemaError; + build: (plan: Plan) => ProtoObject; + warn: (opts: { + path?: string; + cause: string; + sample: unknown; + sampleType: string; + }) => SchemaError; } -interface SchemaError {} -interface TOptions { - errorPattern?: string; +type ProtoObject = { construct?: ProtoFunction; [key: string]: unknown }; +type ProtoFunction = (plan: Plan, tools: Tools, isPartial?: boolean) => object; +class ProtoClass implements ProtoObject { + constructor(plan: Plan, tools: Tools, isPartial?: boolean) {} + [key: string]: unknown; } +type Proto = ProtoClass | ProtoFunction | ProtoObject; -interface TObject { - type: 'map' | 'object'; - properties: { [key: unknown]: TypeField }; - patternProperties: { [key: unknown]: TypeField }; +class ForgePrototype { + $id?: string; + $kind: string; + $type: string; + $required: boolean; + $plan: Plan; + test?: ( + sample: unknown, + root?: string, + partial?: boolean, + ) => Array & { valid: boolean }; + toTypescript?: () => string; + [key: string]: unknown; } -interface TArray { - type: 'set' | 'array'; - items: Array; - condition: Condition; +class Forge { + constructor(schema: Schema, prototypes?: Map) {} + attach: (name: string, ...prototypes: Proto[]) => void; + get: (name: string) => { new (plan: Plan): ForgePrototype }; + has: (name: string) => boolean; } -export default class Schema { - kind?: 'struct' | 'scalar' | 'any'; - subtype?: string; - type?: string; +type TModule = (schema: Schema, options: Options, plan: Plan) => void; +type Plan = string | PlanCore | Schema; +class Schema extends ForgePrototype { + forge: Forge; + modules: Map; warnings: Array; - constructor(plan: string | TType, options?: TOptions); - test: (sample: unknown, root?: string) => Array; - [key: string]: string; + tools: Tools; + constructor(plan: Plan, options?: Options) {} + pull: (name: string) => Schema | null; + register: (name: string, module: TModule) => void; + [key: string]: unknown; } + +export = Schema;