Skip to content

Commit

Permalink
feat: add tuple type
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Jan 3, 2022
1 parent ad01b2d commit 853b6cd
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 72 deletions.
7 changes: 4 additions & 3 deletions src/Lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
SchemaFieldDescription,
SchemaLazyDescription,
} from './schema';
import { Flags, ISchema } from './util/types';
import { Flags, ISchema, NestedTestConfig } from './util/types';
import { Schema } from '.';

export type LazyBuilder<
Expand Down Expand Up @@ -82,8 +82,9 @@ class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
return this._resolve(value, options).cast(value, options);
}

asTest(value: any, options?: ValidateOptions<TContext>) {
return this._resolve(value, options).asTest(value, options);
asNestedTest(options: NestedTestConfig<TContext>) {
let value = options.parent[options.index ?? options.key!];
return this._resolve(value, options).asNestedTest(options);
}

validate(value: any, options?: ValidateOptions<TContext>): Promise<T> {
Expand Down
23 changes: 9 additions & 14 deletions src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default class ArraySchema<
options: InternalOptions<TContext> = {},

panic: (err: Error, value: unknown) => void,
callback: (err: ValidationError[], value: unknown) => void,
next: (err: ValidationError[], value: unknown) => void,
) {
// let sync = options.sync;
// let path = options.path;
Expand All @@ -104,25 +104,21 @@ export default class ArraySchema<

super._validate(_value, options, panic, (arrayErrors, value) => {
if (!recursive || !innerType || !this._typeCheck(value)) {
callback(arrayErrors, value);
next(arrayErrors, value);
return;
}

originalValue = originalValue || value;

// #950 Ensure that sparse array empty slots are validated
let tests: RunTest[] = new Array(value.length);
for (let idx = 0; idx < value.length; idx++) {
let item = value[idx];
let path = `${options.path || ''}[${idx}]`;

tests[idx] = innerType!.asTest(item, {
...options,
path,
for (let index = 0; index < value.length; index++) {
tests[index] = innerType!.asNestedTest({
options,
index,
parent: value,
// FIXME
index: idx,
originalValue: originalValue[idx],
parentPath: options.path,
originalParent: options.originalValue ?? _value,
});
}

Expand All @@ -132,8 +128,7 @@ export default class ArraySchema<
tests,
},
panic,
(innerTypeErrors) =>
callback(innerTypeErrors.concat(arrayErrors), value),
(innerTypeErrors) => next(innerTypeErrors.concat(arrayErrors), value),
);
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import NumberSchema, { create as numberCreate } from './number';
import DateSchema, { create as dateCreate } from './date';
import ObjectSchema, { AnyObject, create as objectCreate } from './object';
import ArraySchema, { create as arrayCreate } from './array';
import TupleSchema, { create as tupleCreate } from './tuple';
import { create as refCreate } from './Reference';
import { create as lazyCreate } from './Lazy';
import ValidationError from './ValidationError';
Expand Down Expand Up @@ -62,6 +63,7 @@ export {
arrayCreate as array,
refCreate as ref,
lazyCreate as lazy,
tupleCreate as tuple,
reach,
isSchema,
addMethod,
Expand All @@ -78,6 +80,7 @@ export {
DateSchema,
ObjectSchema,
ArraySchema,
TupleSchema,
};

export type {
Expand Down
29 changes: 11 additions & 18 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,25 +204,23 @@ export default class ObjectSchema<

protected _validate(
_value: any,
opts: InternalOptions<TContext> = {},
options: InternalOptions<TContext> = {},
panic: (err: Error, value: unknown) => void,
next: (err: ValidationError[], value: unknown) => void,
) {
let {
from = [],
originalValue = _value,
recursive = this.spec.recursive,
} = opts;

from = [{ schema: this, value: originalValue }, ...from!];
} = options;

options.from = [{ schema: this, value: originalValue }, ...from];
// this flag is needed for handling `strict` correctly in the context of
// validation vs just casting. e.g strict() on a field is only used when validating
opts.__validating = true;
opts.originalValue = originalValue;
opts.from = from;
options.__validating = true;
options.originalValue = originalValue;

super._validate(_value, opts, panic, (objectErrors, value) => {
super._validate(_value, options, panic, (objectErrors, value) => {
if (!recursive || !isObject(value)) {
next(objectErrors, value);
return;
Expand All @@ -238,18 +236,13 @@ export default class ObjectSchema<
continue;
}

let path =
key.indexOf('.') === -1
? (opts.path ? `${opts.path}.` : '') + key
: `${opts.path || ''}["${key}"]`;

tests.push(
field.asTest(value[key], {
...opts,
path,
from,
field.asNestedTest({
options,
key,
parent: value,
originalValue: originalValue[key],
parentPath: options.path,
originalParent: originalValue,
}),
);
}
Expand Down
49 changes: 43 additions & 6 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ import ValidationError from './ValidationError';
import ReferenceSet from './util/ReferenceSet';
import Reference from './Reference';
import isAbsent from './util/isAbsent';
import type { Flags, ISchema, ResolveFlags, Thunk, _ } from './util/types';
import type {
Flags,
ISchema,
NestedTestConfig,
ResolveFlags,
Thunk,
_,
} from './util/types';
import toArray from './util/toArray';

export type SchemaSpec<TDefault> = {
Expand Down Expand Up @@ -480,11 +487,41 @@ export default abstract class Schema<
}
}

asTest(value: any, options?: ValidateOptions<TContext>): RunTest {
// Nested validations fields are always strict:
// 1. parent isn't strict so the casting will also have cast inner values
// 2. parent is strict in which case the nested values weren't cast either
const testOptions = { ...options, strict: true, value };
asNestedTest({
key,
index,
parent,
parentPath,
originalParent,
options,
}: NestedTestConfig): RunTest {
const k = key ?? index;
if (k == null) {
throw TypeError('Must include `key` or `index` for nested validations');
}

const isIndex = typeof k === 'number';
let value = parent[k];

const testOptions = {
...options,
// Nested validations fields are always strict:
// 1. parent isn't strict so the casting will also have cast inner values
// 2. parent is strict in which case the nested values weren't cast either
strict: true,
parent,
value,
originalValue: originalParent[k],
// FIXME: tests depend on `index` being passed around deeply,
// we should not let the options.key/index bleed through
key: undefined,
// index: undefined,
[isIndex ? 'index' : 'key']: k,
path:
isIndex || k.includes('.')
? `${parentPath || ''}[${value ? k : `"${k}"`}]`
: (parentPath ? `${parentPath}.` : '') + key,
};

return (_: any, panic, next) =>
this.resolve(testOptions)._validate(value, testOptions, panic, next);
Expand Down
65 changes: 43 additions & 22 deletions src/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// @ts-ignore
import isoParse from './util/isodate';
import { date as locale } from './locale';
import isAbsent from './util/isAbsent';
import Ref from './Reference';

import type {
AnyObject,
InternalOptions,
Expand All @@ -19,18 +16,15 @@ import type {
ToggleDefault,
UnsetFlag,
} from './util/types';
import Schema, { SchemaSpec } from './schema';
import Schema, { RunTest, SchemaSpec } from './schema';
import ValidationError from './ValidationError';

type AnyTuple = [unknown, ...unknown[]];

// type SchemaTuple<T extends AnyTuple> = {
// [K in keyof T]: ISchema<T[K]>;
// };

export function create<T extends AnyTuple>(schemas: {
[K in keyof T]: ISchema<T[K]>;
}) {
return new TupleSchema<T>(schemas);
return new TupleSchema<T | undefined>(schemas);
}

export default interface TupleSchema<
Expand Down Expand Up @@ -89,37 +83,64 @@ export default class TupleSchema<
type: 'tuple',
spec: { types: schemas } as any,
check(v: any): v is NonNullable<TType> {
return (
Array.isArray(v) && v.length === (this.spec as any)!.types!.length
);
const types = (this.spec as TupleSchemaSpec<TType>).types;
return Array.isArray(v) && v.length === types.length;
},
});
}

protected _cast(_value: any, _opts: InternalOptions<TContext>) {
protected _cast(inputValue: any, options: InternalOptions<TContext>) {
const { types } = this.spec;
const value = super._cast(inputValue, options);

const value = super._cast(_value, _opts);
if (!this._typeCheck(value)) {
return value;
}

let isChanged = false;
const castArray = types.map((type, idx) => {
const castElement = type.cast(value[idx], {
..._opts,
path: `${_opts.path || ''}[${idx}]`,
...options,
path: `${options.path || ''}[${idx}]`,
});

if (castElement !== value[idx]) {
isChanged = true;
}

if (castElement !== value[idx]) isChanged = true;
return castElement;
});

return isChanged ? castArray : value;
}

protected _validate(
_value: any,
options: InternalOptions<TContext> = {},
panic: (err: Error, value: unknown) => void,
next: (err: ValidationError[], value: unknown) => void,
) {
let itemTypes = this.spec.types;

super._validate(_value, options, panic, (tupleErrors, value) => {
// intentionally not respecting recursive
if (!this._typeCheck(value)) {
next(tupleErrors, value);
return;
}

let tests: RunTest[] = [];
for (let [index, itemSchema] of itemTypes.entries()) {
tests[index] = itemSchema!.asNestedTest({
options,
index,
parent: value,
parentPath: options.path,
originalParent: options.originalValue ?? _value,
});
}

this.runTests({ value, tests }, panic, (innerTypeErrors) =>
next(innerTypeErrors.concat(tupleErrors), value),
);
});
}
}

create.prototype = TupleSchema.prototype;
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type TransformFunction<T extends AnySchema> = (
schema: T,
) => any;

export interface Ancester<TContext> {
schema: ISchema<any, TContext>;
value: any;
}
export interface ValidateOptions<TContext = {}> {
/**
* Only validate the input, skipping type casting and transformation. Default - false
Expand Down Expand Up @@ -39,10 +43,11 @@ export interface InternalOptions<TContext = {}>
__validating?: boolean;
originalValue?: any;
index?: number;
key?: string;
parent?: any;
path?: string;
sync?: boolean;
from?: { schema: ISchema<any, TContext>; value: any }[];
from?: Ancester<TContext>[];
}

export interface MessageParams {
Expand Down
11 changes: 10 additions & 1 deletion src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export type Defined<T> = T extends undefined ? never : T;

export type NotNull<T> = T extends null ? never : T;

export interface NestedTestConfig {
options: InternalOptions<any>;
parent: any;
originalParent: any;
parentPath: string | undefined;
key?: string;
index?: number;
}

export interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
__flags: F;
__context: C;
Expand All @@ -22,7 +31,7 @@ export interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
cast(value: any, options?: CastOptions<C>): T;
validate(value: any, options?: ValidateOptions<C>): Promise<T>;

asTest(value: any, options?: InternalOptions<C>): Test;
asNestedTest(config: NestedTestConfig): Test;

describe(options?: ResolveOptions<C>): SchemaFieldDescription;
resolve(options: ResolveOptions<C>): ISchema<T, C, F>;
Expand Down
Loading

0 comments on commit 853b6cd

Please sign in to comment.