Skip to content

Commit

Permalink
Added file type
Browse files Browse the repository at this point in the history
  • Loading branch information
obha committed Nov 21, 2024
1 parent 207205c commit a462132
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
- [Cyclical objects](#cyclical-objects)
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Files](#files)
- [Functions](#functions)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
Expand Down Expand Up @@ -1925,6 +1926,34 @@ TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws
```

## Files

This basic implementation validates the input type. As usual, you can set custom error messages.

```ts
const imageFile = z.file({
required_error: "file is required",
invalid_type_error: "This object must be a file",
});
```

You can validate the file's minimum and maximum size.

```ts
const imageFile = z.file().size({ min: 100000, max: 200000 });
```

You check again min and max size of the file.

```ts
// Validate MIME type for any image
const imageFileAny = z.file().mimeType("image/*");

// Validate specific MIME types
const imageFileJpegPng = z.file().mimeType("image/jpeg,image/png");
const imageFileJpegPngArray = z.file().mimeType(["image/jpeg", "image/png"]);
```

## Functions

Zod also lets you define "function schemas". This makes it easy to validate the inputs and outputs of a function without intermixing your validation code and "business logic".
Expand Down
29 changes: 29 additions & 0 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
- [Cyclical objects](#cyclical-objects)
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Files](#files)
- [Functions](#functions)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
Expand Down Expand Up @@ -1925,6 +1926,34 @@ TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws
```

## Files

This basic implementation validates the input type. As usual, you can set custom error messages.

```ts
const imageFile = z.file({
required_error: "file is required",
invalid_type_error: "This object must be a file",
});
```

You can validate the file's minimum and maximum size.

```ts
const imageFile = z.file().size({ min: 100000, max: 200000 });
```

You check again min and max size of the file.

```ts
// Validate MIME type for any image
const imageFileAny = z.file().mimeType("image/*");

// Validate specific MIME types
const imageFileJpegPng = z.file().mimeType("image/jpeg,image/png");
const imageFileJpegPngArray = z.file().mimeType(["image/jpeg", "image/png"]);
```

## Functions

Zod also lets you define "function schemas". This makes it easy to validate the inputs and outputs of a function without intermixing your validation code and "business logic".
Expand Down
49 changes: 49 additions & 0 deletions deno/lib/__tests__/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
const test = Deno.test;

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

class MockFile {
static create(size?: number, name?: string, mimeType?: string) {
name = name || "mock.txt";
size = size || 1024;
mimeType = mimeType || "plain/txt";

function range(count: number) {
let output = "";
for (let i = 0; i < count; i++) {
output += "a";
}
return output;
}

return new File([range(size)], name, { type: mimeType });
}
}

const basicSchema = z.file().size({ max: 50000, min: 1024 });
const imageSchema = z
.file()
.size({ min: 20000 })
.mimeType("image/*,image/jpeg,image/png", "Invalide mime type");

test("passing validations", () => {
expect(basicSchema.parse(MockFile.create(1024))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(50000))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(2014))).toBeInstanceOf(File);
expect(
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "image/jpeg"))
).toBeInstanceOf(File);
});

test("failing validations", () => {
expect(() => basicSchema.parse(MockFile.create(1023))).toThrow();
expect(() => basicSchema.parse(MockFile.create(50001))).toThrow();
expect(() =>
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "video/mp4"))
).toThrow();
expect(() => basicSchema.parse(undefined)).toThrow();
expect(() => basicSchema.parse({})).toThrow();
expect(() => basicSchema.parse([])).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 @@ -52,6 +52,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodSet:
break;
case z.ZodFirstPartyTypeKind.ZodFile:
break;
case z.ZodFirstPartyTypeKind.ZodFunction:
break;
case z.ZodFirstPartyTypeKind.ZodLazy:
Expand Down
4 changes: 4 additions & 0 deletions deno/lib/helpers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const ZodParsedType = util.arrayToEnum([
"date",
"bigint",
"symbol",
"file",
"function",
"undefined",
"null",
Expand Down Expand Up @@ -189,6 +190,9 @@ export const getParsedType = (data: any): ZodParsedType => {
return ZodParsedType.symbol;

case "object":
if (data instanceof File) {
return ZodParsedType.file;
}
if (Array.isArray(data)) {
return ZodParsedType.array;
}
Expand Down
132 changes: 132 additions & 0 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3847,6 +3847,134 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
};
}

///////////////////////////////////////////
///////////////////////////////////////////
////////// //////////
////////// ZodFile //////////
////////// //////////
///////////////////////////////////////////
///////////////////////////////////////////
type SizeRange = { min?: number; max?: number };

export type ZodFileCheck =
| { kind: "size"; value: SizeRange; message?: string }
| { kind: "mime_type"; value: string | string[]; message?: string };

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

export class ZodFile extends ZodType<File, ZodFileDef, File> {
size(range: SizeRange, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "size",
value: range,
message: errorUtil.toString(message),
});
}

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

_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const parsedType = this._getType(input);

if (parsedType !== ZodParsedType.file) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.file,
received: ctx.parsedType,
});
return INVALID;
}

const file = input.data as File;

const status = new ParseStatus();

for (const check of this._def.checks) {
const ctx = this._getOrReturnCtx(input);

if (
check.kind === "size" &&
check.value.min &&
check.value.min > file.size
) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value.min,
type: ZodParsedType.number,
inclusive: true,
exact: false,
message: check.message,
});

status.dirty();
} else if (
check.kind === "size" &&
check.value.max &&
check.value.max < file.size
) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value.max,
type: ZodParsedType.number,
inclusive: true,
exact: false,
message: check.message,
});

status.dirty();
} else if (check.kind === "mime_type") {
const mimeTypes = Array.isArray(check.value)
? check.value
: check.value.split(",").map((s) => s.trim());

const state = mimeTypes.reduce((state, currentMimeType) => {
const regex = new RegExp(currentMimeType, "gm");

return state || regex.test(file.type);
}, false);

if (!state) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.object,
received: ctx.parsedType,
message: check.message,
});

status.dirty();
}
}
}

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

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

static create = (params?: RawCreateParams): ZodFile => {
return new ZodFile({
typeName: ZodFirstPartyTypeKind.ZodFile,
checks: [],
...processCreateParams(params),
});
};
}

///////////////////////////////////////////
///////////////////////////////////////////
////////// //////////
Expand Down Expand Up @@ -5117,6 +5245,7 @@ export const late = {
};

export enum ZodFirstPartyTypeKind {
ZodFile = "ZodFile",
ZodString = "ZodString",
ZodNumber = "ZodNumber",
ZodNaN = "ZodNaN",
Expand Down Expand Up @@ -5176,6 +5305,7 @@ export type ZodFirstPartySchemaTypes =
| ZodRecord<any, any>
| ZodMap<any>
| ZodSet<any>
| ZodFile
| ZodFunction<any, any>
| ZodLazy<any>
| ZodLiteral<any>
Expand Down Expand Up @@ -5227,6 +5357,7 @@ const tupleType = ZodTuple.create;
const recordType = ZodRecord.create;
const mapType = ZodMap.create;
const setType = ZodSet.create;
const fileType = ZodFile.create;
const functionType = ZodFunction.create;
const lazyType = ZodLazy.create;
const literalType = ZodLiteral.create;
Expand Down Expand Up @@ -5267,6 +5398,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
48 changes: 48 additions & 0 deletions src/__tests__/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @ts-ignore TS6133
import { expect, test } from "@jest/globals";

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

class MockFile {
static create(size?: number, name?: string, mimeType?: string) {
name = name || "mock.txt";
size = size || 1024;
mimeType = mimeType || "plain/txt";

function range(count: number) {
let output = "";
for (let i = 0; i < count; i++) {
output += "a";
}
return output;
}

return new File([range(size)], name, { type: mimeType });
}
}

const basicSchema = z.file().size({ max: 50000, min: 1024 });
const imageSchema = z
.file()
.size({ min: 20000 })
.mimeType("image/*,image/jpeg,image/png", "Invalide mime type");

test("passing validations", () => {
expect(basicSchema.parse(MockFile.create(1024))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(50000))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(2014))).toBeInstanceOf(File);
expect(
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "image/jpeg"))
).toBeInstanceOf(File);
});

test("failing validations", () => {
expect(() => basicSchema.parse(MockFile.create(1023))).toThrow();
expect(() => basicSchema.parse(MockFile.create(50001))).toThrow();
expect(() =>
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "video/mp4"))
).toThrow();
expect(() => basicSchema.parse(undefined)).toThrow();
expect(() => basicSchema.parse({})).toThrow();
expect(() => basicSchema.parse([])).toThrow();
});
2 changes: 2 additions & 0 deletions src/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodSet:
break;
case z.ZodFirstPartyTypeKind.ZodFile:
break;
case z.ZodFirstPartyTypeKind.ZodFunction:
break;
case z.ZodFirstPartyTypeKind.ZodLazy:
Expand Down
Loading

0 comments on commit a462132

Please sign in to comment.