2023-04-17 08:01:23 +00:00
|
|
|
import JSON5 from 'json5';
|
|
|
|
import type { JsonValue } from 'type-fest';
|
2023-02-22 08:21:09 +00:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
interface ErrorContext<T> {
|
|
|
|
error: z.ZodError;
|
|
|
|
input: T;
|
|
|
|
}
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
interface LooseOpts<T> {
|
|
|
|
onError?: (err: ErrorContext<T>) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Works like `z.array()`, but drops wrong elements instead of invalidating the whole array.
|
|
|
|
*
|
|
|
|
* **Important**: non-array inputs are still invalid.
|
|
|
|
* Use `LooseArray(...).catch([])` to handle it.
|
|
|
|
*
|
|
|
|
* @param Elem Schema for array elements
|
|
|
|
* @param onError Callback for errors
|
|
|
|
* @returns Schema for array
|
|
|
|
*/
|
|
|
|
export function LooseArray<Schema extends z.ZodTypeAny>(
|
|
|
|
Elem: Schema,
|
|
|
|
{ onError }: LooseOpts<unknown[]> = {}
|
|
|
|
): z.ZodEffects<z.ZodArray<z.ZodAny, 'many'>, z.TypeOf<Schema>[], any[]> {
|
|
|
|
if (!onError) {
|
|
|
|
// Avoid error-related computations inside the loop
|
|
|
|
return z.array(z.any()).transform((input) => {
|
|
|
|
const output: z.infer<Schema>[] = [];
|
|
|
|
for (const x of input) {
|
|
|
|
const parsed = Elem.safeParse(x);
|
|
|
|
if (parsed.success) {
|
|
|
|
output.push(parsed.data);
|
2023-02-22 08:21:09 +00:00
|
|
|
}
|
2023-04-21 08:25:48 +00:00
|
|
|
}
|
|
|
|
return output;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return z.array(z.any()).transform((input) => {
|
|
|
|
const output: z.infer<Schema>[] = [];
|
|
|
|
const issues: z.ZodIssue[] = [];
|
|
|
|
|
|
|
|
for (let idx = 0; idx < input.length; idx += 1) {
|
|
|
|
const x = input[idx];
|
|
|
|
const parsed = Elem.safeParse(x);
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
if (parsed.success) {
|
|
|
|
output.push(parsed.data);
|
|
|
|
continue;
|
|
|
|
}
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
for (const issue of parsed.error.issues) {
|
|
|
|
issue.path.unshift(idx);
|
|
|
|
issues.push(issue);
|
|
|
|
}
|
|
|
|
}
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
if (issues.length) {
|
|
|
|
const error = new z.ZodError(issues);
|
|
|
|
onError({ error, input });
|
|
|
|
}
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
return output;
|
|
|
|
});
|
2023-02-22 08:21:09 +00:00
|
|
|
}
|
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
/**
|
|
|
|
* 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 onError Callback for errors
|
|
|
|
* @returns Schema for record
|
|
|
|
*/
|
|
|
|
export function LooseRecord<Schema extends z.ZodTypeAny>(
|
|
|
|
Elem: Schema,
|
|
|
|
{ onError }: LooseOpts<Record<string, unknown>> = {}
|
2023-02-22 08:21:09 +00:00
|
|
|
): z.ZodEffects<
|
2023-04-21 08:25:48 +00:00
|
|
|
z.ZodRecord<z.ZodString, z.ZodAny>,
|
|
|
|
Record<string, z.TypeOf<Schema>>,
|
|
|
|
Record<string, any>
|
2023-02-22 08:21:09 +00:00
|
|
|
> {
|
2023-04-21 08:25:48 +00:00
|
|
|
if (!onError) {
|
|
|
|
// Avoid error-related computations inside the loop
|
|
|
|
return z.record(z.any()).transform((input) => {
|
|
|
|
const output: Record<string, z.infer<Schema>> = {};
|
|
|
|
for (const [key, val] of Object.entries(input)) {
|
|
|
|
const parsed = Elem.safeParse(val);
|
|
|
|
if (parsed.success) {
|
|
|
|
output[key] = parsed.data;
|
2023-02-22 08:21:09 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-21 08:25:48 +00:00
|
|
|
return output;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return z.record(z.any()).transform((input) => {
|
|
|
|
const output: Record<string, z.infer<Schema>> = {};
|
|
|
|
const issues: z.ZodIssue[] = [];
|
|
|
|
|
|
|
|
for (const [key, val] of Object.entries(input)) {
|
|
|
|
const parsed = Elem.safeParse(val);
|
|
|
|
|
|
|
|
if (parsed.success) {
|
|
|
|
output[key] = parsed.data;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const issue of parsed.error.issues) {
|
|
|
|
issue.path.unshift(key);
|
|
|
|
issues.push(issue);
|
|
|
|
}
|
2023-02-22 08:21:09 +00:00
|
|
|
}
|
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
if (issues.length) {
|
|
|
|
const error = new z.ZodError(issues);
|
|
|
|
onError({ error, input });
|
|
|
|
}
|
2023-02-22 14:45:26 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
return output;
|
|
|
|
});
|
2023-02-22 14:45:26 +00:00
|
|
|
}
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
export const Json = z.string().transform((str, ctx): JsonValue => {
|
|
|
|
try {
|
|
|
|
return JSON.parse(str);
|
|
|
|
} catch (e) {
|
|
|
|
ctx.addIssue({ code: 'custom', message: 'Invalid JSON' });
|
|
|
|
return z.NEVER;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
type Json = z.infer<typeof Json>;
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
export const Json5 = z.string().transform((str, ctx): JsonValue => {
|
2023-04-07 14:53:57 +00:00
|
|
|
try {
|
2023-04-17 08:01:23 +00:00
|
|
|
return JSON5.parse(str);
|
|
|
|
} catch (e) {
|
|
|
|
ctx.addIssue({ code: 'custom', message: 'Invalid JSON5' });
|
|
|
|
return z.NEVER;
|
2023-04-07 14:53:57 +00:00
|
|
|
}
|
2023-04-17 08:01:23 +00:00
|
|
|
});
|