diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c5f24..23966ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ## [Unreleased][unreleased] - + + +## [0.6.0][] - 2023-11-00 + +- Typescript generation module +- Documentation fixes ## [0.5.0][] - 2023-11-05 @@ -86,7 +89,8 @@ - Default exotic types: Any, Undefined, JSON - Custom Errors -[unreleased]: https://github.com/astrohelm/metaforge/compare/v0.5.0...HEAD +[unreleased]: https://github.com/astrohelm/metaforge/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/astrohelm/metaforge/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/astrohelm/metaforge/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/astrohelm/metaforge/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/astrohelm/metaforge/compare/v0.2.0...v0.3.0 diff --git a/README.md b/README.md index 9b89ddd..4e3f521 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -
['string', 'number']
- enum shorthand: ['winter', 'spring']
- schema shorthand: new Schema('?string')
+
+### Example:
+
+```js
+const schema = new Schema(
+ {
+ a: 'string', //? scalar shorthand
+ b: '?string', //? optional shorthand
+ c: ['string', 'string'], //? tuple
+ d: new Schema('?string'), //? Schema shorthand
+ e: ['winter', 'spring'], //? Enum shorthand
+ f: { a: 'number', b: 'string' }, //? Object shorthand
+ g: { $type: 'array', items: 'string' }, //? Array items shorthand
+ h: 'MyExternalSchema',
+ },
+ { namespace: { MyExternalSchema: new Schema('string') } },
+);
+```
+
+> String shorthand is analog to { $type: type, required: type.includes('?') }
diff --git a/modules/handyman/index.test.js b/modules/handyman/index.test.js
new file mode 100644
index 0000000..63f9c62
--- /dev/null
+++ b/modules/handyman/index.test.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const [test, assert] = [require('node:test'), require('node:assert')];
+const Schema = require('../..');
+
+test('[Handyman] Schema with namespace', () => {
+ 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);
+});
+
+test('[Handyman] Pull schemas', () => {
+ const schema = new Schema({
+ $id: 'MySchema',
+ a: 'string',
+ b: { $id: 'MySubSchema', c: 'number' },
+ c: new Schema('?string'),
+ d: { $type: 'schema', schema: new Schema('number'), $id: 'MySubSchema2' },
+ e: { $type: 'schema', schema: new Schema({ $type: 'number', $id: 'MySubSchema3' }) },
+ });
+
+ assert.strictEqual(schema.warnings.length, 0);
+ assert.strictEqual(!!schema.pull('MySchema'), false);
+ assert.strictEqual(!!schema.pull('MySubSchema'), true);
+ assert.strictEqual(!!schema.pull('MySubSchema2'), true);
+ assert.strictEqual(!!schema.pull('MySubSchema3'), true);
+});
+
+test('[Handyman] Shorthands', () => {
+ const schema = new Schema(
+ {
+ a: 'string', //? scalar shorthand
+ b: '?string', //? optional shorthand
+ c: ['string', 'string'], //? tuple
+ d: new Schema('?string'), //? Schema shorthand
+ e: ['winter', 'spring'], //? Enum shorthand
+ f: { a: 'number', b: 'string' }, //? Object shorthand
+ g: { $type: 'array', items: 'string' }, //? Array items shorthand
+ h: 'MyExternalSchema',
+ },
+ { namespace: { MyExternalSchema: new Schema('string') } },
+ );
+
+ assert.strictEqual(schema.warnings.length, 0);
+});
diff --git a/tests/basic.test.js b/modules/test/basic.test.js
similarity index 98%
rename from tests/basic.test.js
rename to modules/test/basic.test.js
index 1faebd9..7c49403 100644
--- a/tests/basic.test.js
+++ b/modules/test/basic.test.js
@@ -1,7 +1,7 @@
'use strict';
const [test, assert] = [require('node:test'), require('node:assert')];
-const Schema = require('..');
+const Schema = require('../..');
test('Schema without errors & warnings', () => {
const userSchema = new Schema({
diff --git a/modules/test/index.js b/modules/test/index.js
index b393f60..dfe0156 100644
--- a/modules/test/index.js
+++ b/modules/test/index.js
@@ -13,11 +13,11 @@ module.exports = (schema, options) => {
const Error = schema.tools.Error;
function TestWrapper(plan) {
- if (plan.$type === 'schema') return this.test.bind(this);
+ if (plan.$type === 'schema') return this.test;
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));
+ typeof this.test === 'function' && tests.unshift(this.test);
this.test = (sample, path = 'root', isPartial = false) => {
if (sample === undefined || sample === null) {
if (!this.$required) return [];
diff --git a/tests/rules.test.js b/modules/test/rules.test.js
similarity index 97%
rename from tests/rules.test.js
rename to modules/test/rules.test.js
index 519f7cf..e65f09c 100644
--- a/tests/rules.test.js
+++ b/modules/test/rules.test.js
@@ -1,7 +1,7 @@
'use strict';
const [test, assert] = [require('node:test'), require('node:assert')];
-const Schema = require('..');
+const Schema = require('../..');
test('Rules', () => {
const rule1 = sample => sample?.length > 5;
diff --git a/modules/types/README.md b/modules/types/README.md
index 8d1564e..cf02ebb 100644
--- a/modules/types/README.md
+++ b/modules/types/README.md
@@ -1,3 +1,94 @@
-# Metatype module / generate typescript:JSDOC from schema
+# Metatype module
-> WORK IN PROGRESS
+Generate type annotation from schema;
+
+> Warning: You will receive compressed version;
+
+## Usage
+
+By default module runs in mjs mode, that means that:
+
+- It will export all schemas with $id field & root schema
+- it will export as default root schema
+
+> In cjs mode, it will export only root schema
+
+```js
+const plan = 'string';
+const schema = new Schema(plan);
+schema.dts('Example', { mode: 'mjs' });
+// type Example = string;
+// export type = { Example };
+// export default Example;
+```
+
+## Example
+
+### Input:
+
+```js
+{
+ "firstName": 'string',
+ "lastName": 'string',
+ "label": ["member", "guest", "vip"]
+ "age": '?number',
+ settings: { alertLevel: 'string', $id: 'Setting' }
+}
+```
+
+### Output (mjs mode):
+
+```ts
+interface Settings {
+ alertLevel: string;
+}
+
+interface Example {
+ firstName: string;
+ lastName: string;
+ label: 'member' | 'guest' | 'vip';
+ age?: number;
+ settings: Settings;
+}
+
+export type { Example };
+export default Example;
+```
+
+### Output (cjs mode):
+
+```ts
+interface Settings {
+ alertLevel: string;
+}
+
+interface Example {
+ firstName: string;
+ lastName: string;
+ label: 'member' | 'guest' | 'vip';
+ settings: Settings;
+ age?: number;
+}
+
+export = Example;
+```
+
+## Writing custom prototypes with Metatype
+
+By default all custom types will recieve unknown type; If you want to have custom type, you may
+create custom prototype with toTypescript field;
+
+```js
+function Date(plan, tools) {
+ this.toTypescript = (name, namespace) => {
+ // If you want to have your type in exports, you can add it name to exports;
+ const { definitions, exports } = namespace;
+ // If your type is complex, you can push your builded type / interface to definitions and return it name
+ const type = `type ${name} = Date`;
+ definitions.add(type);
+ //? You can return only name or value that can be assigned to type
+ return name; // Equal to:
+ return 'Date';
+ };
+}
+```
diff --git a/modules/types/index.js b/modules/types/index.js
index 787fa93..71de5a5 100644
--- a/modules/types/index.js
+++ b/modules/types/index.js
@@ -1,11 +1,33 @@
'use strict';
+const { nameFix } = require('./utils');
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);
+ schema.forge.attach('before', { toTypescript: () => 'unknown' });
+ schema.forge.attach('after', function TypescriptWrapper() {
+ const compile = this.toTypescript;
+ this.toTypescript = (name, namespace) => compile(nameFix(name), namespace);
+ });
+
+ schema.dts = (name = 'MetaForge', options = {}) => {
+ const mode = options.mode ?? 'mjs';
+ if (name !== nameFix(name)) throw new Error('Invalid name format');
+ const namespace = { definitions: new Set(), exports: new Set() };
+ const type = schema.toTypescript(name, namespace);
+ if (type !== name) {
+ if (namespace.exports.size === 1) {
+ const definitions = Array.from(namespace.definitions).join('');
+ if (mode === 'cjs') return definitions + `export = ${type}`;
+ return definitions + `export type ${name}=${type};export default ${type};`;
+ }
+ namespace.definitions.add(`type ${name}=${type};`);
+ }
+ namespace.exports.add(name);
+ const definitions = Array.from(namespace.definitions).join('');
+ if (mode === 'cjs') return definitions + `export = ${name};`;
+ const exports = `export type{${Array.from(namespace.exports).join(',')}};`;
+ return definitions + exports + `export default ${name};`;
+ };
};
diff --git a/modules/types/index.test.js b/modules/types/index.test.js
new file mode 100644
index 0000000..40b56a0
--- /dev/null
+++ b/modules/types/index.test.js
@@ -0,0 +1,95 @@
+/* eslint-disable quotes */
+'use strict';
+
+const [test, assert] = [require('node:test'), require('node:assert')];
+const Schema = require('../../');
+
+const generate = (type, name) => new Schema(type).dts(name);
+const base = 'type MetaForge=';
+const exp = 'export type{MetaForge};export default MetaForge;';
+test('[DTS] Basic', () => {
+ assert.strictEqual(generate({ $type: 'string' }), base + 'string;' + exp);
+ assert.strictEqual(generate('number'), base + 'number;' + exp);
+ assert.strictEqual(generate('bigint'), base + 'bigint;' + exp);
+ assert.strictEqual(generate('boolean'), base + 'boolean;' + exp);
+ assert.strictEqual(generate('unknown'), base + 'unknown;' + exp);
+ assert.strictEqual(generate('?any'), base + '(any|null|undefined);' + exp);
+});
+
+test('[DTS] Enumerable', () => {
+ assert.strictEqual(generate(['hello', 'world']), base + "('hello'|'world');" + exp);
+ const data = ['hello', 'there', 'my', 'dear', 'world'];
+ const result = `type MetaForge='hello'|'there'|'my'|'dear'|'world';`;
+ assert.strictEqual(generate(data), result + exp);
+});
+
+test('[DTS] Union', () => {
+ assert.strictEqual(
+ generate({ $type: 'union', types: ['string', '?number'] }),
+ 'type MetaForge=(string|(number|null|undefined));' + exp,
+ );
+ assert.strictEqual(
+ generate({ $type: 'union', types: [{ $type: 'union', types: ['string', '?number'] }] }),
+ 'type MetaForge=((string|(number|null|undefined)));' + exp,
+ );
+});
+
+test('[DTS] Array', () => {
+ assert.strictEqual(
+ generate(['string', '?number']),
+ 'type MetaForge=[string,(number|null|undefined)];' + exp,
+ );
+ assert.strictEqual(
+ generate({ $type: 'set', items: ['string', '?number'] }),
+ 'type MetaForge=Set