From ed13c2b81af080f5779055ee8371daf8e0d4486e Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Thu, 16 Mar 2023 07:37:26 -0500 Subject: [PATCH 1/7] Add tuple.describe() --- src/array.ts | 10 ++++++ src/locale.ts | 2 +- src/schema.ts | 2 +- src/tuple.ts | 48 ++++++++++++++++++++++++----- src/util/reach.ts | 2 +- test/mixed.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++-- test/tuple.ts | 2 +- 7 files changed, 131 insertions(+), 13 deletions(-) diff --git a/src/array.ts b/src/array.ts index c19f8ab9f..fbd646650 100644 --- a/src/array.ts +++ b/src/array.ts @@ -38,17 +38,23 @@ export function create = AnyObject, T = any>( return new ArraySchema(type as any); } +interface ArraySchemaSpec extends SchemaSpec { + innerType?: ISchema>; +} + export default class ArraySchema< TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = '', > extends Schema { + declare spec: ArraySchemaSpec; readonly innerType?: ISchema, TContext>; constructor(type?: ISchema, TContext>) { super({ type: 'array', + spec: { innerType: type } as any, check(v: any): v is NonNullable { return Array.isArray(v); }, @@ -135,6 +141,10 @@ export default class ArraySchema< const next = super.clone(spec); // @ts-expect-error readonly next.innerType = this.innerType; + next.spec = { + ...next.spec, + innerType: this.innerType + } as ArraySchemaSpec; return next; } diff --git a/src/locale.ts b/src/locale.ts index 6f8a20d7b..9aa57af89 100644 --- a/src/locale.ts +++ b/src/locale.ts @@ -136,7 +136,7 @@ export let array: Required = { export let tuple: Required = { notType: (params) => { const { path, value, spec } = params; - const typeLen = spec.types.length; + const typeLen = spec.innerType.length; if (Array.isArray(value)) { if (value.length < typeLen) return `${path} tuple value has too few items, expected a length of ${typeLen} but got ${ diff --git a/src/schema.ts b/src/schema.ts index a1f46dcb3..363bec36f 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -100,7 +100,7 @@ export interface SchemaRefDescription { } export interface SchemaInnerTypeDescription extends SchemaDescription { - innerType?: SchemaFieldDescription; + innerType?: SchemaFieldDescription | SchemaFieldDescription[]; } export interface SchemaObjectDescription extends SchemaDescription { diff --git a/src/tuple.ts b/src/tuple.ts index 7cc796c4b..c8947a58e 100644 --- a/src/tuple.ts +++ b/src/tuple.ts @@ -11,7 +11,12 @@ import type { UnsetFlag, Maybe, } from './util/types'; -import Schema, { RunTest, SchemaSpec } from './schema'; +import type { ResolveOptions } from './Condition'; +import Schema, { + RunTest, + SchemaInnerTypeDescription, + SchemaSpec, +} from './schema'; import ValidationError from './ValidationError'; import { tuple as tupleLocale } from './locale'; @@ -59,7 +64,7 @@ export default interface TupleSchema< } interface TupleSchemaSpec extends SchemaSpec { - types: T extends any[] + innerType: T extends any[] ? { [K in keyof T]: ISchema; } @@ -73,24 +78,26 @@ export default class TupleSchema< TFlags extends Flags = '', > extends Schema { declare spec: TupleSchemaSpec; + readonly innerType: [ISchema, ...ISchema[]]; constructor(schemas: [ISchema, ...ISchema[]]) { super({ type: 'tuple', - spec: { types: schemas } as any, + spec: { innerType: schemas } as any, check(v: any): v is NonNullable { - const types = (this.spec as TupleSchemaSpec).types; + const types = this.innerType; return Array.isArray(v) && v.length === types.length; }, }); + this.innerType = schemas; + this.withMutation(() => { this.typeError(tupleLocale.notType); }); } protected _cast(inputValue: any, options: InternalOptions) { - const { types } = this.spec; const value = super._cast(inputValue, options); if (!this._typeCheck(value)) { @@ -98,7 +105,7 @@ export default class TupleSchema< } let isChanged = false; - const castArray = types.map((type, idx) => { + const castArray = this.innerType.map((type, idx) => { const castElement = type.cast(value[idx], { ...options, path: `${options.path || ''}[${idx}]`, @@ -116,7 +123,7 @@ export default class TupleSchema< panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ) { - let itemTypes = this.spec.types; + let itemTypes = this.innerType; super._validate(_value, options, panic, (tupleErrors, value) => { // intentionally not respecting recursive @@ -148,6 +155,33 @@ export default class TupleSchema< ); }); } + + clone(spec?: SchemaSpec) { + const next = super.clone(spec); + // @ts-expect-error readonly + next.innerType = this.innerType; + next.spec = { + ...next.spec, + innerType: this.innerType + } as TupleSchemaSpec; + return next; + } + + describe(options?: ResolveOptions) { + let base = super.describe(options) as SchemaInnerTypeDescription; + base.innerType = this.innerType.map((schema, index) => { + let innerOptions = options; + if (innerOptions?.value) { + innerOptions = { + ...innerOptions, + parent: innerOptions.value, + value: innerOptions.value[index], + }; + } + return schema.describe(innerOptions); + }); + return base; + } } create.prototype = TupleSchema.prototype; diff --git a/src/util/reach.ts b/src/util/reach.ts index c182d84f0..5a2ce5064 100644 --- a/src/util/reach.ts +++ b/src/util/reach.ts @@ -39,7 +39,7 @@ export function getIn( } parent = value; value = value && value[idx]; - schema = isTuple ? schema.spec.types[idx] : schema.innerType!; + schema = isTuple ? schema.innerType[idx] : schema.innerType!; } // sometimes the array index part of a path doesn't exist: "nested.arr.child" diff --git a/test/mixed.ts b/test/mixed.ts index 145e4cb5e..be55b5964 100644 --- a/test/mixed.ts +++ b/test/mixed.ts @@ -9,6 +9,7 @@ import { ref, Schema, string, + tuple, ValidationError, } from '../src'; import ObjectSchema from '../src/object'; @@ -965,6 +966,7 @@ describe('Mixed Types ', () => { is: 'entered', then: (s) => s.defined(), }), + baz: tuple([string(), number()]) }); }); @@ -976,7 +978,8 @@ describe('Mixed Types ', () => { default: { foo: undefined, bar: 'a', - lazy: undefined + lazy: undefined, + baz: undefined, }, nullable: false, optional: true, @@ -1035,6 +1038,41 @@ describe('Mixed Types ', () => { }, ], }, + baz: { + type: 'tuple', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + tests: [], + oneOf: [], + notOneOf: [], + innerType: [ + { + type: 'string', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + oneOf: [], + notOneOf: [], + tests: [], + }, + { + type: 'number', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + oneOf: [], + notOneOf: [], + tests: [], + } + ], + }, }, }); }); @@ -1047,7 +1085,8 @@ describe('Mixed Types ', () => { default: { foo: undefined, bar: 'a', - lazy: undefined + lazy: undefined, + baz: undefined, }, nullable: false, optional: true, @@ -1111,6 +1150,41 @@ describe('Mixed Types ', () => { }, ], }, + baz: { + type: 'tuple', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + tests: [], + oneOf: [], + notOneOf: [], + innerType: [ + { + type: 'string', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + oneOf: [], + notOneOf: [], + tests: [], + }, + { + type: 'number', + meta: undefined, + label: undefined, + default: undefined, + nullable: false, + optional: true, + oneOf: [], + notOneOf: [], + tests: [], + } + ], + }, }, }); }); diff --git a/test/tuple.ts b/test/tuple.ts index 0fe576a1f..16bef5412 100644 --- a/test/tuple.ts +++ b/test/tuple.ts @@ -91,7 +91,7 @@ describe('Array types', () => { ); }); - it('should throw useful type error for lenght', async () => { + it('should throw useful type error for length', async () => { let schema = tuple([string().label('name'), number().label('age')]); // expect(() => schema.cast(['James'])).toThrowError( From b0e67ef173dbb04b1fbf0eb163b49d3bde1f3d6a Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Thu, 16 Mar 2023 07:42:31 -0500 Subject: [PATCH 2/7] Undo clone method change --- src/array.ts | 4 ---- src/tuple.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/array.ts b/src/array.ts index fbd646650..00c91b86b 100644 --- a/src/array.ts +++ b/src/array.ts @@ -141,10 +141,6 @@ export default class ArraySchema< const next = super.clone(spec); // @ts-expect-error readonly next.innerType = this.innerType; - next.spec = { - ...next.spec, - innerType: this.innerType - } as ArraySchemaSpec; return next; } diff --git a/src/tuple.ts b/src/tuple.ts index c8947a58e..e2725e599 100644 --- a/src/tuple.ts +++ b/src/tuple.ts @@ -160,10 +160,6 @@ export default class TupleSchema< const next = super.clone(spec); // @ts-expect-error readonly next.innerType = this.innerType; - next.spec = { - ...next.spec, - innerType: this.innerType - } as TupleSchemaSpec; return next; } From 17ee34991aa8c983cab9eca3526b4591ca06cd3a Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Tue, 21 Mar 2023 16:57:39 -0500 Subject: [PATCH 3/7] Use types instead of innerType, use proper describe arg --- src/array.ts | 4 ++-- src/locale.ts | 2 +- src/tuple.ts | 23 +++++++---------------- src/util/reach.ts | 2 +- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/array.ts b/src/array.ts index 00c91b86b..019e5e407 100644 --- a/src/array.ts +++ b/src/array.ts @@ -39,7 +39,7 @@ export function create = AnyObject, T = any>( } interface ArraySchemaSpec extends SchemaSpec { - innerType?: ISchema>; + types?: ISchema>; } export default class ArraySchema< @@ -269,7 +269,7 @@ export default class ArraySchema< value: innerOptions.value[0], }; } - base.innerType = this.innerType.describe(options); + base.innerType = this.innerType.describe(innerOptions); } return base; } diff --git a/src/locale.ts b/src/locale.ts index 9aa57af89..6f8a20d7b 100644 --- a/src/locale.ts +++ b/src/locale.ts @@ -136,7 +136,7 @@ export let array: Required = { export let tuple: Required = { notType: (params) => { const { path, value, spec } = params; - const typeLen = spec.innerType.length; + const typeLen = spec.types.length; if (Array.isArray(value)) { if (value.length < typeLen) return `${path} tuple value has too few items, expected a length of ${typeLen} but got ${ diff --git a/src/tuple.ts b/src/tuple.ts index e2725e599..40fbf0c07 100644 --- a/src/tuple.ts +++ b/src/tuple.ts @@ -64,7 +64,7 @@ export default interface TupleSchema< } interface TupleSchemaSpec extends SchemaSpec { - innerType: T extends any[] + types: T extends any[] ? { [K in keyof T]: ISchema; } @@ -78,26 +78,24 @@ export default class TupleSchema< TFlags extends Flags = '', > extends Schema { declare spec: TupleSchemaSpec; - readonly innerType: [ISchema, ...ISchema[]]; constructor(schemas: [ISchema, ...ISchema[]]) { super({ type: 'tuple', - spec: { innerType: schemas } as any, + spec: { types: schemas } as any, check(v: any): v is NonNullable { - const types = this.innerType; + const types = (this.spec as TupleSchemaSpec).types; return Array.isArray(v) && v.length === types.length; }, }); - this.innerType = schemas; - this.withMutation(() => { this.typeError(tupleLocale.notType); }); } protected _cast(inputValue: any, options: InternalOptions) { + const { types } = this.spec; const value = super._cast(inputValue, options); if (!this._typeCheck(value)) { @@ -105,7 +103,7 @@ export default class TupleSchema< } let isChanged = false; - const castArray = this.innerType.map((type, idx) => { + const castArray = types.map((type, idx) => { const castElement = type.cast(value[idx], { ...options, path: `${options.path || ''}[${idx}]`, @@ -123,7 +121,7 @@ export default class TupleSchema< panic: (err: Error, value: unknown) => void, next: (err: ValidationError[], value: unknown) => void, ) { - let itemTypes = this.innerType; + let itemTypes = this.spec.types; super._validate(_value, options, panic, (tupleErrors, value) => { // intentionally not respecting recursive @@ -156,16 +154,9 @@ export default class TupleSchema< }); } - clone(spec?: SchemaSpec) { - const next = super.clone(spec); - // @ts-expect-error readonly - next.innerType = this.innerType; - return next; - } - describe(options?: ResolveOptions) { let base = super.describe(options) as SchemaInnerTypeDescription; - base.innerType = this.innerType.map((schema, index) => { + base.innerType = this.spec.types.map((schema, index) => { let innerOptions = options; if (innerOptions?.value) { innerOptions = { diff --git a/src/util/reach.ts b/src/util/reach.ts index 5a2ce5064..c182d84f0 100644 --- a/src/util/reach.ts +++ b/src/util/reach.ts @@ -39,7 +39,7 @@ export function getIn( } parent = value; value = value && value[idx]; - schema = isTuple ? schema.innerType[idx] : schema.innerType!; + schema = isTuple ? schema.spec.types[idx] : schema.innerType!; } // sometimes the array index part of a path doesn't exist: "nested.arr.child" From 258daf46d68783e059bfb25aa7028ac977809af6 Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Tue, 21 Mar 2023 16:59:48 -0500 Subject: [PATCH 4/7] Undo array changes --- src/array.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/array.ts b/src/array.ts index 019e5e407..71641239e 100644 --- a/src/array.ts +++ b/src/array.ts @@ -38,23 +38,17 @@ export function create = AnyObject, T = any>( return new ArraySchema(type as any); } -interface ArraySchemaSpec extends SchemaSpec { - types?: ISchema>; -} - export default class ArraySchema< TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = '', > extends Schema { - declare spec: ArraySchemaSpec; readonly innerType?: ISchema, TContext>; constructor(type?: ISchema, TContext>) { super({ type: 'array', - spec: { innerType: type } as any, check(v: any): v is NonNullable { return Array.isArray(v); }, From 21ad49eeb61c84c6b0311c826c04dc5fc90df9b5 Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Tue, 21 Mar 2023 17:02:40 -0500 Subject: [PATCH 5/7] Update array spec --- src/array.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/array.ts b/src/array.ts index 71641239e..97e9bfc0d 100644 --- a/src/array.ts +++ b/src/array.ts @@ -38,17 +38,23 @@ export function create = AnyObject, T = any>( return new ArraySchema(type as any); } +interface ArraySchemaSpec extends SchemaSpec { + types?: ISchema>; +} + export default class ArraySchema< TIn extends any[] | null | undefined, TContext, TDefault = undefined, TFlags extends Flags = '', > extends Schema { + declare spec: ArraySchemaSpec; readonly innerType?: ISchema, TContext>; constructor(type?: ISchema, TContext>) { super({ type: 'array', + spec: { types: type } as ArraySchemaSpec, check(v: any): v is NonNullable { return Array.isArray(v); }, From fc9224dcd9dc5c0244af11ba49a47666114b9a42 Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Tue, 21 Mar 2023 17:12:25 -0500 Subject: [PATCH 6/7] Update types and spec --- src/array.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/array.ts b/src/array.ts index 97e9bfc0d..b03b74fef 100644 --- a/src/array.ts +++ b/src/array.ts @@ -38,8 +38,8 @@ export function create = AnyObject, T = any>( return new ArraySchema(type as any); } -interface ArraySchemaSpec extends SchemaSpec { - types?: ISchema>; +interface ArraySchemaSpec extends SchemaSpec { + types?: ISchema, TContext> } export default class ArraySchema< @@ -48,13 +48,13 @@ export default class ArraySchema< TDefault = undefined, TFlags extends Flags = '', > extends Schema { - declare spec: ArraySchemaSpec; + declare spec: ArraySchemaSpec; readonly innerType?: ISchema, TContext>; constructor(type?: ISchema, TContext>) { super({ type: 'array', - spec: { types: type } as ArraySchemaSpec, + spec: { types: type } as ArraySchemaSpec, check(v: any): v is NonNullable { return Array.isArray(v); }, @@ -189,6 +189,11 @@ export default class ArraySchema< // @ts-expect-error readonly next.innerType = schema; + next.spec = { + ...next.spec, + types: schema as ISchema, TContext> + } + return next as any; } From 55d3a8cb76a07214777d54cdc9dd29beaa599203 Mon Sep 17 00:00:00 2001 From: DaddyWarbucks Date: Sun, 16 Apr 2023 15:56:31 -0500 Subject: [PATCH 7/7] Fix array spec types --- src/array.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/array.ts b/src/array.ts index b03b74fef..f3f5e79b5 100644 --- a/src/array.ts +++ b/src/array.ts @@ -39,7 +39,7 @@ export function create = AnyObject, T = any>( } interface ArraySchemaSpec extends SchemaSpec { - types?: ISchema, TContext> + types?: [ISchema, TContext>] } export default class ArraySchema< @@ -54,7 +54,7 @@ export default class ArraySchema< constructor(type?: ISchema, TContext>) { super({ type: 'array', - spec: { types: type } as ArraySchemaSpec, + spec: { types: type ? [type] : type } as ArraySchemaSpec, check(v: any): v is NonNullable { return Array.isArray(v); }, @@ -191,7 +191,7 @@ export default class ArraySchema< next.spec = { ...next.spec, - types: schema as ISchema, TContext> + types: [schema] as [ISchema, TContext>] } return next as any;