diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d676c5..a1fb139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,31 +2,16 @@ ## [Unreleased][unreleased] -## [1.2.0][] - 2023-09-12 - -- Renamed from workspace -> node-workspace - -## [1.1.1][] - 2023-08-23 - -- Astrohelm config update -- Packages update - -## [1.1.0][] - 2023-08-02 - -- Prettier row length 120 -> 100 -- Makefile git commands -- Grep command -- Replace command -- Search between files command -- Search between rows command - -## [1.0.0][] - 2023-07-31 +## [0.1.0][] - 2023-10-16 - Stable release version +- Warnings before testing - Repository created - -[unreleased]: https://github.com/astrohelm/workspace/compare/v1.2.0...HEAD -[1.1.1]: https://github.com/astrohelm/workspace/compare/v1.1.1...v1.2.0 -[1.1.1]: https://github.com/astrohelm/workspace/compare/v1.1.0...v1.1.1 -[1.1.0]: https://github.com/astrohelm/workspace/compare/release...v1.1.0 -[1.0.0]: https://github.com/astrohelm/workspace/releases/tag/release +- Namespaces, runtime schema checking, custom types +- Default struct types: Array, Set, Map, Object +- Default scalar types: String, Boolean, Number, BigInt +- Default exotic types: Any, Undefined, JSON +- Custom Errors + +[unreleased]: https://github.com/astrohelm/workspace/compare/release...HEAD +[0.1.0]: https://github.com/astrohelm/workspace/releases/tag/release diff --git a/dist/.eslintrc b/dist/.eslintrc deleted file mode 100644 index ff83dcd..0000000 --- a/dist/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "parserOptions": { - "sourceType": "module" - } -} \ No newline at end of file diff --git a/lib/custom/prototypes.js b/lib/custom/prototypes.js deleted file mode 100644 index ed214bc..0000000 --- a/lib/custom/prototypes.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const parse = () => ({ type: 'date' }); -const isCompatible = sample => sample instanceof Date; -Object.assign(parse, { isCompatible, targets: ['object'] }); - -const date = { - kind: 'scalar', - parse, - build: () => ({ - test: sample => !isNaN(new Date(sample)), - error: args => `Field "${args.path}" contains invalid date`, - }), -}; - -module.exports = { date }; diff --git a/lib/custom/rules.js b/lib/custom/rules.js deleted file mode 100644 index a6445ed..0000000 --- a/lib/custom/rules.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const sizeRule = sample => { - if (sample < 5) return ['Sample size must be greater than 5']; - if (sample > 100) return ['Sample size must be lesser than 100']; - return []; -}; diff --git a/lib/index.js b/lib/index.js index aa43bae..363f970 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,6 @@ 'use strict'; -module.exports = function () {}; +const prototypes = require('./proto'); +const Schema = require('./schema'); + +module.exports = { Schema, prototypes }; diff --git a/lib/schema/from.js b/lib/parser/store/from.js similarity index 100% rename from lib/schema/from.js rename to lib/parser/store/from.js diff --git a/lib/types/proto/arrays.js b/lib/proto/arrays.js similarity index 97% rename from lib/types/proto/arrays.js rename to lib/proto/arrays.js index 74c9553..7fcfc20 100644 --- a/lib/types/proto/arrays.js +++ b/lib/proto/arrays.js @@ -6,7 +6,7 @@ const array = { kind: 'struct', isInstance: value => Array.isArray(value), construct(schema, tools) { - const { Error, builders, conditions } = tools.builder; + const { Error, builders, conditions } = tools; const builded = builders.array(schema, tools); const { type, condition = 'anyof', required } = schema; return (sample, path) => { diff --git a/lib/proto/enum.js b/lib/proto/enum.js deleted file mode 100644 index c941064..0000000 --- a/lib/proto/enum.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const enumerable = type => { - const compare = value => type.includes(value); - const error = args => `Enum ${JSON.stringify(type)} doesn't include ${args.value}`; - return Object.assign(compare, { type, error, kind: 'scalar' }); -}; - -module.exports = enumerable; diff --git a/lib/types/proto/exotic.js b/lib/proto/exotic.js similarity index 88% rename from lib/types/proto/exotic.js rename to lib/proto/exotic.js index 2d770dc..420239b 100644 --- a/lib/types/proto/exotic.js +++ b/lib/proto/exotic.js @@ -4,7 +4,7 @@ const any = type => ({ kind: 'scalar', type, construct(schema, tools) { - const { isRequired, Error } = tools.builder; + const { isRequired, Error } = tools; const required = isRequired(schema); return (sample, path) => { if (!(required && sample === undefined)) return []; @@ -16,7 +16,7 @@ const any = type => ({ const json = { kind: 'struct', construct: (schema, tools) => { - const { Error } = tools.builder; + const { Error } = tools; return (sample, path) => { if (typeof sample === 'object' && sample) return []; return [new Error({ path, sample, schema, cause: 'Not of expected type: object' })]; diff --git a/lib/proto/index.js b/lib/proto/index.js index c28254c..09109e8 100644 --- a/lib/proto/index.js +++ b/lib/proto/index.js @@ -1,14 +1,8 @@ 'use strict'; -const PROTO = { - ...require('../types/proto/scalars'), - enum: require('./enum'), -}; -const getPrototype = plan => { - const isShorthand = typeof plan === 'string'; - let type = isShorthand ? plan : plan.type; - if (isShorthand) type = type.substring(1); - return PROTO[type](plan); +module.exports = { + ...require('./scalars'), + ...require('./objects'), + ...require('./arrays'), + ...require('./exotic'), }; - -module.exports = { PROTO, getPrototype }; diff --git a/lib/types/proto/objects.js b/lib/proto/objects.js similarity index 97% rename from lib/types/proto/objects.js rename to lib/proto/objects.js index 31fadb7..389455d 100644 --- a/lib/types/proto/objects.js +++ b/lib/proto/objects.js @@ -5,7 +5,7 @@ const object = { entries: value => Object.entries(value), isInstance: value => typeof value === 'object', construct(schema, tools) { - const { Error, builders } = tools.builder; + const { Error, builders } = tools; const { search, required } = builders['object'](schema, tools); return (sample, path) => { const err = cause => new Error({ path, sample, schema, cause }); diff --git a/lib/types/proto/scalars.js b/lib/proto/scalars.js similarity index 93% rename from lib/types/proto/scalars.js rename to lib/proto/scalars.js index 464e79a..514d360 100644 --- a/lib/types/proto/scalars.js +++ b/lib/proto/scalars.js @@ -8,7 +8,7 @@ const scalar = type => { kind: 'scalar', parse: Object.assign(parse, { isCompatible, targets: [type] }), construct(schema, tools) { - const { isRequired, Error } = tools.builder; + const { isRequired, Error } = tools; const required = isRequired(schema); return (sample, path) => { if (typeof sample === type) return []; diff --git a/lib/schema/build.js b/lib/schema/build.js new file mode 100644 index 0000000..4119d2b --- /dev/null +++ b/lib/schema/build.js @@ -0,0 +1,22 @@ +'use strict'; + +const preprocess = require('./preprocess'); +module.exports = (types, tools, schema) => { + const tests = preprocess(types, tools, schema); + const { typeCondition = 'anyof' } = schema; + return (sample, path = 'root') => { + const handler = tools.conditions(typeCondition, tests.length - 1); + const errors = []; + for (let i = 0; i < tests.length; ++i) { + const result = tests[i](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; + }; +}; diff --git a/lib/types/error.js b/lib/schema/error.js similarity index 100% rename from lib/types/error.js rename to lib/schema/error.js diff --git a/lib/schema/index.js b/lib/schema/index.js index 05a6efa..ec35421 100644 --- a/lib/schema/index.js +++ b/lib/schema/index.js @@ -1,26 +1,24 @@ 'use strict'; -const schemaFrom = require('./from'); -const prototypes = require('../proto'); +const arrayBuilder = (schema, { build }) => schema.items.map(v => build(v)); +const [objectBuilder, createError] = [require('./object'), require('./error')]; +const builders = { object: objectBuilder, array: arrayBuilder }; +const tooling = { Error: createError(), ...require('./utils'), builders }; +const build = require('./build'); -//TODO Type fabric, to fix existed and passed types -//TODO Parser that creates test for Schema, append Models, and tests it for errors -const Schema = function (plan, options = {}) { - const { namespace, types, rules } = options; +module.exports = function Schema(schema, { errorPattern, ...options }) { + const tools = { ...tooling, build: null, warn: null }; + if (errorPattern) tools.Error = createError({ pattern: errorPattern }); + const warn = options => { + const err = new tooling.Error(options); + return this.warnings.push(err), err; + }; - this.plan = plan; - this.test = sample => { - const isShorthand = typeof plan === 'string'; - let planType = isShorthand ? plan : plan.type; - if (isShorthand) { - if (planType[0] === '?') planType = planType.substring(1); - const errors = prototypes[planType](sample); - } + [tools.build, tools.warn] = [build.bind(null, options, tools), warn]; - const result = { valid: true, errors: [] }; - return result; - }; + this.warnings = []; + this.test = tools.build(schema); + return Object.freeze(this); }; -module.exports = Schema; -module.exports.from = sample => new Schema(schemaFrom(sample)); +// schema check, dts generation, diff --git a/lib/types/builder/object.js b/lib/schema/object.js similarity index 82% rename from lib/types/builder/object.js rename to lib/schema/object.js index 77ef74f..d0d7c07 100644 --- a/lib/types/builder/object.js +++ b/lib/schema/object.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = (schema, { builder }) => { +module.exports = (schema, { build, isRequired }) => { const builded = { properties: {}, patternProperties: {} }; const requires = new Map(); @@ -8,8 +8,8 @@ module.exports = (schema, { builder }) => { if (!schema[propType]) continue; const entries = Object.entries(schema[propType]); for (const [key, value] of entries) { - builded[propType][key] = builder.build(value); - const required = builder.isRequired(value); + builded[propType][key] = build(value); + const required = isRequired(value); if (required) requires.set(key); } } diff --git a/lib/types/preprocess.js b/lib/schema/preprocess.js similarity index 60% rename from lib/types/preprocess.js rename to lib/schema/preprocess.js index da9a4cc..558119b 100644 --- a/lib/types/preprocess.js +++ b/lib/schema/preprocess.js @@ -1,14 +1,20 @@ 'use strict'; +const path = 'PREPROCESS'; +const { string } = require('astropack'); const { typeOf, isShorthand } = require('./utils'); -module.exports = (schema, types, tools) => { - const tests = []; - const path = 'PREPROCESS'; - const { warn } = tools; +module.exports = ({ types, namespace }, tools, schema) => { + const [tests, { warn }] = [[], tools]; const signal = (cause, sample, sampleType) => warn({ sample, sampleType, path, cause }); - for (const type of typeOf(schema)) { + if (string.case.isFirstUpper(type)) { + const prototype = namespace[type]; + if (prototype) { + tests.push(prototype.test); + continue; + } + } const prototype = types[type]; if (!prototype) { const err = signal('Missing prototype', prototype, type); @@ -20,9 +26,7 @@ module.exports = (schema, types, tools) => { tests.push(() => [err]); continue; } - - const test = prototype.construct(schema, tools); - tests.push(test); + tests.push(prototype.construct(schema, tools)); } return tests; }; diff --git a/lib/types/utils.js b/lib/schema/utils.js similarity index 100% rename from lib/types/utils.js rename to lib/schema/utils.js diff --git a/lib/types/builder/index.js b/lib/types/builder/index.js deleted file mode 100644 index b97c6d4..0000000 --- a/lib/types/builder/index.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const arrayBuilder = (schema, { builder }) => schema.items.map(v => builder.build(v)); -const [objectBuilder, createError] = [require('./object'), require('../error')]; -const preprocess = require('../preprocess'); - -module.exports = function Builder(types) { - Object.assign(this, Object.freeze(require('../utils'))); - this.builders = { object: objectBuilder, array: arrayBuilder }; - this.Error = createError(); - - this.build = schema => { - const warnings = []; - const warn = options => { - const err = new this.Error(options); - return warnings.push(err), err; - }; - const tooling = { builder: Object.freeze({ ...this, build }), warn }; - function build(schema, tools = tooling) { - const tests = preprocess(schema, types, tools); - const { typeCondition = 'anyof' } = schema; - return (sample, path = 'root') => { - const handler = tools.builder.conditions(typeCondition, tests.length - 1); - const errors = []; - for (let i = 0; i < tests.length; ++i) { - const result = tests[i](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; - }; - } - return Object.assign(build(schema), { warnings }); - }; - return Object.freeze(this); -}; diff --git a/lib/types/index.js b/lib/types/index.js deleted file mode 100644 index 04bea83..0000000 --- a/lib/types/index.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -const prototypes = require('./proto'); -const Builder = require('./builder'); - -module.exports = { Builder, prototypes }; diff --git a/lib/types/proto/index.js b/lib/types/proto/index.js deleted file mode 100644 index 09109e8..0000000 --- a/lib/types/proto/index.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -module.exports = { - ...require('./scalars'), - ...require('./objects'), - ...require('./arrays'), - ...require('./exotic'), -}; diff --git a/package.json b/package.json index ffa29cd..99c4010 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,27 @@ { "license": "MIT", - "version": "1.2.0", + "version": "0.1.0", "type": "commonjs", - "name": "astrohelm-workspace", + "name": "astroplan", "homepage": "https://astrohelm.ru", "description": "Astrohelm workspace example", "author": "Alexander Ivanov ", "keywords": [ "nodejs", - "zero-dependencies", - "prettier", - "eslint", - "ts", - "workspace", - "example", - "preset", - "starter-kit", - "astrohelm", "javascript", - "typescript" + "testing", + "metadata", + "json", + "schema", + "validation", + "types", + "checker", + "dsl", + "metalanguage", + "runtime-verification", + "zero-dependencies", + "type-generator", + "astrohelm" ], "main": "index.js", "types": "types/index.d.ts", @@ -28,7 +31,7 @@ "node": "18 || 19 || 20" }, "browser": {}, - "files": ["/dist", "/lib", "/types"], + "files": ["/lib", "/types"], "scripts": { "test": "node --test", "dev": "node index.js", diff --git a/tests/builder.test.js b/tests/builder.test.js index 2d3030f..391cbcd 100644 --- a/tests/builder.test.js +++ b/tests/builder.test.js @@ -1,31 +1,7 @@ 'use strict'; const [test, assert] = [require('node:test'), require('node:assert')]; -const { Builder, prototypes } = require('../lib/types'); -const builder = new Builder(prototypes); - -test('Builder constructor', () => { - assert.strictEqual(typeof builder, 'object'); - assert.strictEqual(typeof builder.build, 'function'); - assert.strictEqual(typeof builder.typeOf, 'function'); - assert.strictEqual(typeof builder.isShorthand, 'function'); - assert.strictEqual(typeof builder.isRequired, 'function'); - assert.strictEqual(typeof builder.conditions, 'function'); - assert.strictEqual(typeof builder.builders, 'object'); - assert.strictEqual(typeof builder.builders.array, 'function'); - assert.strictEqual(typeof builder.builders.object, 'function'); - assert.strictEqual(typeof builder.Error, 'function'); -}); - -test('Builder error', () => { - const { Error } = builder; - const err = new Error({ path: 'Test', schema: {}, cause: 'Test', sampleType: 'string' }); - assert.strictEqual(typeof err, 'object'); - assert.strictEqual(JSON.stringify(err), '{"path":"Test","cause":"Test","count":1}'); - assert.strictEqual('' + err, `[Test] => Test`); - assert.strictEqual(typeof err.add, 'function'); - assert.strictEqual('' + err.add('New').add('Test'), `[Test] => Test, New, Test`); -}); +const { Schema, prototypes } = require('../lib'); test('Schema with errors & warnings', () => { const schema = { @@ -48,13 +24,13 @@ test('Schema with errors & warnings', () => { hello: 'world', 123: 'test', //? +2 Exoitic, Missing field "z" }; - const test = builder.build(schema); - assert.strictEqual(test.warnings.length, 1); - const { cause, message, path } = test.warnings[0]; + const plan = new Schema(schema, { types: prototypes }); + assert.strictEqual(plan.warnings.length, 1); + const { cause, message, path } = plan.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 = test(sample); + const errors = plan.test(sample); assert.strictEqual(errors.length, 7); }); @@ -87,8 +63,44 @@ test('Schema without errors & warnings', () => { hello: 'world', z: 'test', }; - const test = builder.build(schema); - assert.strictEqual(test.warnings.length, 0); - const errors = test(sample); + const plan = new Schema(schema, { types: prototypes }); + assert.strictEqual(plan.warnings.length, 0); + const errors = plan.test(sample); + assert.strictEqual(errors.length, 0); +}); + +test('Schema with namespace', () => { + const schema = { + type: 'object', + 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', + }; + const PlanA = new Schema(schema, { types: prototypes }); + const PlanB = new Schema({ type: 'PlanA' }, { types: prototypes, namespace: { PlanA } }); + assert.strictEqual(PlanB.warnings.length + PlanA.warnings.length, 0); + const errors = PlanB.test(sample); assert.strictEqual(errors.length, 0); });