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
53 changes: 29 additions & 24 deletions src/Lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,27 @@ import type {
SchemaLazyDescription,
} from './schema';
import { Flags } from './util/types';
import { Schema } from '.';
import { InferType, Schema } from '.';

export type LazyBuilder<
T,
TSchema extends ISchema<TContext>,
TContext = AnyObject,
TDefault = any,
TFlags extends Flags = any,
> = (
value: any,
options: ResolveOptions,
) => ISchema<T, TContext, TFlags, TDefault>;
> = (value: any, options: ResolveOptions) => TSchema;

export function create<
T,
TSchema extends ISchema<any, TContext>,
TContext = AnyObject,
TFlags extends Flags = any,
TDefault = any,
>(builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
return new Lazy<T, TContext, TDefault, TFlags>(builder);
>(builder: (value: any, options: ResolveOptions<TContext>) => TSchema) {
return new Lazy<InferType<TSchema>, TContext>(builder);
}

export interface LazySpec {
meta: Record<string, unknown> | undefined;
optional: boolean;
}

class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
implements ISchema<T, TContext, TFlags, TDefault>
class Lazy<T, TContext = AnyObject, TFlags extends Flags = any>
implements ISchema<T, TContext, TFlags, undefined>
{
type = 'lazy' as const;

Expand All @@ -48,37 +42,48 @@ class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
declare readonly __outputType: T;
declare readonly __context: TContext;
declare readonly __flags: TFlags;
declare readonly __default: TDefault;
declare readonly __default: undefined;

spec: LazySpec;

constructor(private builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
this.spec = { meta: undefined };
constructor(private builder: any) {
this.spec = { meta: undefined, optional: false };
}

clone(): Lazy<T, TContext, TDefault, TFlags> {
const next = create(this.builder);
next.spec = { ...this.spec };
clone(spec?: Partial<LazySpec>): Lazy<T, TContext, TFlags> {
const next = new Lazy<T, TContext, TFlags>(this.builder);
next.spec = { ...this.spec, ...spec };
return next;
}

private _resolve = (
value: any,
options: ResolveOptions<TContext> = {},
): Schema<T, TContext, TDefault, TFlags> => {
): Schema<T, TContext, undefined, TFlags> => {
let schema = this.builder(value, options) as Schema<
T,
TContext,
TDefault,
undefined,
TFlags
>;

if (!isSchema(schema))
throw new TypeError('lazy() functions must return a valid schema');

if (this.spec.optional) schema = schema.optional();

return schema.resolve(options);
};

private optionality(optional: boolean) {
const next = this.clone({ optional });
return next;
}

optional(): Lazy<T | undefined, TContext, TFlags> {
return this.optionality(true);
}

resolve(options: ResolveOptions<TContext>) {
return this._resolve(options.value, options);
}
Expand Down Expand Up @@ -129,7 +134,7 @@ class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
}

meta(): Record<string, unknown> | undefined;
meta(obj: Record<string, unknown>): Lazy<T, TContext, TDefault, TFlags>;
meta(obj: Record<string, unknown>): Lazy<T, TContext, TFlags>;
meta(...args: [Record<string, unknown>?]) {
if (args.length === 0) return this.spec.meta;

Expand Down
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,16 @@ import { create as lazyCreate } from './Lazy';
import ValidationError from './ValidationError';
import reach, { getIn } from './util/reach';
import isSchema from './util/isSchema';
import setLocale from './setLocale';
import Schema, { AnySchema } from './schema';
import setLocale, { LocaleObject } from './setLocale';
import Schema, {
AnySchema,
SchemaRefDescription,
SchemaInnerTypeDescription,
SchemaObjectDescription,
SchemaLazyDescription,
SchemaFieldDescription,
SchemaDescription,
} from './schema';
import type { InferType } from './types';

function addMethod<T extends AnySchema>(
Expand Down Expand Up @@ -50,6 +58,13 @@ export type {
AnySchema,
MixedOptions,
TypeGuard,
SchemaRefDescription,
SchemaInnerTypeDescription,
SchemaObjectDescription,
SchemaLazyDescription,
SchemaFieldDescription,
SchemaDescription,
LocaleObject,
};

export {
Expand Down
41 changes: 32 additions & 9 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
import parseJson from './util/parseJson';
import type { Test } from './util/createValidation';
import type ValidationError from './ValidationError';

export type { AnyObject };

type MakeKeysOptional<T> = T extends AnyObject ? _<MakePartial<T>> : T;
Expand All @@ -37,6 +36,31 @@ export type ObjectSchemaSpec = SchemaSpec<any> & {
noUnknown?: boolean;
};

function deepPartial(schema: any) {
if ('fields' in schema) {
const partial: any = {};
for (const [key, fieldSchema] of Object.entries(schema.fields)) {
partial[key] = deepPartial(fieldSchema);
}
return schema.setFields(partial);
}
if (schema.type === 'array') {
const nextArray = schema.optional();
if (nextArray.innerType)
nextArray.innerType = deepPartial(nextArray.innerType);
return nextArray;
}
if (schema.type === 'tuple') {
return schema
.optional()
.clone({ types: schema.spec.types.map(deepPartial) });
}
if ('optional' in schema) {
return schema.optional();
}
return schema;
}

const deepHas = (obj: any, p: string) => {
const path = [...normalizePath(p)];
if (path.length === 1) return path[0] in obj;
Expand Down Expand Up @@ -342,19 +366,18 @@ export default class ObjectSchema<
partial() {
const partial: any = {};
for (const [key, schema] of Object.entries(this.fields)) {
partial[key] = schema instanceof Schema ? schema.optional() : schema;
partial[key] =
'optional' in schema && schema.optional instanceof Function
? schema.optional()
: schema;
}

return this.setFields<Partial<TIn>, TDefault>(partial);
}

deepPartial() {
const partial: any = {};
for (const [key, schema] of Object.entries(this.fields)) {
if (schema instanceof ObjectSchema) partial[key] = schema.deepPartial();
else partial[key] = schema instanceof Schema ? schema.optional() : schema;
}
return this.setFields<PartialDeep<TIn>, TDefault>(partial);
deepPartial(): ObjectSchema<PartialDeep<TIn>, TContext, TDefault, TFlags> {
const next = deepPartial(this);
return next;
}

pick<TKey extends keyof TIn>(keys: readonly TKey[]) {
Expand Down
2 changes: 2 additions & 0 deletions src/setLocale.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import locale, { LocaleObject } from './locale';

export type { LocaleObject };

export default function setLocale(custom: LocaleObject) {
Object.keys(custom).forEach((type) => {
// @ts-ignore
Expand Down
20 changes: 14 additions & 6 deletions test/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,14 +686,15 @@ describe('Object types', () => {
await expect(inst.partial().isValid({ name: '' })).resolves.toEqual(false);
});

xit('deepPartial() should work', async () => {
it('deepPartial() should work', async () => {
let inst = object({
age: number().required(),
name: string().required(),
contacts: array(
object({
name: string().required(),
age: number().required(),
lazy: lazy(() => number().required()),
}),
).defined(),
});
Expand All @@ -703,17 +704,24 @@ describe('Object types', () => {
inst.isValid({ age: 2, name: 'fs', contacts: [{}] }),
).resolves.toEqual(false);

await expect(inst.deepPartial().isValid({})).resolves.toEqual(true);
const instPartial = inst.deepPartial();

await expect(
inst.deepPartial().validate({ contacts: [{}] }),
).resolves.toEqual(true);
inst.validate({ age: 1, name: 'f', contacts: [{ name: 'f', age: 1 }] }),
).rejects.toThrowError('contacts[0].lazy is a required field');

await expect(instPartial.isValid({})).resolves.toEqual(true);

await expect(instPartial.isValid({ contacts: [{}] })).resolves.toEqual(
true,
);

await expect(
inst.deepPartial().isValid({ contacts: [{ age: null }] }),
instPartial.isValid({ contacts: [{ age: null }] }),
).resolves.toEqual(false);

await expect(
inst.deepPartial().isValid({ contacts: [{ age: null }] }),
instPartial.isValid({ contacts: [{ lazy: null }] }),
).resolves.toEqual(false);
});

Expand Down
19 changes: 17 additions & 2 deletions test/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,15 @@ bool: {
Lazy: {
const l = lazy(() => string().default('asfasf'));

// $ExpectType string
l.cast(null);

const l2 = lazy((v) =>
v ? string().default('asfasf') : number().required(),
);

// $ExpectType string | number
l2.cast(null);
}

Array: {
Expand Down Expand Up @@ -885,6 +893,7 @@ Object: {
const schema = object({
// age: number(),
name: string().required(),
lazy: lazy(() => number().defined()),
address: object()
.shape({
line1: string().required(),
Expand All @@ -896,18 +905,24 @@ Object: {
const partial = schema.partial();

// $ExpectType string | undefined
partial.validateSync({ age: '1' })!.name;
partial.validateSync({})!.name;

// $ExpectType string
partial.validateSync({})!.address!.line1;

// $ExpectType number | undefined
partial.validateSync({})!.lazy;

const deepPartial = schema.deepPartial();

// $ExpectType string | undefined
deepPartial.validateSync({ age: '1' })!.name;
deepPartial.validateSync({})!.name;

// $ExpectType string | undefined
deepPartial.validateSync({})!.address!.line1;

// $ExpectType number | undefined
deepPartial.validateSync({})!.lazy;
}
}

Expand Down