Skip to content

Commit

Permalink
Merge pull request #7 from enormora/string
Browse files Browse the repository at this point in the history
Format invalid_string issues
  • Loading branch information
lo1tuma authored Mar 22, 2024
2 parents dda283d + 1a1f430 commit 8ac2f48
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 1 deletion.
22 changes: 22 additions & 0 deletions integration-tests/zod-error-formatter/string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test } from '@sondr3/minitest';
import assert from 'node:assert';
import { z } from 'zod';
import { safeParse } from '../../source/zod-error-formatter/formatter.js';

test('formats messages for invalid string email validation', () => {
const schema = z.string().email();
const result = safeParse(schema, 'foo');

assert.strictEqual(result.success, false);
assert.deepStrictEqual(result.error.issues, ['invalid email']);
});

test('formats messages for invalid string includes validation', () => {
const schema = z.string().includes('foo', { position: 2 });
const result = safeParse(schema, 'foo');

assert.strictEqual(result.success, false);
assert.deepStrictEqual(result.error.issues, [
'string must include "foo" at one ore more positions greater than or equal to 2'
]);
});
10 changes: 10 additions & 0 deletions source/zod-error-formatter/format-issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,13 @@ test('returns the formatted issue when an invalid_enum_value issue is given', ()
});
assert.strictEqual(formattedIssue, 'at foo: invalid enum value: expected one of "a" or "b", but got number');
});

test('returns the formatted issue when an invalid_string issue is given', () => {
const formattedIssue = formatIssue({
code: 'invalid_string',
path: ['foo'],
message: '',
validation: 'ip'
});
assert.strictEqual(formattedIssue, 'at foo: invalid ip');
});
4 changes: 3 additions & 1 deletion source/zod-error-formatter/format-issue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ZodIssue, ZodIssueCode } from 'zod';
import { formatInvalidEnumValueIssueMessage } from './issue-specific/invalid-enum-value.js';
import { formatInvalidLiteralIssueMessage } from './issue-specific/invalid-literal.js';
import { formatInvalidStringIssueMessage } from './issue-specific/invalid-string.js';
import { formatInvalidTypeIssueMessage } from './issue-specific/invalid-type.js';
import { formatNotMultipleOfIssueMessage } from './issue-specific/not-multiple-of.js';
import { formatTooBigIssueMessage } from './issue-specific/too-big.js';
Expand All @@ -21,7 +22,8 @@ const issueCodeToFormatterMap: FormatterMap = {
too_big: formatTooBigIssueMessage,
too_small: formatTooSmallIssueMessage,
not_multiple_of: formatNotMultipleOfIssueMessage,
invalid_enum_value: formatInvalidEnumValueIssueMessage
invalid_enum_value: formatInvalidEnumValueIssueMessage,
invalid_string: formatInvalidStringIssueMessage
};

export function formatIssue(issue: ZodIssue): string {
Expand Down
143 changes: 143 additions & 0 deletions source/zod-error-formatter/issue-specific/invalid-string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { test } from '@sondr3/minitest';
import assert from 'node:assert';
import { formatInvalidStringIssueMessage } from './invalid-string.js';

test('formats the invalid string issue correctly when validation is "regex"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'regex'
});
assert.strictEqual(message, 'string doesn’t match expected pattern');
});

test('formats the invalid string issue correctly when validation is "email"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'email'
});
assert.strictEqual(message, 'invalid email');
});

test('formats the invalid string issue correctly when validation is "url"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'url'
});
assert.strictEqual(message, 'invalid url');
});

test('formats the invalid string issue correctly when validation is "emoji"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'emoji'
});
assert.strictEqual(message, 'invalid emoji');
});

test('formats the invalid string issue correctly when validation is "uuid"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'uuid'
});
assert.strictEqual(message, 'invalid uuid');
});

test('formats the invalid string issue correctly when validation is "cuid"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'cuid'
});
assert.strictEqual(message, 'invalid cuid');
});

test('formats the invalid string issue correctly when validation is "cuid2"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'cuid2'
});
assert.strictEqual(message, 'invalid cuid2');
});

test('formats the invalid string issue correctly when validation is "ulid"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'ulid'
});
assert.strictEqual(message, 'invalid ulid');
});

test('formats the invalid string issue correctly when validation is "datetime"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'datetime'
});
assert.strictEqual(message, 'invalid datetime');
});

test('formats the invalid string issue correctly when validation is "ip"', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: 'ip'
});
assert.strictEqual(message, 'invalid ip');
});

test('formats the invalid string issue correctly when validation is requires an includes term without position', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: { includes: 'foo' }
});
assert.strictEqual(message, 'string must include "foo"');
});

test('formats the invalid string issue correctly when validation is requires an includes term with position', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: { includes: 'foo', position: 42 }
});
assert.strictEqual(message, 'string must include "foo" at one ore more positions greater than or equal to 42');
});

test('formats the invalid string issue correctly when validation is requires a starts-with term', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: { startsWith: 'foo' }
});
assert.strictEqual(message, 'string must start with "foo"');
});

test('formats the invalid string issue correctly when validation is requires a ends-with term', () => {
const message = formatInvalidStringIssueMessage({
code: 'invalid_string',
path: [],
message: '',
validation: { endsWith: 'foo' }
});
assert.strictEqual(message, 'string must end with "foo"');
});
34 changes: 34 additions & 0 deletions source/zod-error-formatter/issue-specific/invalid-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ZodInvalidStringIssue } from 'zod';

function hasProperty<ObjectType extends Record<string, unknown>, Key extends string>(
object: ObjectType,
key: Key
): object is Extract<ObjectType, Record<Key, unknown>> {
return Object.hasOwn(object, key);
}

function formatIncludesValidation(includes: string, position?: number): string {
if (position !== undefined) {
return `string must include "${includes}" at one ore more positions greater than or equal to ${position}`;
}

return `string must include "${includes}"`;
}

export function formatInvalidStringIssueMessage(issue: ZodInvalidStringIssue): string {
const { validation } = issue;

if (validation === 'regex') {
return 'string doesn’t match expected pattern';
}
if (typeof validation === 'string') {
return `invalid ${validation}`;
}
if (hasProperty(validation, 'includes')) {
return formatIncludesValidation(validation.includes, validation.position);
}
if (hasProperty(validation, 'startsWith')) {
return `string must start with "${validation.startsWith}"`;
}
return `string must end with "${validation.endsWith}"`;
}

0 comments on commit 8ac2f48

Please sign in to comment.