Skip to content

Instantly share code, notes, and snippets.

@webNeat
Last active June 18, 2023 04:34
Show Gist options
  • Save webNeat/ce0895709fdba05c085e1a67208d653b to your computer and use it in GitHub Desktop.
Save webNeat/ce0895709fdba05c085e1a67208d653b to your computer and use it in GitHub Desktop.
abstract class ZodType {
abstract _parse(input: ParseInput): ParseReturnType<Output>;
parse(data: unknown, params?: Partial<ParseParams>): Output {
const result = this.safeParse(data, params);
if (result.success) return result.data;
throw result.error;
}
safeParse(data: unknown, params?: Partial<ParseParams>): SafeParseReturnType<Input, Output> {
const ctx: ParseContext = {
common: {
issues: [],
async: params?.async ?? false,
contextualErrorMap: params?.errorMap,
},
path: params?.path || [],
schemaErrorMap: this._def.errorMap,
parent: null,
data,
parsedType: getParsedType(data),
};
const result = this._parseSync({ data, path: ctx.path, parent: ctx });
return handleResult(ctx, result);
}
_parseSync(input: ParseInput): SyncParseReturnType<Output> {
const result = this._parse(input);
if (isAsync(result)) {
throw new Error("Synchronous parse encountered promise.");
}
return result;
}
_getType(input: ParseInput): string {
return getParsedType(input.data);
}
_getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext {
return (
ctx || {
common: input.parent.common,
data: input.data,
parsedType: getParsedType(input.data),
schemaErrorMap: this._def.errorMap,
path: input.path,
parent: input.parent,
}
);
}
}
class ZodString extends ZodType<string, ZodStringDef> {
_parse(input: ParseInput): ParseReturnType<string> {
if (this._def.coerce) {
input.data = String(input.data);
}
const parsedType = this._getType(input);
if (parsedType !== ZodParsedType.string) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(
ctx,
{
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.string,
received: ctx.parsedType,
}
//
);
return INVALID;
}
const status = new ParseStatus();
let ctx: undefined | ParseContext = undefined;
for (const check of this._def.checks) {
if (check.kind === "min") {
if (input.data.length < check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "max") {
if (input.data.length > check.value) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "string",
inclusive: true,
exact: false,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "length") {
const tooBig = input.data.length > check.value;
const tooSmall = input.data.length < check.value;
if (tooBig || tooSmall) {
ctx = this._getOrReturnCtx(input, ctx);
if (tooBig) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message,
});
} else if (tooSmall) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "string",
inclusive: true,
exact: true,
message: check.message,
});
}
status.dirty();
}
} else if (check.kind === "email") {
if (!emailRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "email",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "emoji") {
if (!emojiRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "emoji",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "uuid") {
if (!uuidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "uuid",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "cuid") {
if (!cuidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "cuid",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "cuid2") {
if (!cuid2Regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "cuid2",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "ulid") {
if (!ulidRegex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ulid",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "url") {
try {
new URL(input.data);
} catch {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "url",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "regex") {
check.regex.lastIndex = 0;
const testResult = check.regex.test(input.data);
if (!testResult) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "regex",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} else if (check.kind === "includes") {
if (!(input.data as string).includes(check.value, check.position)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { includes: check.value, position: check.position },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "toLowerCase") {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
} else if (check.kind === "startsWith") {
if (!(input.data as string).startsWith(check.value)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { startsWith: check.value },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "endsWith") {
if (!(input.data as string).endsWith(check.value)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: { endsWith: check.value },
message: check.message,
});
status.dirty();
}
} else if (check.kind === "datetime") {
const regex = datetimeRegex(check);
if (!regex.test(input.data)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_string,
validation: "datetime",
message: check.message,
});
status.dirty();
}
} else if (check.kind === "ip") {
if (!isValidIP(input.data, check.version)) {
ctx = this._getOrReturnCtx(input, ctx);
addIssueToContext(ctx, {
validation: "ip",
code: ZodIssueCode.invalid_string,
message: check.message,
});
status.dirty();
}
} else {
util.assertNever(check);
}
}
return { status: status.value, value: input.data };
}
}
class ParseStatus {
value: "aborted" | "dirty" | "valid" = "valid";
dirty() {
if (this.value === "valid") this.value = "dirty";
}
abort() {
if (this.value !== "aborted") this.value = "aborted";
}
static mergeArray(
status: ParseStatus,
results: SyncParseReturnType<any>[]
): SyncParseReturnType {
const arrayValue: any[] = [];
for (const s of results) {
if (s.status === "aborted") return INVALID;
if (s.status === "dirty") status.dirty();
arrayValue.push(s.value);
}
return { status: status.value, value: arrayValue };
}
static async mergeObjectAsync(
status: ParseStatus,
pairs: { key: ParseReturnType<any>; value: ParseReturnType<any> }[]
): Promise<SyncParseReturnType<any>> {
const syncPairs: ObjectPair[] = [];
for (const pair of pairs) {
syncPairs.push({
key: await pair.key,
value: await pair.value,
});
}
return ParseStatus.mergeObjectSync(status, syncPairs);
}
static mergeObjectSync(
status: ParseStatus,
pairs: {
key: SyncParseReturnType<any>;
value: SyncParseReturnType<any>;
alwaysSet?: boolean;
}[]
): SyncParseReturnType {
const finalObject: any = {};
for (const pair of pairs) {
const { key, value } = pair;
if (key.status === "aborted") return INVALID;
if (value.status === "aborted") return INVALID;
if (key.status === "dirty") status.dirty();
if (value.status === "dirty") status.dirty();
if (
key.value !== "__proto__" &&
(typeof value.value !== "undefined" || pair.alwaysSet)
) {
finalObject[key.value] = value.value;
}
}
return { status: status.value, value: finalObject };
}
}
const getParsedType = (data: any): ZodParsedType => {
const t = typeof data;
switch (t) {
case "undefined":
return ZodParsedType.undefined;
case "string":
return ZodParsedType.string;
case "number":
return isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
case "boolean":
return ZodParsedType.boolean;
case "function":
return ZodParsedType.function;
case "bigint":
return ZodParsedType.bigint;
case "symbol":
return ZodParsedType.symbol;
case "object":
if (Array.isArray(data)) {
return ZodParsedType.array;
}
if (data === null) {
return ZodParsedType.null;
}
if (
data.then &&
typeof data.then === "function" &&
data.catch &&
typeof data.catch === "function"
) {
return ZodParsedType.promise;
}
if (typeof Map !== "undefined" && data instanceof Map) {
return ZodParsedType.map;
}
if (typeof Set !== "undefined" && data instanceof Set) {
return ZodParsedType.set;
}
if (typeof Date !== "undefined" && data instanceof Date) {
return ZodParsedType.date;
}
return ZodParsedType.object;
default:
return ZodParsedType.unknown;
}
}
const isAsync = <T>(x: ParseReturnType<T>): x is AsyncParseReturnType<T> => typeof Promise !== "undefined" && x instanceof Promise;
const handleResult = <Input, Output>(
ctx: ParseContext,
result: SyncParseReturnType<Output>
):
| { success: true; data: Output }
| { success: false; error: ZodError<Input> } => {
if (isValid(result)) {
return { success: true, data: result.value };
} else {
if (!ctx.common.issues.length) {
throw new Error("Validation failed but no issues detected.");
}
return {
success: false,
get error() {
if ((this as any)._error) return (this as any)._error as Error;
const error = new ZodError(ctx.common.issues);
(this as any)._error = error;
return (this as any)._error;
},
};
}
};
export function addIssueToContext(ctx: ParseContext, issueData: IssueData): void {
const issue = makeIssue({
issueData: issueData,
data: ctx.data,
path: ctx.path,
errorMaps: [
ctx.common.contextualErrorMap, // contextual error map is first priority
ctx.schemaErrorMap, // then schema-bound map if available
getErrorMap(), // then global override map
defaultErrorMap, // then global default map
].filter((x) => !!x) as ZodErrorMap[],
});
ctx.common.issues.push(issue);
}
const makeIssue = (params: {
data: any;
path: (string | number)[];
errorMaps: ZodErrorMap[];
issueData: IssueData;
}): ZodIssue => {
const { data, path, errorMaps, issueData } = params;
const fullPath = [...path, ...(issueData.path || [])];
const fullIssue = {
...issueData,
path: fullPath,
};
let errorMessage = "";
const maps = errorMaps
.filter((m) => !!m)
.slice()
.reverse() as ZodErrorMap[];
for (const map of maps) {
errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
}
return {
...issueData,
path: fullPath,
message: issueData.message || errorMessage,
};
};
function getErrorMap() {
return overrideErrorMap;
}
function isValidIP(ip: string, version?: IpVersion) {
if ((version === "v4" || !version) && ipv4Regex.test(ip)) {
return true;
}
if ((version === "v6" || !version) && ipv6Regex.test(ip)) {
return true;
}
return false;
}
export const isValid = <T>(x: ParseReturnType<T>): x is OK<T> | DIRTY<T> => (x as any).status === "valid";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment