Skip to content

Commit

Permalink
Merge pull request #15 from astrohelm/dev
Browse files Browse the repository at this point in the history
Metaforge v0.3.0
  • Loading branch information
shuritch authored Oct 19, 2023
2 parents 511f54f + c477027 commit b674fd4
Show file tree
Hide file tree
Showing 26 changed files with 545 additions and 277 deletions.
28 changes: 19 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@

## [Unreleased][unreleased]

<!--
## [0.3.0][] - 2023-10-1\*
<!-- ## [1.0.0][] - 2023-10-1\*
- Plan generation from sample [issue](https://github.com/astrohelm/astroplan/issues/10)
- Typescript types generation [issue](https://github.com/astrohelm/astroplan/issues/5)
- DOCS & Typings & JSDoc [issue](https://github.com/astrohelm/astroplan/issues/11)
- FS utilities [issue](https://github.com/astrohelm/astroplan/issues/9) -->
- FS utilities [issue](https://github.com/astrohelm/astroplan/issues/9)
- Plan generation from sample [issue](https://github.com/astrohelm/astroplan/issues/10)
- DOCS & Typings & JSDoc [issue](https://github.com/astrohelm/astroplan/issues/11) -->

## [0.3.0][] - 2023-10-19

- Schema child method, now you can update only schema or only options for new schemas
- Metadata fetcher, now you can gather all metadata by <code>schema.meta</code>
- Fixed case when schema returns only methods without metadata, now it returns prototypes key with
all metadata
- Preprocess & postprocess for objects properties mutations
- New prototype <code>enum</code>
- Code cleanup & tests

## [0.2.0][] - 2023-10-1\*
## [0.2.0][] - 2023-10-18

- Renamed astroplan -> metaforge
- Meta information & Builded tree export [issue](https://github.com/astrohelm/astroplan/issues/8)
Expand All @@ -29,6 +38,7 @@
- Default exotic types: Any, Undefined, JSON
- Custom Errors

[unreleased]: https://github.com/astrohelm/workspace/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/astrohelm/workspace/releases/tag/v0.2.0
[0.1.0]: https://github.com/astrohelm/workspace/releases/tag/release
[unreleased]: https://github.com/astrohelm/metaforge/compare/v0.2.0...HEAD
[0.3.0]: https://github.com/astrohelm/metaforge/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/astrohelm/metaforge/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/astrohelm/metaforge/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h1 align="center">MetaForge v0.2.0 🕵️</h1>
<h1 align="center">MetaForge v0.3.0 🕵️</h1>

<p align="center">
Copyright © 2023 <a href="https://github.com/astrohelm/metaforge/graphs/contributors">Astrohelm contributors</a>.
Expand Down
23 changes: 8 additions & 15 deletions lib/builder/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
'use strict';

const preprocess = require('./preprocess');

//? [Preprocess] Purpose: find, join and build prototype by plan
module.exports = (types, tools, plan) => {
const prototypes = preprocess(types, tools, plan);
if (prototypes.length === 1) return prototypes[0];
if (!prototypes.length) return { test: () => [], ts: () => 'unknown', debug: 'Check warnings' };
const { condition = 'anyof' } = plan;
// TODO: assign Meta information
const { condition: conditionName = 'anyof' } = plan;

return {
prototypes,
type: () => 'object',
test: (sample, path = 'root') => {
const handler = tools.conditions(condition, prototypes.length - 1);
const errors = [];
for (let i = 0; i < prototypes.length; ++i) {
const result = prototypes[i].test(sample, path);
const [toDo, err] = handler(result, i);
if (err) {
if (result.length) errors.push(...result);
else errors.push(`[${path}] => ${err}: ${JSON.stringify(sample)}`);
}
if (toDo === 'skip' || toDo === 'break') break;
if (toDo === 'continue') continue;
}
return errors;
const createError = cause => new tools.Error({ cause, path, sample });
const condition = tools.getCondition(conditionName, prototypes.length - 1);
return tools.runCondition(condition, { path, sample, createError, prototypes });
},
};
};
20 changes: 9 additions & 11 deletions lib/builder/preprocess.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
'use strict';

const path = 'PREPROCESS';
const { string } = require('astropack');
const { string: astropack } = require('astropack');
const { typeOf, isShorthand } = require('../schema/tools');
const ERR_SHORTHAND = 'Shorthand usage with non-scalar schema';
const ERR_MISSING_PROTO = 'Missing prototype';
const ERR_MISSING_SCHEMA = 'Missing or wrong schema at namespace';

module.exports = ({ types, namespace }, tools, schema) => {
const [prototypes, { warn }] = [[], tools];
const signal = (cause, sample, sampleType) => warn({ sample, sampleType, path, cause });
for (const type of typeOf(schema)) {
if (string.case.isFirstUpper(type)) {
if (astropack.case.isFirstUpper(type)) {
const prototype = namespace.get(type);
if (prototype && prototype.test) prototypes.push(prototype);
else signal('Missing or wrong schema at namespace', namespace, type);
else signal(ERR_MISSING_SCHEMA, namespace, type);
continue;
}
const Type = types.get(type);
if (!Type) {
signal('Missing prototype', schema, type);
continue;
}
if (Type.kind !== 'scalar' && isShorthand(schema)) {
signal('Shorthand usage with non-scalar schema', schema, type);
if (!Type || ((Type.kind !== 'scalar' || type === 'enum') && isShorthand(schema))) {
if (!Type) signal(ERR_MISSING_PROTO, schema, type);
else signal(ERR_SHORTHAND, schema, type);
continue;
}
const prototype = new Type(schema, tools);
prototypes.push(prototype);
}
return prototypes;
};

// sql, input, runtime check, core, rpc protocol
19 changes: 19 additions & 0 deletions lib/parser/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const SKIP = ['undefined', 'function', 'symbol'];
const ParseStorage = require('./store');

const createParser = store => {
const parser = sample => {
const type = Array.isArray(sample) ? 'array' : typeof sample;
if (SKIP.includes(type) || (type === 'object' && !sample)) return '?unknown';
for (const [, parse] of store[type]) {
const result = parse(sample, parser);
if (result) return result;
}
return '?unknown';
};
return parser;
};

module.exports = { createParser, ParseStorage };
39 changes: 39 additions & 0 deletions lib/parser/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

const STANDARD = ['string', 'number', 'bigint', 'boolean', 'object', 'array'];
function TypedParseStorage(name, prototype) {
const handlers = new Map();
this.toJSON = () => handlers;
this.toString = () => JSON.stringify(handlers);
this[Symbol.iterator] = function* () {
yield* handlers.entries();
yield [name, prototype];
};

return new Proxy(this, {
ownKeys: () => handlers.keys(),
has: (_, prop) => handlers.has(prop),
set: (_, prop, value) => handlers.set(prop, value),
deleteProperty: (_, prop) => handlers.delete(prop),
get: (target, prop) => {
if (prop === Symbol.iterator) return this[Symbol.iterator].bind(target);
return handlers.get(prop);
},
});
}

module.exports = function ParseStorage(argTypes) {
const types = new Map([...argTypes]);
const store = STANDARD.reduce((acc, name) => {
acc[name] = new TypedParseStorage(name, types.get(name).parse);
types.delete(name);
return acc;
}, {});

for (const [name, Type] of types.entries()) {
if (!Type.parse) continue;
for (const type of Type.parse.targets) store[type][name] = Type.parse;
}

return types.clear(), Object.freeze(Object.assign(this, store));
};
44 changes: 0 additions & 44 deletions lib/proto/arrays.js

This file was deleted.

22 changes: 22 additions & 0 deletions lib/proto/arrays/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const astropack = require('astropack');
const struct = require('./struct');

const parse = (sample, parse) => {
if (!Array.isArray(sample)) return null;
const items = sample.reduce((acc, sample) => {
const plan = parse(sample);
for (const saved of acc) if (astropack.utils.equals(saved, plan)) return acc;
return acc.push(plan), acc;
}, []);
if (items.length === 0) return { type: 'array', items: ['unknown'] };
if (items.length === 1) return { type: 'array', items: [items[0]] };
return { type: 'array', items };
};

module.exports = {
array: struct(value => Array.isArray(value)),
set: struct(value => value?.constructor?.name === 'Set'),
};
module.exports.array.parse = Object.assign(parse, { targets: ['object'] });
31 changes: 31 additions & 0 deletions lib/proto/arrays/struct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

const ERR_MISS = 'Data type missconfiguration, expected: ';
const ERR_EMPTY = 'Empty list recived, but required';
module.exports = isInstance => ({
meta: { kind: 'struct', origin: 'default' },
construct(plan, tools) {
const { Error, build, getCondition } = tools;
this.required = plan.required ?? true;
this.items = plan.items.map(v => build(v));
this.condition = plan.condition ?? 'anyof';

this.ts = () => 'object';
this.test = (sample, path) => {
const createError = cause => new Error({ path, sample, plan, cause });
if (!isInstance(sample)) return [createError(ERR_MISS + this.type)];
const entries = [...sample];
if (!entries.length && this.required) return [createError(ERR_EMPTY)];
const errors = [];
for (let i = 0; i < entries.length; ++i) {
const condition = getCondition(this.condition, this.items.length - 1);
const suboption = { path: `${path}[${i}]`, sample: entries[i] };
const createError = cause => new tools.Error({ cause, plan, ...suboption });
const option = { createError, prototypes: this.items, ...suboption };
const result = tools.runCondition(condition, option);
if (result.length) errors.push(...result);
}
return errors;
};
},
});
35 changes: 35 additions & 0 deletions lib/proto/constructor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const defaultTest = () => [];
const INVALID = 'Invalid prototype, missing construct method';
const TEST_FAIL = `Didn't pass test`;
const RULE_FAIL = `Didn't pass rule: `;
const { parseResult: parse, setDefault } = require('./utils');

//? [Pre-preprocess] Purpose: Prototype wrapper and tester
module.exports = (name, proto, defaultMeta) => {
if (!proto?.construct || typeof proto.construct !== 'function') throw new Error(INVALID);
const meta = { type: name };
function Type(plan, tools) {
Object.assign(this, meta);
if (plan.meta) Object.assign(this, plan.meta);
proto.construct.call(this, plan, tools), setDefault(this, plan, tools);

const [test, rules] = [this.test ?? defaultTest, plan.rules ?? []];
this.test = (sample, path = 'root') => {
const options = { sample, path, tools, plan };
const errors = parse(test(sample, path), options, TEST_FAIL);
if (rules.length === 0) return errors;
for (let i = 0; i < rules.length; ++i) {
const result = parse(rules[i](sample), options, RULE_FAIL + i);
if (result.length > 0) errors.push(...result);
}
return errors;
};
return Object.freeze(this);
}
if (proto.meta) Object.assign(meta, proto.meta);
if (defaultMeta) Object.assign(meta, defaultMeta);
if (proto.parse && Array.isArray(proto.parse.targets)) Type.parse = proto.parse;
return Object.assign(Type, meta);
};
23 changes: 23 additions & 0 deletions lib/proto/constructor/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const parseResult = (res, options, cause) => {
const { sample, path, plan, tools } = options;
if (typeof res === 'object' && Array.isArray(res)) {
if (!res.length || res[0] instanceof tools.Error) return res;
return res.map(v => parseResult(v, options)).flat(2);
}
if (typeof res === 'boolean' && !res) return [new tools.Error({ sample, path, plan, cause })];
if (typeof res === 'string') return [new tools.Error({ sample, path, plan, cause: res })];
if (res instanceof tools.Error) return [res];
return [];
};

const setDefault = (ctx, plan, tools) => {
if (typeof plan.preprocess === 'function') ctx.preprocess = plan.preprocess;
if (typeof plan.postprocess === 'function') ctx.postprocess = plan.postprocess;
if (!('required' in ctx)) ctx.required = tools.isRequired(plan);
if (!ctx.origin) ctx.origin = 'custom';
if (!ctx.type) ctx.type = 'unknown';
};

module.exports = { parseResult, setDefault };
26 changes: 13 additions & 13 deletions lib/proto/exotic.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
'use strict';

const REQUIRED = 'Value is required';
const any = type => ({
meta: { kind: 'scalar', subtype: 'exotic' },
const ERR_REQUIRED = 'Value is required';
const ERR_MISS = 'Not of expected type: object';
const META = { kind: 'scalar', origin: 'default' };
const any = {
meta: META,
construct(plan, tools) {
const { isRequired, Error } = tools;
[this.type, this.required] = [type, isRequired(plan)];
this.required = isRequired(plan);
this.ts = () => this.type;
this.test = (sample, path) => {
if (!(!this.required && sample === undefined)) return [];
return [new Error({ path, sample, plan, cause: REQUIRED })];
return [new Error({ path, sample, plan, cause: ERR_REQUIRED })];
};
},
});
};

const json = {
meta: { kind: 'struct', subtype: 'exotic' },
meta: { kind: 'struct', origin: 'default' },
construct(plan, tools) {
const { isRequired, Error } = tools;
[this.type, this.required] = ['json', isRequired(plan)];
this.required = isRequired(plan);
this.ts = () => 'object';
this.test = (sample, path) => {
if (!this.required && sample === undefined) return [];
if (typeof sample === 'object' && sample) return [];
const err = cause => new Error({ path, sample, plan, cause });
if (this.required && sample === undefined) return [err(REQUIRED)];
return [err('Not of expected type: object')];
if (!this.required && sample === undefined) return [];
return [new Error({ path, sample, plan, cause: ERR_MISS })];
};
},
};

module.exports = { any: any('any'), unknown: any('unknown'), json };
module.exports = { any, unknown: any, json };
Loading

0 comments on commit b674fd4

Please sign in to comment.