Last active
June 18, 2023 04:34
-
-
Save webNeat/ce0895709fdba05c085e1a67208d653b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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