Navigation Menu

Skip to content

Commit

Permalink
feat(schema-utils): Support LooseRecord key validation (#22404)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed May 24, 2023
1 parent 2958a44 commit 71ce657
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 21 deletions.
36 changes: 36 additions & 0 deletions lib/util/schema-utils.spec.ts
Expand Up @@ -49,6 +49,42 @@ describe('util/schema-utils', () => {
expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' });
});

it('supports key schema', () => {
const s = LooseRecord(
z.string().refine((x) => x === 'bar'),
z.string()
);
expect(s.parse({ foo: 'foo', bar: 'bar' })).toEqual({ bar: 'bar' });
});

it('reports key schema errors', () => {
let errorData: unknown = null;
const s = LooseRecord(
z.string().refine((x) => x === 'bar'),
z.string(),
{
onError: (x) => {
errorData = x;
},
}
);

s.parse({ foo: 'foo', bar: 'bar' });

expect(errorData).toMatchObject({
error: {
issues: [
{
code: 'custom',
message: 'Invalid input',
path: ['foo'],
},
],
},
input: { bar: 'bar', foo: 'foo' },
});
});

it('runs callback for wrong elements', () => {
let err: z.ZodError | undefined = undefined;
const Schema = LooseRecord(
Expand Down
109 changes: 88 additions & 21 deletions lib/util/schema-utils.ts
Expand Up @@ -67,54 +67,121 @@ export function LooseArray<Schema extends z.ZodTypeAny>(
});
}

type LooseRecordResult<
KeySchema extends z.ZodTypeAny,
ValueSchema extends z.ZodTypeAny
> = z.ZodEffects<
z.ZodRecord<z.ZodString, z.ZodAny>,
Record<z.TypeOf<KeySchema>, z.TypeOf<ValueSchema>>,
Record<z.TypeOf<KeySchema>, any>
>;

type LooseRecordOpts<
KeySchema extends z.ZodTypeAny,
ValueSchema extends z.ZodTypeAny
> = LooseOpts<Record<z.TypeOf<KeySchema> | z.TypeOf<ValueSchema>, unknown>>;

/**
* Works like `z.record()`, but drops wrong elements instead of invalidating the whole record.
*
* **Important**: non-record inputs other are still invalid.
* Use `LooseRecord(...).catch({})` to handle it.
*
* @param Elem Schema for record values
* @param KeyValue Schema for record keys
* @param ValueValue Schema for record values
* @param onError Callback for errors
* @returns Schema for record
*/
export function LooseRecord<Schema extends z.ZodTypeAny>(
Elem: Schema,
{ onError }: LooseOpts<Record<string, unknown>> = {}
): z.ZodEffects<
z.ZodRecord<z.ZodString, z.ZodAny>,
Record<string, z.TypeOf<Schema>>,
Record<string, any>
> {
export function LooseRecord<ValueSchema extends z.ZodTypeAny>(
Value: ValueSchema
): LooseRecordResult<z.ZodString, ValueSchema>;
export function LooseRecord<
KeySchema extends z.ZodTypeAny,
ValueSchema extends z.ZodTypeAny
>(
Key: KeySchema,
Value: ValueSchema
): LooseRecordResult<KeySchema, ValueSchema>;
export function LooseRecord<ValueSchema extends z.ZodTypeAny>(
Value: ValueSchema,
{ onError }: LooseRecordOpts<z.ZodString, ValueSchema>
): LooseRecordResult<z.ZodString, ValueSchema>;
export function LooseRecord<
KeySchema extends z.ZodTypeAny,
ValueSchema extends z.ZodTypeAny
>(
Key: KeySchema,
Value: ValueSchema,
{ onError }: LooseRecordOpts<KeySchema, ValueSchema>
): LooseRecordResult<KeySchema, ValueSchema>;
export function LooseRecord<
KeySchema extends z.ZodTypeAny,
ValueSchema extends z.ZodTypeAny
>(
arg1: ValueSchema | KeySchema,
arg2?: ValueSchema | LooseOpts<Record<string, unknown>>,
arg3?: LooseRecordOpts<KeySchema, ValueSchema>
): LooseRecordResult<KeySchema, ValueSchema> {
let Key: z.ZodSchema = z.any();
let Value: ValueSchema;
let opts: LooseRecordOpts<KeySchema, ValueSchema> = {};
if (arg2 && arg3) {
Key = arg1 as KeySchema;
Value = arg2 as ValueSchema;
opts = arg3;
} else if (arg2) {
if (arg2 instanceof z.ZodType) {
Key = arg1 as KeySchema;
Value = arg2;
} else {
Value = arg1 as ValueSchema;
opts = arg2;
}
} else {
Value = arg1 as ValueSchema;
}

const { onError } = opts;
if (!onError) {
// Avoid error-related computations inside the loop
return z.record(z.any()).transform((input) => {
const output: Record<string, z.infer<Schema>> = {};
const output: Record<string, z.infer<ValueSchema>> = {};
for (const [key, val] of Object.entries(input)) {
const parsed = Elem.safeParse(val);
if (parsed.success) {
output[key] = parsed.data;
const parsedKey = Key.safeParse(key);
const parsedValue = Value.safeParse(val);
if (parsedKey.success && parsedValue.success) {
output[key] = parsedValue.data;
}
}
return output;
});
}

return z.record(z.any()).transform((input) => {
const output: Record<string, z.infer<Schema>> = {};
const output: Record<string, z.infer<ValueSchema>> = {};
const issues: z.ZodIssue[] = [];

for (const [key, val] of Object.entries(input)) {
const parsed = Elem.safeParse(val);

if (parsed.success) {
output[key] = parsed.data;
const parsedKey = Key.safeParse(key);
if (!parsedKey.success) {
for (const issue of parsedKey.error.issues) {
issue.path.unshift(key);
issues.push(issue);
}
continue;
}

for (const issue of parsed.error.issues) {
issue.path.unshift(key);
issues.push(issue);
const parsedValue = Value.safeParse(val);
if (!parsedValue.success) {
for (const issue of parsedValue.error.issues) {
issue.path.unshift(key);
issues.push(issue);
}
continue;
}

output[key] = parsedValue.data;
continue;
}

if (issues.length) {
Expand Down

0 comments on commit 71ce657

Please sign in to comment.