diff --git a/packages/light-type/src/lib/chainable/ChainableArray.ts b/packages/light-type/src/lib/chainable/ChainableArray.ts index a0ece42..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, @@ -10,9 +11,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], @@ -33,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/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/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: [], + }) +} 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/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..dcf166c 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: t.type, parse(input, ctx) { const nextInput = t.parse(input, ctx) 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'