Skip to content

Commit

Permalink
feat: add cast nullability migration path. (#1749)
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense authored Aug 19, 2022
1 parent e4ae6ed commit 2bb099e
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 11 deletions.
13 changes: 11 additions & 2 deletions src/Lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import type { ResolveOptions } from './Condition';

import type {
CastOptionalityOptions,
CastOptions,
SchemaFieldDescription,
SchemaLazyDescription,
Expand Down Expand Up @@ -88,8 +89,16 @@ class Lazy<T, TContext = AnyObject, TFlags extends Flags = any>
return this._resolve(options.value, options);
}

cast(value: any, options?: CastOptions<TContext>): T {
return this._resolve(value, options).cast(value, options);
cast(value: any, options?: CastOptions<TContext>): T;
cast(
value: any,
options?: CastOptionalityOptions<TContext>,
): T | null | undefined;
cast(
value: any,
options?: CastOptions<TContext> | CastOptionalityOptions<TContext>,
): any {
return this._resolve(value, options).cast(value, options as any);
}

asNestedTest(options: NestedTestConfig) {
Expand Down
38 changes: 29 additions & 9 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ export interface CastOptions<C = {}> {
path?: string;
}

export interface CastOptionalityOptions<C = {}>
extends Omit<CastOptions<C>, 'assert'> {
/**
* Whether or not to throw TypeErrors if casting fails to produce a valid type.
* defaults to `true`. The `'ignore-optionality'` options is provided as a migration
* path from pre-v1 where `schema.nullable().required()` was allowed. When provided
* cast will only throw for values that are the wrong type *not* including `null` and `undefined`
*/
assert: 'ignore-optionality';
}

export type RunTest = (
opts: TestOptions,
panic: PanicCallback,
Expand Down Expand Up @@ -328,19 +339,33 @@ export default abstract class Schema<
/**
* Run the configured transform pipeline over an input value.
*/
cast(value: any, options: CastOptions<TContext> = {}): this['__outputType'] {
cast(value: any, options?: CastOptions<TContext>): this['__outputType'];
cast(
value: any,
options: CastOptionalityOptions<TContext>,
): this['__outputType'] | null | undefined;
cast(
value: any,
options: CastOptions<TContext> | CastOptionalityOptions<TContext> = {},
): this['__outputType'] {
let resolvedSchema = this.resolve({
value,
...options,
// parent: options.parent,
// context: options.context,
});
let allowOptionality = options.assert === 'ignore-optionality';

let result = resolvedSchema._cast(value, options);
let result = resolvedSchema._cast(value, options as any);

if (options.assert !== false && !resolvedSchema.isType(result)) {
if (allowOptionality && isAbsent(result)) {
return result as any;
}

let formattedValue = printValue(value);
let formattedResult = printValue(result);

throw new TypeError(
`The value of ${
options.path || 'field'
Expand Down Expand Up @@ -523,8 +548,7 @@ export default abstract class Schema<
validate(
value: any,
options?: ValidateOptions<TContext>,
): Promise<this['__outputType']>;
validate(value: any, options?: ValidateOptions<TContext>): any {
): Promise<this['__outputType']> {
let schema = this.resolve({ ...options, value });

return new Promise((resolve, reject) =>
Expand All @@ -537,16 +561,12 @@ export default abstract class Schema<
},
(errors, validated) => {
if (errors.length) reject(new ValidationError(errors!, validated));
else resolve(validated);
else resolve(validated as this['__outputType']);
},
),
);
}

validateSync(
value: any,
options?: ValidateOptions<TContext>,
): this['__outputType'];
validateSync(
value: any,
options?: ValidateOptions<TContext>,
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ResolveOptions } from './Condition';
import type {
AnySchema,
CastOptionalityOptions,
CastOptions,
SchemaFieldDescription,
SchemaSpec,
Expand All @@ -18,6 +19,8 @@ export interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
__default: D;

cast(value: any, options?: CastOptions<C>): T;
cast(value: any, options: CastOptionalityOptions<C>): T | null | undefined;

validate(value: any, options?: ValidateOptions<C>): Promise<T>;

asNestedTest(config: NestedTestConfig): Test;
Expand Down
10 changes: 10 additions & 0 deletions test/mixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ describe('Mixed Types ', () => {
);
});

it('should allow missing values with the "ignore-optionality" option', () => {
expect(
string().required().cast(null, { assert: 'ignore-optionality' }),
).toBe(null);

expect(
string().required().cast(undefined, { assert: 'ignore-optionality' }),
).toBe(undefined);
});

it('should warn about null types', async () => {
await expect(string().strict().validate(null)).rejects.toThrowError(
/this cannot be null/,
Expand Down
6 changes: 6 additions & 0 deletions test/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ Mixed: {
type: 'string',
check: (value): value is string => typeof value === 'string',
});

// $ExpectType string
mixed<string>().defined().cast('', { assert: true });

// $ExpectType string | null | undefined
mixed<string>().defined().cast('', { assert: 'ignore-optionality' });
}

Strings: {
Expand Down

0 comments on commit 2bb099e

Please sign in to comment.