Skip to content

Commit

Permalink
Add string.startsWith and string.endsWith (#1235)
Browse files Browse the repository at this point in the history
* Add string.startsWith and string.endsWith

* Update implementation of startsWith and endsWith

* Update readmes

Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu>
  • Loading branch information
grumpyoldman-io and Colin McDonnell committed Jul 18, 2022
1 parent ff43b2e commit 799fbb1
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 10 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -453,6 +453,8 @@ z.string().url();
z.string().uuid();
z.string().cuid();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);

// trim whitespace
z.string().trim();
Expand Down Expand Up @@ -484,6 +486,8 @@ z.string().length(5, { message: "Must be exactly 5 characters long" });
z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().uuid({ message: "Invalid UUID" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
```

## Numbers
Expand Down
12 changes: 8 additions & 4 deletions README_ZH.md
Expand Up @@ -391,6 +391,8 @@ z.string().email();
z.string().url();
z.string().uuid();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);

// 已废弃,等同于 .min(1)
z.string().nonempty();
Expand All @@ -409,6 +411,8 @@ z.string().length(5, { message: "Must be exactly 5 characters long" });
z.string().email({ message: "Invalid email address." });
z.string().url({ message: "Invalid url" });
z.string().uuid({ message: "Invalid UUID" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
```

## Numbers
Expand Down Expand Up @@ -555,9 +559,9 @@ const user = z.object({

const deepPartialUser = user.deepPartial();

/*
/*
{
username?: string | undefined,
username?: string | undefined,
location?: {
latitude?: number | undefined;
longitude?: number | undefined;
Expand Down Expand Up @@ -884,12 +888,12 @@ const FishEnum = z.enum(fish);
FishEnum.enum.Salmon; // => 自动补全

FishEnum.enum;
/*
/*
=> {
Salmon: "Salmon",
Tuna: "Tuna",
Trout: "Trout",
}
}
*/
```

Expand Down
4 changes: 4 additions & 0 deletions deno/lib/README.md
Expand Up @@ -453,6 +453,8 @@ z.string().url();
z.string().uuid();
z.string().cuid();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);

// trim whitespace
z.string().trim();
Expand Down Expand Up @@ -484,6 +486,8 @@ z.string().length(5, { message: "Must be exactly 5 characters long" });
z.string().email({ message: "Invalid email address" });
z.string().url({ message: "Invalid url" });
z.string().uuid({ message: "Invalid UUID" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
```

## Numbers
Expand Down
24 changes: 21 additions & 3 deletions deno/lib/ZodError.ts
Expand Up @@ -86,7 +86,14 @@ export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}

export type StringValidation = "email" | "url" | "uuid" | "regex" | "cuid";
export type StringValidation =
| "email"
| "url"
| "uuid"
| "regex"
| "cuid"
| { startsWith: string }
| { endsWith: string };

export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
Expand Down Expand Up @@ -344,8 +351,19 @@ export const defaultErrorMap = (
message = `Invalid date`;
break;
case ZodIssueCode.invalid_string:
if (issue.validation !== "regex") message = `Invalid ${issue.validation}`;
else message = "Invalid";
if (typeof issue.validation === "object") {
if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.endsWith}"`;
} else {
util.assertNever(issue.validation);
}
} else if (issue.validation !== "regex") {
message = `Invalid ${issue.validation}`;
} else {
message = "Invalid";
}
break;
case ZodIssueCode.too_small:
if (issue.type === "array")
Expand Down
6 changes: 6 additions & 0 deletions deno/lib/__tests__/string.test.ts
Expand Up @@ -8,6 +8,8 @@ const minFive = z.string().min(5, "min5");
const maxFive = z.string().max(5, "max5");
const justFive = z.string().length(5);
const nonempty = z.string().nonempty("nonempty");
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");

test("passing validations", () => {
minFive.parse("12345");
Expand All @@ -16,6 +18,8 @@ test("passing validations", () => {
maxFive.parse("1234");
nonempty.parse("1");
justFive.parse("12345");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
});

test("failing validations", () => {
Expand All @@ -24,6 +28,8 @@ test("failing validations", () => {
expect(() => nonempty.parse("")).toThrow();
expect(() => justFive.parse("1234")).toThrow();
expect(() => justFive.parse("123456")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
});

test("email validations", () => {
Expand Down
38 changes: 38 additions & 0 deletions deno/lib/types.ts
Expand Up @@ -450,6 +450,8 @@ type ZodStringCheck =
| { kind: "url"; message?: string }
| { kind: "uuid"; message?: string }
| { kind: "cuid"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
| { kind: "trim"; message?: string };

Expand Down Expand Up @@ -570,6 +572,26 @@ export class ZodString extends ZodType<string, ZodStringDef> {
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} 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 {
util.assertNever(check);
}
Expand Down Expand Up @@ -616,6 +638,22 @@ export class ZodString extends ZodType<string, ZodStringDef> {
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
value: value,
...errorUtil.errToObj(message),
});
}

endsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "endsWith",
value: value,
...errorUtil.errToObj(message),
});
}

min(minLength: number, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "min",
Expand Down
24 changes: 21 additions & 3 deletions src/ZodError.ts
Expand Up @@ -86,7 +86,14 @@ export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}

export type StringValidation = "email" | "url" | "uuid" | "regex" | "cuid";
export type StringValidation =
| "email"
| "url"
| "uuid"
| "regex"
| "cuid"
| { startsWith: string }
| { endsWith: string };

export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
Expand Down Expand Up @@ -344,8 +351,19 @@ export const defaultErrorMap = (
message = `Invalid date`;
break;
case ZodIssueCode.invalid_string:
if (issue.validation !== "regex") message = `Invalid ${issue.validation}`;
else message = "Invalid";
if (typeof issue.validation === "object") {
if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.endsWith}"`;
} else {
util.assertNever(issue.validation);
}
} else if (issue.validation !== "regex") {
message = `Invalid ${issue.validation}`;
} else {
message = "Invalid";
}
break;
case ZodIssueCode.too_small:
if (issue.type === "array")
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/string.test.ts
Expand Up @@ -7,6 +7,8 @@ const minFive = z.string().min(5, "min5");
const maxFive = z.string().max(5, "max5");
const justFive = z.string().length(5);
const nonempty = z.string().nonempty("nonempty");
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");

test("passing validations", () => {
minFive.parse("12345");
Expand All @@ -15,6 +17,8 @@ test("passing validations", () => {
maxFive.parse("1234");
nonempty.parse("1");
justFive.parse("12345");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
});

test("failing validations", () => {
Expand All @@ -23,6 +27,8 @@ test("failing validations", () => {
expect(() => nonempty.parse("")).toThrow();
expect(() => justFive.parse("1234")).toThrow();
expect(() => justFive.parse("123456")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
});

test("email validations", () => {
Expand Down
38 changes: 38 additions & 0 deletions src/types.ts
Expand Up @@ -450,6 +450,8 @@ type ZodStringCheck =
| { kind: "url"; message?: string }
| { kind: "uuid"; message?: string }
| { kind: "cuid"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
| { kind: "trim"; message?: string };

Expand Down Expand Up @@ -570,6 +572,26 @@ export class ZodString extends ZodType<string, ZodStringDef> {
}
} else if (check.kind === "trim") {
input.data = input.data.trim();
} 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 {
util.assertNever(check);
}
Expand Down Expand Up @@ -616,6 +638,22 @@ export class ZodString extends ZodType<string, ZodStringDef> {
});
}

startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
value: value,
...errorUtil.errToObj(message),
});
}

endsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "endsWith",
value: value,
...errorUtil.errToObj(message),
});
}

min(minLength: number, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "min",
Expand Down

0 comments on commit 799fbb1

Please sign in to comment.