From d55475066565b6c252a82e80320d687967148af2 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 19 Feb 2023 21:43:12 +0000 Subject: [PATCH 1/4] Add data type to all TypeInner instances --- .../src/lib/chainable/ChainableArray.ts | 2 ++ .../src/lib/chainable/ChainableObject.ts | 1 + packages/light-type/src/lib/lt.ts | 15 +++++++++++++-- packages/light-type/src/lib/types/TypeInner.ts | 18 ++++++++++++++++++ packages/light-type/src/lib/types/pipes.ts | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/light-type/src/lib/chainable/ChainableArray.ts b/packages/light-type/src/lib/chainable/ChainableArray.ts index a0ece42..04e9465 100644 --- a/packages/light-type/src/lib/chainable/ChainableArray.ts +++ b/packages/light-type/src/lib/chainable/ChainableArray.ts @@ -10,9 +10,11 @@ export class ChainableArray< constructor(protected readonly elementType: TElement) { super({ + type: 'array', parse(input, ctx) { if (Array.isArray(input)) { const items = new Array(input.length) + for (let i = 0; i < input.length; i++) { items[i] = elementType.t.parse( input[i], diff --git a/packages/light-type/src/lib/chainable/ChainableObject.ts b/packages/light-type/src/lib/chainable/ChainableObject.ts index 54e0d57..d25fdef 100644 --- a/packages/light-type/src/lib/chainable/ChainableObject.ts +++ b/packages/light-type/src/lib/chainable/ChainableObject.ts @@ -33,6 +33,7 @@ export class ChainableObject< const keys = Object.keys(lightObject) as TKey[] super({ + type: 'object', parse(input, ctx) { if (typeof input === 'object' && input !== null) { const obj = input as TInput diff --git a/packages/light-type/src/lib/lt.ts b/packages/light-type/src/lib/lt.ts index 988ab02..2d30099 100644 --- a/packages/light-type/src/lib/lt.ts +++ b/packages/light-type/src/lib/lt.ts @@ -8,7 +8,6 @@ import { import { AnyLightObject } from './types/LightObject' import { Primitive, LiteralBase, AnyKey } from './types/utils' import { ChainableObject } from './chainable/ChainableObject' -import { LightTypeError } from './errors/LightTypeError' import { ChainableArray } from './chainable/ChainableArray' import { AnyTupleInput, AnyUnionInput } from './types/creators' @@ -56,6 +55,7 @@ export function array( export function any() { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new ChainableType({ + type: 'any', parse(input) { return input }, @@ -71,6 +71,7 @@ export function any() { */ export function unknown() { return new ChainableType({ + type: 'unknown', parse(input) { return input }, @@ -86,6 +87,7 @@ export function unknown() { */ export function boolean() { return new ChainableType({ + type: 'boolean', parse(input, ctx) { if (typeof input === 'boolean') { return input @@ -111,6 +113,7 @@ export function boolean() { */ export function number() { return new ChainableType({ + type: 'number', parse(input, ctx) { if (typeof input === 'number') { return input @@ -136,6 +139,7 @@ export function number() { */ export function string() { return new ChainableType({ + type: 'string', parse(input, ctx) { if (typeof input === 'string') { return input @@ -161,6 +165,7 @@ export function string() { */ export function date() { return new ChainableType({ + type: 'date', parse(input, ctx) { if (input instanceof Date && !isNaN(input.valueOf())) { return input @@ -198,6 +203,7 @@ export function literal( const list = Array.from(values).join(', ') return new ChainableType, TLiteral>({ + type: 'literal', parse(input: unknown, ctx) { if (values.has(input as TLiteral)) { return input as TLiteral @@ -235,6 +241,7 @@ export function record< type TOutput = Record return new ChainableType({ + type: 'record', parse(input, ctx) { if (typeof input === 'object' && input !== null) { const maybeTInput = input as TInput @@ -288,6 +295,7 @@ export function map< type TOutput = Map return new ChainableType({ + type: 'map', parse(_input, ctx) { const input = _input instanceof Map @@ -343,6 +351,7 @@ export function tuple(tuple: T) { } return new ChainableType({ + type: 'tuple', parse(input, ctx) { if (Array.isArray(input)) { if (input.length !== tuple.length) { @@ -393,6 +402,7 @@ export function union(types: T) { }[number] return new ChainableType({ + type: 'union', parse(input, ctx) { for (const type of types) { const specialContext = new Context() @@ -428,6 +438,7 @@ export function union(types: T) { */ export function set(valueType: LightType) { return new ChainableType, Set>({ + type: 'set', parse(_input, ctx) { let input = [] as TInput[] if (_input instanceof Set) { @@ -466,4 +477,4 @@ export function set(valueType: LightType) { * // `unknown -> Date` * ``` */ -export const pipe = createPipeFunction(unknown()) +export const pipe = createPipeFunction(unknown().t) diff --git a/packages/light-type/src/lib/types/TypeInner.ts b/packages/light-type/src/lib/types/TypeInner.ts index 3a94e7b..210f1d2 100644 --- a/packages/light-type/src/lib/types/TypeInner.ts +++ b/packages/light-type/src/lib/types/TypeInner.ts @@ -1,5 +1,23 @@ import { InternalContext } from '../context/Context' +export type DataType = + | 'object' + | 'array' + | 'any' + | 'unknown' + | 'boolean' + | 'number' + | 'string' + | 'date' + | 'literal' + | 'record' + | 'map' + | 'tuple' + | 'union' + | 'set' + | 'none' + export interface TypeInner { + type: DataType parse(input: unknown, context: InternalContext): TOutput } diff --git a/packages/light-type/src/lib/types/pipes.ts b/packages/light-type/src/lib/types/pipes.ts index b939f72..26a6c54 100644 --- a/packages/light-type/src/lib/types/pipes.ts +++ b/packages/light-type/src/lib/types/pipes.ts @@ -62,6 +62,7 @@ export function createPipeFunction( function pipe(...funcs: PipeElem[]) { return new ChainableType({ + type: 'none', parse(input, ctx) { const nextInput = t.parse(input, ctx) From e6b9a89ef4a43ccedb8198d9f5438da854b87433 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 19 Feb 2023 21:50:02 +0000 Subject: [PATCH 2/4] Add array validators --- .../src/lib/chainable/ChainableArray.ts | 5 +++ .../light-type/src/lib/validators/arrays.ts | 43 +++++++++++++++++++ .../light-type/src/lib/validators/index.ts | 1 + 3 files changed, 49 insertions(+) create mode 100644 packages/light-type/src/lib/validators/arrays.ts diff --git a/packages/light-type/src/lib/chainable/ChainableArray.ts b/packages/light-type/src/lib/chainable/ChainableArray.ts index 04e9465..6c8adb5 100644 --- a/packages/light-type/src/lib/chainable/ChainableArray.ts +++ b/packages/light-type/src/lib/chainable/ChainableArray.ts @@ -1,5 +1,6 @@ import { AnyLightType, InferInput, InferOutput } from '../types/LightType' import { ChainableType } from './ChainableType' +import { arrays } from '../validators' export class ChainableArray< TElement extends AnyLightType, @@ -35,4 +36,8 @@ export class ChainableArray< }, }) } + + min = (min: number) => this.pipe(arrays.min(min)) + max = (max: number) => this.pipe(arrays.max(max)) + length = (length: number) => this.pipe(arrays.length(length)) } diff --git a/packages/light-type/src/lib/validators/arrays.ts b/packages/light-type/src/lib/validators/arrays.ts new file mode 100644 index 0000000..d2d1bee --- /dev/null +++ b/packages/light-type/src/lib/validators/arrays.ts @@ -0,0 +1,43 @@ +import { Assertion } from './assert' + +export function min(elements: number): Assertion { + return (input, ctx) => { + if (input.length < elements) { + ctx.addIssue({ + type: 'min', + message: 'Min Length is ' + elements, + value: elements, + }) + } + + return input + } +} + +export function max(elements: number): Assertion { + return (input, ctx) => { + if (input.length > elements) { + ctx.addIssue({ + type: 'max', + message: 'Max Length is ' + elements, + value: elements, + }) + } + + return input + } +} + +export function length(elements: number): Assertion { + return (input, ctx) => { + if (input.length !== elements) { + ctx.addIssue({ + type: 'length', + message: 'Expected Length is ' + elements, + value: input, + }) + } + + return input + } +} diff --git a/packages/light-type/src/lib/validators/index.ts b/packages/light-type/src/lib/validators/index.ts index fd7d845..1a97e0c 100644 --- a/packages/light-type/src/lib/validators/index.ts +++ b/packages/light-type/src/lib/validators/index.ts @@ -1,3 +1,4 @@ export * as strings from './strings' export * as numbers from './numbers' +export * as arrays from './arrays' export * from './assert' From 0451667914a68784c13100fac740bcd616b1a061 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 19 Feb 2023 21:56:16 +0000 Subject: [PATCH 3/4] Fix omissions in adding datatypes --- packages/light-type/src/lib/chainable/ChainableType.ts | 4 ++++ packages/light-type/src/lib/types/pipes.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/light-type/src/lib/chainable/ChainableType.ts b/packages/light-type/src/lib/chainable/ChainableType.ts index a9b9e05..83b6451 100644 --- a/packages/light-type/src/lib/chainable/ChainableType.ts +++ b/packages/light-type/src/lib/chainable/ChainableType.ts @@ -65,6 +65,7 @@ export class ChainableType const t = this.t return new ChainableType({ + type: t.type, parse(input, ctx) { if (input === undefined) { return undefined @@ -86,6 +87,7 @@ export class ChainableType const t = this.t return new ChainableType({ + type: t.type, parse(input, ctx) { if (input === null) { return null @@ -117,6 +119,7 @@ export class ChainableType const t = this.t return new ChainableType({ + type: t.type, parse(input, ctx) { if (input === undefined) { return defaultValue @@ -141,6 +144,7 @@ export class ChainableType const t = this.t return new ChainableType({ + type: t.type, parse(input, ctx) { if (input === null) { return defaultValue diff --git a/packages/light-type/src/lib/types/pipes.ts b/packages/light-type/src/lib/types/pipes.ts index 26a6c54..dcf166c 100644 --- a/packages/light-type/src/lib/types/pipes.ts +++ b/packages/light-type/src/lib/types/pipes.ts @@ -62,7 +62,7 @@ export function createPipeFunction( function pipe(...funcs: PipeElem[]) { return new ChainableType({ - type: 'none', + type: t.type, parse(input, ctx) { const nextInput = t.parse(input, ctx) From 06a93793e9a1fbfb52d840ccf388e2852d9d7f7c Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 19 Feb 2023 22:45:38 +0000 Subject: [PATCH 4/4] WIP prototype Proxy wrapper to overcome problems with classes --- .../src/lib/chainable/ChainableProxy.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/light-type/src/lib/chainable/ChainableProxy.ts diff --git a/packages/light-type/src/lib/chainable/ChainableProxy.ts b/packages/light-type/src/lib/chainable/ChainableProxy.ts new file mode 100644 index 0000000..7956f4b --- /dev/null +++ b/packages/light-type/src/lib/chainable/ChainableProxy.ts @@ -0,0 +1,93 @@ +import { DataType, TypeInner as OldTypeInner } from '../types/TypeInner' +import { Simplify } from '../types/utils' +import { Assertion, numbers, strings } from '../validators' + +type TypeInner = OldTypeInner & { + pipeline: unknown[] +} + +type Any = TypeInner +type ProducerOf = TypeInner + +type NumberExtensions = T & { + min(min: number): AddExtensions + max(max: number): AddExtensions +} +type StringExtensions = T & { + min(min: number): AddExtensions + max(max: number): AddExtensions + length(length: number): AddExtensions + regex(regex: RegExp): AddExtensions +} + +type CommonExtensions = T extends TypeInner< + infer TInput, + infer TOutput +> + ? { + optional(): AddExtensions< + TypeInner + > + default( + value: TOutput + ): AddExtensions> + nullable(): AddExtensions> + defaultNull( + value: TOutput + ): AddExtensions> + } + : never + +type AddExtensions = CommonExtensions & + (T extends ProducerOf + ? NumberExtensions + : T extends ProducerOf + ? StringExtensions + : T) + +function Next>(t: T) { + return new Proxy(t as AddExtensions, { + get(target, p, receiver) { + if (target.type === 'number') { + const t = target as ProducerOf + + switch (p) { + case 'min': { + return function (min: number) { + t.pipeline.push(numbers.min(min)) + return receiver + } + } + case 'max': { + return function (min: number) { + t.pipeline.push(numbers.min(min)) + return receiver + } + } + } + } + + return Reflect.get(target, p, receiver) + }, + }) +} + +function number() { + return Next>({ + type: 'number', + parse(input: number, context) { + return input as number + }, + pipeline: [], + }) +} + +function string() { + return Next>({ + type: 'number', + parse(input: string, context) { + return input as string + }, + pipeline: [], + }) +}