Skip to content

Commit

Permalink
v0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
shuritch committed Nov 5, 2023
1 parent f70a369 commit 5cbbc95
Show file tree
Hide file tree
Showing 38 changed files with 1,032 additions and 1,027 deletions.
46 changes: 43 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,50 @@

## [Unreleased][unreleased]

<!-- ## [1.0.0][] - 2023-10-2\*
## [0.4.0][] - 2023-11-05

- Typescript types generation [issue](https://github.com/astrohelm/astroplan/issues/5)
- DOCS & Typings & JSDoc [issue](https://github.com/astrohelm/astroplan/issues/11) -->
- 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 <code>union</code>
- 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: <code>['winter', 'spring', 'summer, 'autumn']</code>
- tuple example: <code>['string', 'number']</code>
- object example: <code>{ a: string, b: number }</code>
- string example: <code>'string'</code>
- schema example: <code>'MyGlobalSchema'</code>
- schema#2 example: <code>new Schema('string')</code>
- 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
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
<h1 align="center">MetaForge v0.4.0 🕵️</h1>

## 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
```

<p align="center">
Copyright © 2023 <a href="https://github.com/astrohelm/metaforge/graphs/contributors">Astrohelm contributors</a>.
This library is <a href="./LICENSE">MIT licensed</a>.<br/>
Expand Down
34 changes: 33 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
21 changes: 0 additions & 21 deletions lib/builder/index.js

This file was deleted.

30 changes: 0 additions & 30 deletions lib/builder/preprocess.js

This file was deleted.

7 changes: 3 additions & 4 deletions lib/schema/error.js → lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
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);
if (plan) this.plan = plan;
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));
Expand Down
48 changes: 48 additions & 0 deletions lib/forge.js
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 0 additions & 19 deletions lib/parser/index.js

This file was deleted.

39 changes: 0 additions & 39 deletions lib/parser/store.js

This file was deleted.

65 changes: 65 additions & 0 deletions lib/proto.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 5cbbc95

Please sign in to comment.