Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ let schema = object({
loose: boolean(),
bar: string().when('loose', {
is: true,
otherwise: (s) => s.strict(),
otherwise: (schema) => schema.strict(),
}),
}),
),
Expand Down Expand Up @@ -634,7 +634,7 @@ await schema.isValid(42); // => false
await schema.isValid(new Date()); // => true
```

#### `mixed.when(keys: string | Array<string>, builder: object | (value, schema)=> Schema): Schema`
#### `mixed.when(keys: string | string[], builder: object | (values: any[], schema) => Schema): Schema`

Adjust the schema based on a sibling or sibling children fields. You can provide an object
literal where the key `is` is value or a matcher function, `then` provides the true schema and/or
Expand All @@ -652,8 +652,8 @@ let schema = object({
count: number()
.when('isBig', {
is: true, // alternatively: (val) => val == true
then: yup.number().min(5),
otherwise: yup.number().min(0),
then: (schema) => schema..min(5),
otherwise: (schema) => schema..min(0),
})
.when('$other', (other, schema) => (other === 4 ? schema.max(6) : schema)),
});
Expand All @@ -669,8 +669,8 @@ let schema = object({
isBig: boolean(),
count: number().when(['isBig', 'isSpecial'], {
is: true, // alternatively: (isBig, isSpecial) => isBig && isSpecial
then: yup.number().min(5),
otherwise: yup.number().min(0),
then: (schema) => schema..min(5),
otherwise: (schema) => schema..min(0),
}),
});

Expand Down
85 changes: 46 additions & 39 deletions src/Condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,80 @@ import isSchema from './util/isSchema';
import Reference from './Reference';
import type { ISchema } from './util/types';

export interface ConditionBuilder<T extends ISchema<any, any>> {
(this: T, value: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, v3: any, schema: T): ISchema<any, any> | void;
(v1: any, v2: any, v3: any, v4: any, schema: T): ISchema<any, any> | void;
}

export type ConditionConfig<T extends ISchema<any>> = {
export type ConditionBuilder<
T extends ISchema<any, any>,
U extends ISchema<any, any> = T,
> = (values: any[], schema: T, options: ResolveOptions) => U;

export type ConditionConfig<
T extends ISchema<any>,
TThen extends ISchema<any, any> = T,
TOtherwise extends ISchema<any, any> = T,
> = {
is: any | ((...values: any[]) => boolean);
then?: (schema: T) => ISchema<any>;
otherwise?: (schema: T) => ISchema<any>;
then?: (schema: T) => TThen;
otherwise?: (schema: T) => TOtherwise;
};

export type ConditionOptions<T extends ISchema<any, any>> =
| ConditionBuilder<T>
| ConditionConfig<T>;

export type ResolveOptions<TContext = any> = {
value?: any;
parent?: any;
context?: TContext;
};

class Condition<T extends ISchema<any, any> = ISchema<any, any>> {
fn: ConditionBuilder<T>;

constructor(public refs: Reference[], options: ConditionOptions<T>) {
this.refs = refs;

if (typeof options === 'function') {
this.fn = options;
return;
}

if (!('is' in options))
throw new TypeError('`is:` is required for `when()` conditions');

if (!options.then && !options.otherwise)
class Condition<
TIn extends ISchema<any, any> = ISchema<any, any>,
TOut extends ISchema<any, any> = TIn,
> {
fn: ConditionBuilder<TIn, TOut>;

static fromOptions<
TIn extends ISchema<any, any>,
TThen extends ISchema<any, any>,
TOtherwise extends ISchema<any, any>,
>(refs: Reference[], config: ConditionConfig<TIn, TThen, TOtherwise>) {
if (!config.then && !config.otherwise)
throw new TypeError(
'either `then:` or `otherwise:` is required for `when()` conditions',
);

let { is, then, otherwise } = options;
let { is, then, otherwise } = config;

let check =
typeof is === 'function'
? is
: (...values: any[]) => values.every((value) => value === is);

this.fn = function (...args: any[]) {
let _opts = args.pop();
let schema = args.pop();
let branch = check(...args) ? then : otherwise;
return new Condition<TIn, TThen | TOtherwise>(
refs,
(values, schema: any) => {
let branch = check(...values) ? then : otherwise;

return branch?.(schema) ?? schema;
};
return branch?.(schema) ?? schema;
},
);
}

resolve(base: T, options: ResolveOptions) {
constructor(public refs: Reference[], builder: ConditionBuilder<TIn, TOut>) {
this.refs = refs;
this.fn = builder;
}

resolve(base: TIn, options: ResolveOptions) {
let values = this.refs.map((ref) =>
// TODO: ? operator here?
ref.getValue(options?.value, options?.parent, options?.context),
);

let schema = this.fn.apply(base, values.concat(base, options) as any);
let schema = this.fn(values, base, options);

if (schema === undefined || schema === base) return base;
if (
schema === undefined ||
// @ts-ignore this can be base
schema === base
) {
return base;
}

if (!isSchema(schema))
throw new TypeError('conditions must return a schema object');
Expand Down
38 changes: 32 additions & 6 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import cloneDeep from 'nanoclone';

import { mixed as locale } from './locale';
import Condition, { ConditionOptions, ResolveOptions } from './Condition';
import Condition, {
ConditionBuilder,
ConditionConfig,
ResolveOptions,
} from './Condition';
import runTests from './util/runTests';
import createValidation, {
TestFunction,
Expand Down Expand Up @@ -670,11 +674,29 @@ export default abstract class BaseSchema<
return next;
}

when(options: ConditionOptions<this>): this;
when(keys: string | string[], options: ConditionOptions<this>): this;
when<U extends ISchema<any> = this>(builder: ConditionBuilder<this, U>): U;
when<U extends ISchema<any> = this>(
keys: string | string[],
builder: ConditionBuilder<this, U>,
): U;
when<
UThen extends ISchema<any> = this,
UOtherwise extends ISchema<any> = this,
>(options: ConditionConfig<this, UThen, UOtherwise>): UThen | UOtherwise;
when<
UThen extends ISchema<any> = this,
UOtherwise extends ISchema<any> = this,
>(
keys: string | string[],
options: ConditionConfig<this, UThen, UOtherwise>,
): UThen | UOtherwise;
when(
keys: string | string[] | ConditionOptions<this>,
options?: ConditionOptions<this>,
keys:
| string
| string[]
| ConditionBuilder<this, any>
| ConditionConfig<this, any, any>,
options?: ConditionBuilder<this, any> | ConditionConfig<this, any, any>,
) {
if (!Array.isArray(keys) && typeof keys !== 'string') {
options = keys;
Expand All @@ -689,7 +711,11 @@ export default abstract class BaseSchema<
if (dep.isSibling) next.deps.push(dep.key);
});

next.conditions.push(new Condition(deps, options!) as Condition);
next.conditions.push(
typeof options === 'function'
? new Condition(deps, options!)
: Condition.fromOptions(deps, options!),
);

return next;
}
Expand Down
4 changes: 2 additions & 2 deletions test/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ describe('Array types', () => {
let value = ['2', '3'];
let expectedPaths = ['[0]', '[1]'];

let itemSchema = string().when([], function (_, context) {
let path = context.path;
let itemSchema = string().when([], function (_, _s, opts: any) {
let path = opts.path;
expect(expectedPaths).toContain(path);
return string().required();
});
Expand Down
4 changes: 2 additions & 2 deletions test/mixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,12 +764,12 @@ describe('Mixed Types ', () => {

it('should handle multiple conditionals', function () {
let called = false;
let inst = mixed().when(['$prop', '$other'], (prop, other) => {
let inst = mixed().when(['$prop', '$other'], ([prop, other], schema) => {
expect(other).toBe(true);
expect(prop).toBe(1);
called = true;

return mixed();
return schema;
});

inst.cast({}, { context: { prop: 1, other: true } });
Expand Down
18 changes: 8 additions & 10 deletions test/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,7 @@ describe('Object types', () => {
let inst = object().shape({
noteDate: number()
.when('stats.isBig', { is: true, then: (s) => s.min(5) })
.when('other', function (v) {
if (v === 4) return this.max(6);
}),
.when('other', ([v], schema) => (v === 4 ? schema.max(6) : schema)),
stats: object({ isBig: bool() }),
other: number()
.min(1)
Expand Down Expand Up @@ -780,23 +778,23 @@ describe('Object types', () => {
it('should allow opt out of topo sort on specific edges', () => {
expect(() => {
object().shape({
orgID: number().when('location', (v, schema) => {
if (v == null) return schema.required();
orgID: number().when('location', ([v], schema) => {
return v == null ? schema.required() : schema;
}),
location: string().when('orgID', (v, schema) => {
if (v == null) return schema.required();
return v == null ? schema.required() : schema;
}),
});
}).toThrowError('Cyclic dependency, node was:"location"');

expect(() => {
object().shape(
{
orgID: number().when('location', function (v) {
if (v == null) return this.required();
orgID: number().when('location', ([v], schema) => {
return v == null ? schema.required() : schema;
}),
location: string().when('orgID', function (v) {
if (v == null) return this.required();
location: string().when('orgID', ([v], schema) => {
return v == null ? schema.required() : schema;
}),
},
[['location', 'orgID']],
Expand Down
33 changes: 32 additions & 1 deletion test/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
/* eslint-disable no-unused-labels */
import { array, number, string, date, ref, mixed } from '../../src';
import { array, number, string, date, ref, mixed, bool } from '../../src';
import { create as lazy } from '../../src/Lazy';
import ObjectSchema, { create as object } from '../../src/object';

Expand Down Expand Up @@ -720,3 +720,34 @@ Object: {
deepPartial.validateSync({})!.address!.line1;
}
}

Conditions: {
// $ExpectType StringSchema<string, AnyObject, undefined, ""> | NumberSchema<number | undefined, AnyObject, undefined, "">
string().when('foo', ([foo], schema) => (foo ? schema.required() : number()));

// $ExpectType StringSchema<string | undefined, AnyObject, undefined, "">
string().when('foo', ([foo], schema) => (foo ? schema.required() : schema));

// $ExpectType StringSchema<string, AnyObject, undefined, ""> | NumberSchema<number | undefined, AnyObject, undefined, "">
string().when('foo', {
is: true,
then: () => number(),
otherwise: (s) => s.required(),
});

const result = object({
foo: bool().defined(),
polyField: mixed<string>().when('foo', {
is: true,
then: () => number(),
otherwise: (s) => s.required(),
}),
}).cast({ foo: true, polyField: '1' });

// $ExpectType { polyField?: string | number | undefined; foo: boolean; }
result;

mixed()
.when('foo', ([foo]) => (foo ? string() : number()))
.min(1);
}
4 changes: 2 additions & 2 deletions test/yup.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ describe('Yup', function () {
inst = object().shape({
num: number().max(4),
nested: object().shape({
arr: array().when('$bar', function (bar) {
arr: array().when('$bar', function ([bar]) {
return bar !== 3
? array().of(number())
: array().of(
object().shape({
foo: number(),
num: number().when('foo', (foo) => {
num: number().when('foo', ([foo]) => {
if (foo === 5) return num;
}),
}),
Expand Down