From 2991d83bcc6dc638275f015a8edad67547945cb6 Mon Sep 17 00:00:00 2001 From: Michael Brich Date: Mon, 20 May 2024 10:56:50 -0700 Subject: [PATCH] Type cleanup & fixes for SchemaFieldType combined with custom type keys. --- src/{schema/built/ins.ts => builtin/types.ts} | 44 ++++++++++--------- src/custom/schema/verify.ts | 4 +- src/custom/types.ts | 22 ++++++++-- src/schema.ts | 20 ++++----- src/schema/field.ts | 10 ++++- src/schema/field/data.ts | 7 ++- src/schema/field/type.ts | 4 +- src/schema/output/transformer.ts | 4 +- src/schema/verify/value.ts | 4 +- tests/_data/schema.ts | 26 ++++++++++- tests/custom/types.spec.ts | 30 +++++++++++++ tests/schema.spec.ts | 6 +-- tests/schema/recursive.spec.ts | 9 ++-- tests/schema/rulesets.spec.ts | 34 ++++++++++++++ 14 files changed, 169 insertions(+), 55 deletions(-) rename src/{schema/built/ins.ts => builtin/types.ts} (76%) create mode 100644 tests/schema/rulesets.spec.ts diff --git a/src/schema/built/ins.ts b/src/builtin/types.ts similarity index 76% rename from src/schema/built/ins.ts rename to src/builtin/types.ts index bfc1d44..cb0e884 100644 --- a/src/schema/built/ins.ts +++ b/src/builtin/types.ts @@ -23,28 +23,30 @@ * */ -import {type SchemaFieldType} from '../field/type'; +import {type SchemaFieldType} from '../schema/field/type'; /** * @category Schemas */ -export const schemaBuiltIns: SchemaFieldType[] = [ - 'array', - 'bigint', - 'BigInt', - 'boolean', - 'datetime', - 'dbl', - 'float', - 'int', - 'iterable', - 'json-serialized', - 'json', - 'null', - 'number', - 'string', - 'time', - 'uint', - 'undefined', - 'url' -]; +export function builtinTypes(): SchemaFieldType[] { + return [ + 'array', + 'bigint', + 'BigInt', + 'boolean', + 'datetime', + 'dbl', + 'float', + 'int', + 'iterable', + 'json-serialized', + 'json', + 'null', + 'number', + 'string', + 'time', + 'uint', + 'undefined', + 'url' + ]; +} diff --git a/src/custom/schema/verify.ts b/src/custom/schema/verify.ts index 9e1f511..d778e04 100644 --- a/src/custom/schema/verify.ts +++ b/src/custom/schema/verify.ts @@ -28,6 +28,6 @@ import {type SchemaVerifyInit} from '../../schema/verify/init'; /** * @category Schemas - Custom Types */ -export interface CustomSchemaVerify extends SchemaVerifyInit { - type: string; +export interface CustomSchemaVerify extends SchemaVerifyInit { + typeId: string; } diff --git a/src/custom/types.ts b/src/custom/types.ts index 7373928..4f92967 100644 --- a/src/custom/types.ts +++ b/src/custom/types.ts @@ -80,6 +80,11 @@ export class CustomTypes, VerifiedT = In return this.registered.has(id); } + /** + * Check whether `id` is a registered custom type schema. Doesn't return true + * when `id`is registered with a non-schema. + * @param id + */ public hasSchema(id: string): boolean { if (!this.has(id)) { return false; @@ -90,9 +95,14 @@ export class CustomTypes, VerifiedT = In return false; } - return typeof o?.verify === 'function'; + return this.isSchema(o); } + /** + * Check whether `id` is a registered custom type schema. Doesn't return true + * when `id`is registered with a non-schema. + * @param id + */ public hasVerifier(id: string): boolean { if (!this.has(id)) { return false; @@ -173,7 +183,8 @@ export class CustomTypes, VerifiedT = In return false; } - return true; + const schema = o as Schema; + return typeof schema?.verify === 'function'; } public async verifyValue(id: string, type: string, value: unknown, base: Log): Promise> { @@ -186,15 +197,18 @@ export class CustomTypes, VerifiedT = In public async verifyOnly(init: CustomSchemaVerify): Promise>> { const fate = new Fate>(); - const schema = this.getSchema(init.type); + const schema = this.getSchema(init.typeId); if (!schema) { - return fate.setErrorCode(schemaError('missing_custom_type_schema', init.type)); + return fate.setErrorCode(schemaError('missing_schema_typeId', init.typeId)); } return schema.verifyOnly(init); } + /** + * Reset all properties to their initial values. + */ public reset(): void { this.registered.clear(); } diff --git a/src/schema.ts b/src/schema.ts index 1f3f912..78f8de1 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -38,7 +38,7 @@ import {isUInt} from './is/uint'; import {isInt} from './is/int'; import {type SchemaFieldData} from './schema/field/data'; import {CustomTypes} from './custom/types'; -import {schemaBuiltIns} from './schema/built/ins'; +import {builtinTypes} from './builtin/types'; import {valueTypeLabel} from './value/type/label'; import {SchemaPath} from './schema/path'; import {type SchemaVerifyInit} from './schema/verify/init'; @@ -162,15 +162,11 @@ export class Schema, VerifiedT = InputT> ); } - public isBuiltIn(type: SchemaFieldType): boolean { - if (typeof type !== 'string') { - return false; - } - - return schemaBuiltIns.includes(type); + public isBuiltIn(type: SchemaFieldType): boolean { + return builtinTypes().includes(type); } - public schemaSupportsType(type: SchemaFieldType): boolean { + public schemaSupportsType(type: SchemaFieldType): boolean { if (this.isBuiltIn(type)) { return true; } @@ -183,7 +179,7 @@ export class Schema, VerifiedT = InputT> * @param type * @param value */ - public valueIsBuiltInType(type: SchemaFieldType, value: unknown): value is DataT { + public valueHasBuiltinType(type: SchemaFieldType, value: unknown): value is DataT { if (typeof type !== 'string') { return false; } @@ -224,11 +220,11 @@ export class Schema, VerifiedT = InputT> * @param type * @param value */ - public async verifyValue(init: SchemaVerifyValue): Promise>> { + public async verifyValue(init: SchemaVerifyValue): Promise>> { const fate = new Fate>(); if (this.isBuiltIn(init.fieldType)) { - if (this.valueIsBuiltInType(init.fieldType, init.value)) { + if (this.valueHasBuiltinType(init.fieldType, init.value)) { // TODO: Add validation here. Type match does not automatically prove valid content. fate.data = init.value; return fate.setSuccess(true); @@ -245,7 +241,7 @@ export class Schema, VerifiedT = InputT> if (this.customTypes.hasSchema(init.fieldType) && typeof init.value === 'object') { return this.customTypes.verifyOnly({ id: init.fieldId, - type: init.fieldType, + typeId: init.fieldType, data: init.value as SchemaData, path: init.path, base: init.base, diff --git a/src/schema/field.ts b/src/schema/field.ts index 8c809cc..95ec0fc 100644 --- a/src/schema/field.ts +++ b/src/schema/field.ts @@ -23,6 +23,7 @@ * */ +import {Ruleset} from '../ruleset'; import {type SchemaFieldData} from './field/data'; import {type SchemaFieldType} from './field/type'; @@ -32,12 +33,19 @@ import {type SchemaFieldType} from './field/type'; export class SchemaField { public readonly name: string; public readonly key: keyof InputT; - public readonly types: SchemaFieldType[]; + public readonly types: SchemaFieldType[]; public readonly defaultValue: unknown; + public readonly ruleset: Ruleset; constructor(data: SchemaFieldData) { this.key = data.name; this.name = data.name.toString(); + this.ruleset = new Ruleset(); + + if (Array.isArray(data.rules)) { + this.ruleset.add(...data.rules); + } + if (Array.isArray(data.types)) { this.types = data.types; } else if (typeof data.types === 'string') { diff --git a/src/schema/field/data.ts b/src/schema/field/data.ts index 860e689..0c8f107 100644 --- a/src/schema/field/data.ts +++ b/src/schema/field/data.ts @@ -23,13 +23,16 @@ * */ -import type {SchemaFieldType} from './type'; +import {Block} from '../../block'; +import {Statement} from '../../statement'; +import {type SchemaFieldType} from './type'; /** * @category Schemas */ export interface SchemaFieldData { name: keyof InputT; - types: SchemaFieldType | (SchemaFieldType | keyof InputT)[] | keyof InputT[]; + types: SchemaFieldType | SchemaFieldType[]; defaultValue?: unknown; + rules?: Block>[]; } diff --git a/src/schema/field/type.ts b/src/schema/field/type.ts index 7a43d06..ea13dcb 100644 --- a/src/schema/field/type.ts +++ b/src/schema/field/type.ts @@ -26,7 +26,7 @@ /** * @category Schemas */ -export type SchemaFieldType = +export type SchemaFieldType = | 'array' | 'bigint' | 'BigInt' @@ -47,4 +47,4 @@ export type SchemaFieldType = | 'undefined' | 'url' | 'time' - | keyof CustomT; + | Extract; diff --git a/src/schema/output/transformer.ts b/src/schema/output/transformer.ts index d2924de..38977bf 100644 --- a/src/schema/output/transformer.ts +++ b/src/schema/output/transformer.ts @@ -25,7 +25,7 @@ import {Fate} from '@toreda/fate'; import {Log} from '@toreda/log'; -import {type VerifiedResult} from '../../verified/schema'; +import {type VerifiedSchema} from '../../verified/schema'; /** * Transforms schema parser output. @@ -33,6 +33,6 @@ import {type VerifiedResult} from '../../verified/schema'; * @category Schemas */ export type SchemaOutputTransformer = ( - mapped: VerifiedResult, + mapped: VerifiedSchema, base: Log ) => Promise>; diff --git a/src/schema/verify/value.ts b/src/schema/verify/value.ts index 061d4a5..24c6f3a 100644 --- a/src/schema/verify/value.ts +++ b/src/schema/verify/value.ts @@ -31,9 +31,9 @@ import {type SchemaData} from '../data'; /** * @category Schemas */ -export interface SchemaVerifyValue { +export interface SchemaVerifyValue { fieldId: string; - fieldType: SchemaFieldType; + fieldType: SchemaFieldType; path: SchemaPath; value: unknown | SchemaData; base: Log; diff --git a/tests/_data/schema.ts b/tests/_data/schema.ts index 38895ad..99590d2 100644 --- a/tests/_data/schema.ts +++ b/tests/_data/schema.ts @@ -2,7 +2,7 @@ import {Log} from '@toreda/log'; import {Schema} from '../../src/schema'; import {Primitive} from '@toreda/types'; import {SchemaData} from '../../src/schema/data'; -import {SchemaInit} from '../../src'; +import {SchemaInit} from '../../src/schema/init'; export interface SampleData extends SchemaData { str1: string; @@ -91,3 +91,27 @@ export class SampleSchema extends Schema { }); } } + +export class SampleRulesetSchema extends Schema { + constructor(init: SchemaInit) { + super({ + name: 'SampleSchema', + fields: [ + { + name: 'str1', + types: ['string'] + }, + { + name: 'int1', + types: ['number'] + }, + { + name: 'bool1', + types: ['boolean', 'null'] + } + ], + options: init?.options, + base: init.base + }); + } +} diff --git a/tests/custom/types.spec.ts b/tests/custom/types.spec.ts index a90f3b1..e5b23d6 100644 --- a/tests/custom/types.spec.ts +++ b/tests/custom/types.spec.ts @@ -5,6 +5,8 @@ import {type Primitive} from '@toreda/types'; import {type CustomTypeVerifier} from '../../src/custom/type/verifier'; import {Fate} from '@toreda/fate'; import {type SchemaInit} from '../../src/schema/init'; +import {CustomSchemaVerify} from '../../src/custom/schema/verify'; +import {schemaError} from '../../src'; describe('CustomTypes', () => { let base: Log; @@ -12,6 +14,7 @@ describe('CustomTypes', () => { let instance: CustomTypes; let sampleSchema: SampleSchema; let typeVerifier: CustomTypeVerifier; + let verifyInit: CustomSchemaVerify; beforeAll(() => { typeVerifier = async (): Promise> => { @@ -51,6 +54,12 @@ describe('CustomTypes', () => { } ] }; + + verifyInit = { + data: {}, + base: base, + typeId: 'sampleSchema' + }; }); describe('Constructor', () => { @@ -197,5 +206,26 @@ describe('CustomTypes', () => { expect(result).toEqual(verifier); }); }); + + describe('verifyOnly', () => { + it(`should fail with code when init.type is undefined`, async () => { + verifyInit.type = undefined as any; + const result = await instance.verifyOnly(verifyInit); + + expect(result.ok()).toBe(false); + expect(result.errorCode()).toBe(schemaError('missing_schema_typeId', 'sampleSchema')); + }); + }); + }); + + describe('reset', () => { + it(`should clear all registered types`, () => { + instance.registered.set('aa3', sampleSchema); + instance.registered.set('aa4', sampleSchema); + expect(instance.registered.size).toBe(2); + + instance.reset(); + expect(instance.registered.size).toBe(0); + }); }); }); diff --git a/tests/schema.spec.ts b/tests/schema.spec.ts index 7ce06cb..10c0a3b 100644 --- a/tests/schema.spec.ts +++ b/tests/schema.spec.ts @@ -2,10 +2,10 @@ import {Levels, Log} from '@toreda/log'; import {schemaError} from '../src/schema/error'; import {SchemaField} from '../src/schema/field'; import {valueTypeLabel} from '../src/value/type/label'; -import {SampleData, SampleSchema} from './_data/schema'; +import {type SampleData, SampleSchema} from './_data/schema'; import {SchemaPath} from '../src/schema/path'; -import {SchemaInit} from '../src'; -import {Primitive} from '@toreda/types'; +import {type SchemaInit} from '../src'; +import {type Primitive} from '@toreda/types'; const EMPTY_OBJECT = {}; const EMPTY_STRING = ''; diff --git a/tests/schema/recursive.spec.ts b/tests/schema/recursive.spec.ts index f6509fc..9eb5528 100644 --- a/tests/schema/recursive.spec.ts +++ b/tests/schema/recursive.spec.ts @@ -9,8 +9,8 @@ import { } from '../_data/schema'; import {schemaError} from '../../src/schema/error'; import {SchemaPath} from '../../src/schema/path'; -import {SchemaInit} from '../../src'; -import {Primitive} from '@toreda/types'; +import {type SchemaInit} from '../../src'; +import {type Primitive} from '@toreda/types'; const EMPTY_OBJECT = {}; const EMPTY_STRING = ''; @@ -52,7 +52,10 @@ describe('Schema - Recursive Parsing', () => { name: 'bool1', types: ['boolean', 'null'] } - ] + ], + customTypes: { + ct2: schemaSubB + } }); schemaPath = new SchemaPath(); }); diff --git a/tests/schema/rulesets.spec.ts b/tests/schema/rulesets.spec.ts new file mode 100644 index 0000000..fe43dc5 --- /dev/null +++ b/tests/schema/rulesets.spec.ts @@ -0,0 +1,34 @@ +import {Levels, Log} from '@toreda/log'; +import {Ruleset} from '../../src/ruleset'; +import {SampleRulesetSchema, SampleSchema} from '../_data/schema'; + +describe('Schema Rulesets', () => { + let instance: SampleSchema; + let base: Log; + + beforeAll(() => { + base = new Log({ + globalLevel: Levels.ALL, + groupsStartEnabled: true, + consoleEnabled: true + }); + + instance = new SampleRulesetSchema({ + base: base, + name: 'SampleRulesetSchema', + fields: [] + }); + }); + + describe('Constructor', () => { + it(`should initialize an empty ruleset for each field`, () => { + + }); + }); + + describe('Implementation', () => { + it(``, async () => { + + }); + }); +});