Skip to content

Commit

Permalink
feat: implement File type
Browse files Browse the repository at this point in the history
814
  • Loading branch information
jgradzki committed Mar 14, 2022
1 parent 70615e6 commit 126f2fb
Show file tree
Hide file tree
Showing 13 changed files with 595 additions and 9 deletions.
2 changes: 1 addition & 1 deletion coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 15 additions & 2 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ZodIssueCode = util.arrayToEnum([
"too_big",
"invalid_intersection_types",
"not_multiple_of",
"invalid_file_type",
]);

export type ZodIssueCode = keyof typeof ZodIssueCode;
Expand Down Expand Up @@ -78,14 +79,14 @@ export interface ZodTooSmallIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_small;
minimum: number;
inclusive: boolean;
type: "array" | "string" | "number" | "set";
type: "array" | "string" | "number" | "set" | "file";
}

export interface ZodTooBigIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_big;
maximum: number;
inclusive: boolean;
type: "array" | "string" | "number" | "set";
type: "array" | "string" | "number" | "set" | "file";
}

export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
Expand All @@ -97,6 +98,12 @@ export interface ZodNotMultipleOfIssue extends ZodIssueBase {
multipleOf: number;
}

export interface ZodInvalidFileType extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_file_type;
expected: string[];
received: string;
}

export interface ZodCustomIssue extends ZodIssueBase {
code: typeof ZodIssueCode.custom;
params?: { [k: string]: any };
Expand All @@ -118,6 +125,7 @@ export type ZodIssueOptionalMessage =
| ZodTooBigIssue
| ZodInvalidIntersectionTypesIssue
| ZodNotMultipleOfIssue
| ZodInvalidFileType
| ZodCustomIssue;

export type ZodIssue = ZodIssueOptionalMessage & { message: string };
Expand Down Expand Up @@ -356,6 +364,11 @@ export const defaultErrorMap = (
case ZodIssueCode.not_multiple_of:
message = `Number must be a multiple of ${issue.multipleOf}`;
break;
case ZodIssueCode.invalid_file_type:
message = `The file type should be ${issue.expected.join(
", "
)}. Received: ${issue.received}`;
break;
default:
message = _ctx.defaultError;
util.assertNever(issue);
Expand Down
84 changes: 84 additions & 0 deletions deno/lib/__tests__/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;

import * as z from "../index.ts";

const file = z.file();
const max = z.file().max(4);
const min = z.file().min(2);
const type = z.file().type("image/png");
const multipleTypes = z.file().type(["image/png", "image/jpeg"]);

test("passing validations", () => {
file.parse(
new File([], "file.png", {
type: "image/png",
})
);
max.parse(
new File([], "file.png", {
type: "image/png",
})
);
min.parse(
new File(["d", "b", "e", "z"], "file.png", {
type: "image/png",
})
);
type.parse(
new File(["m", "i"], "file.png", {
type: "image/png",
})
);
multipleTypes.parse(
new File([], "file.png", {
type: "image/png",
})
);
});

test("failing validations", () => {
expect(() => file.parse(null)).toThrow();
expect(() =>
max.parse(
new File(["z", "o", "d", "l", "i", "b"], "file.png", {
type: "image/png",
})
)
).toThrow();
expect(() =>
max.parse(
new File(["z", "o", "d", "l", "i"], "file.png", {
type: "image/png",
})
)
).toThrow();
expect(() =>
min.parse(
new File(["s"], "file.png", {
type: "image/png",
})
)
).toThrow();
expect(() =>
min.parse(
new File([], "file.png", {
type: "image/png",
})
)
).toThrow();
expect(() =>
type.parse(
new File([], "file.gif", {
type: "image/jpg",
})
)
).toThrow();
expect(() =>
multipleTypes.parse(
new File([], "file.gif", {
type: "image/jpg",
})
)
).toThrow();
});
2 changes: 2 additions & 0 deletions deno/lib/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodPromise:
break;
case z.ZodFirstPartyTypeKind.ZodFile:
break;
default:
util.assertNever(def);
}
Expand Down
1 change: 1 addition & 0 deletions deno/lib/helpers/parseUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ZodParsedType = util.arrayToEnum([
"never",
"map",
"set",
"file",
]);

export type ZodParsedType = keyof typeof ZodParsedType;
Expand Down
141 changes: 140 additions & 1 deletion deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3488,6 +3488,141 @@ export class ZodNaN extends ZodType<number, ZodNaNDef> {
};
}

/////////////////////////////////////////
/////////////////////////////////////////
////////// //////////
////////// ZodFile //////////
////////// //////////
/////////////////////////////////////////
/////////////////////////////////////////

// const FileConstructor = typeof File === "undefined" ? WebStdFile : File;

type ZodFileCheck =
| { kind: "max"; value: number; message?: string }
| { kind: "min"; value: number; message?: string }
| { kind: "type"; value: string | string[]; message?: string };

export interface ZodFileDef extends ZodTypeDef {
typeName: ZodFirstPartyTypeKind.ZodFile;
checks: ZodFileCheck[];
}

export class ZodFile<FileConstructor extends Function> extends ZodType<
FileConstructor,
ZodFileDef
> {
constructor(def: ZodFileDef, private readonly fileConstructor: Function) {
super(def);
}

_parse(input: ParseInput): ParseReturnType<FileConstructor> {
const { status, ctx } = this._processInputParams(input);

if (!(input.data instanceof this.fileConstructor)) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.file,
received: ctx.parsedType,
});

return INVALID;
}

for (const check of this._def.checks) {
if (check.kind === "max") {
if (ctx.data.size > check.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value,
type: "file",
inclusive: true,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "min") {
if (ctx.data.size < check.value) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value,
type: "file",
inclusive: true,
message: check.message,
});
status.dirty();
}
} else if (check.kind === "type") {
const types = Array.isArray(check.value) ? check.value : [check.value];

if (!types.includes(ctx.data.type)) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_file_type,
expected: types,
received: ctx.data.type,
message: check.message,
});
status.dirty();
}
}
}

return { status: status.value, value: ctx.data };
}

_addCheck(check: ZodFileCheck) {
return new ZodFile(
{
...this._def,
checks: [...this._def.checks, check],
},
this.fileConstructor
);
}

max(maxSize: number, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "max",
value: maxSize,
...errorUtil.errToObj(message),
});
}

min(minSize: number, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "min",
value: minSize,
...errorUtil.errToObj(message),
});
}

type(accept: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "type",
value: accept,
...errorUtil.errToObj(message),
});
}

static create = <T extends Function = typeof File>(
fileConstructor?: T
): ZodFile<T> => {
if (typeof File === "undefined" && !fileConstructor) {
throw new Error(
'"File not supported in current environment, you need to pass constructor to create()'
);
}

return new ZodFile(
{
typeName: ZodFirstPartyTypeKind.ZodFile,
checks: [],
},
fileConstructor || File
);
};
}

export const custom = <T>(
check?: (data: unknown) => any,
params?: Parameters<ZodTypeAny["refine"]>[1]
Expand Down Expand Up @@ -3534,6 +3669,7 @@ export enum ZodFirstPartyTypeKind {
ZodNullable = "ZodNullable",
ZodDefault = "ZodDefault",
ZodPromise = "ZodPromise",
ZodFile = "ZodFile",
}
export type ZodFirstPartySchemaTypes =
| ZodString
Expand Down Expand Up @@ -3565,7 +3701,8 @@ export type ZodFirstPartySchemaTypes =
| ZodOptional<any>
| ZodNullable<any>
| ZodDefault<any>
| ZodPromise<any>;
| ZodPromise<any>
| ZodFile<any>;

const instanceOfType = <T extends new (...args: any[]) => any>(
cls: T,
Expand Down Expand Up @@ -3605,6 +3742,7 @@ const promiseType = ZodPromise.create;
const effectsType = ZodEffects.create;
const optionalType = ZodOptional.create;
const nullableType = ZodNullable.create;
const fileType = ZodFile.create;
const preprocessType = ZodEffects.createWithPreprocess;
const ostring = () => stringType().optional();
const onumber = () => numberType().optional();
Expand All @@ -3619,6 +3757,7 @@ export {
discriminatedUnionType as discriminatedUnion,
effectsType as effect,
enumType as enum,
fileType as file,
functionType as function,
instanceOfType as instanceof,
intersectionType as intersection,
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,8 @@
"yarn fix:lint",
"yarn fix:format"
]
},
"dependencies": {
"@web-std/file": "^3.0.2"
}
}
Loading

0 comments on commit 126f2fb

Please sign in to comment.