2023-08-18 08:02:17 +00:00
|
|
|
import { codeBlock } from 'common-tags';
|
2022-09-27 10:00:35 +00:00
|
|
|
import { z } from 'zod';
|
2023-06-29 16:22:26 +00:00
|
|
|
import {
|
|
|
|
Json,
|
|
|
|
Json5,
|
|
|
|
LooseArray,
|
|
|
|
LooseRecord,
|
2023-08-18 08:02:17 +00:00
|
|
|
Toml,
|
2023-06-29 16:22:26 +00:00
|
|
|
UtcDate,
|
2023-08-07 06:04:54 +00:00
|
|
|
Yaml,
|
2023-06-29 16:22:26 +00:00
|
|
|
} from './schema-utils';
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-06 15:56:27 +00:00
|
|
|
describe('util/schema-utils', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
describe('LooseArray', () => {
|
2023-02-22 08:21:09 +00:00
|
|
|
it('parses array', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
const s = LooseArray(z.string());
|
2023-02-22 08:21:09 +00:00
|
|
|
expect(s.parse(['foo', 'bar'])).toEqual(['foo', 'bar']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('drops wrong items', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
const s = LooseArray(z.string());
|
2023-02-22 08:21:09 +00:00
|
|
|
expect(s.parse(['foo', 123, null, undefined, []])).toEqual(['foo']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('runs callback for wrong elements', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
let err: z.ZodError | undefined = undefined;
|
|
|
|
const Schema = LooseArray(z.string(), {
|
|
|
|
onError: ({ error }) => {
|
|
|
|
err = error;
|
|
|
|
},
|
2023-02-22 08:21:09 +00:00
|
|
|
});
|
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
const res = Schema.parse(['foo', 123, 'bar']);
|
|
|
|
|
|
|
|
expect(res).toEqual(['foo', 'bar']);
|
|
|
|
expect(err).toMatchObject({
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected string, received number',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'number',
|
|
|
|
path: [1],
|
|
|
|
},
|
|
|
|
],
|
2023-02-22 08:21:09 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
describe('LooseRecord', () => {
|
2023-02-22 08:21:09 +00:00
|
|
|
it('parses record', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
const s = LooseRecord(z.string());
|
2023-02-22 08:21:09 +00:00
|
|
|
expect(s.parse({ foo: 'bar' })).toEqual({ foo: 'bar' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('drops wrong items', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
const s = LooseRecord(z.string());
|
2023-02-22 08:21:09 +00:00
|
|
|
expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' });
|
|
|
|
});
|
|
|
|
|
2023-05-24 10:36:19 +00:00
|
|
|
it('supports key schema', () => {
|
|
|
|
const s = LooseRecord(
|
2023-05-25 12:21:32 +00:00
|
|
|
z
|
|
|
|
.string()
|
|
|
|
.refine((x) => x === 'bar')
|
|
|
|
.transform((x) => x.toUpperCase()),
|
|
|
|
z.string().transform((x) => x.toUpperCase())
|
2023-05-24 10:36:19 +00:00
|
|
|
);
|
2023-05-25 12:21:32 +00:00
|
|
|
expect(s.parse({ foo: 'foo', bar: 'bar' })).toEqual({ BAR: 'BAR' });
|
2023-05-24 10:36:19 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('reports key schema errors', () => {
|
|
|
|
let errorData: unknown = null;
|
|
|
|
const s = LooseRecord(
|
|
|
|
z.string().refine((x) => x === 'bar'),
|
|
|
|
z.string(),
|
|
|
|
{
|
|
|
|
onError: (x) => {
|
|
|
|
errorData = x;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
s.parse({ foo: 'foo', bar: 'bar' });
|
|
|
|
|
|
|
|
expect(errorData).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
code: 'custom',
|
|
|
|
message: 'Invalid input',
|
|
|
|
path: ['foo'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
input: { bar: 'bar', foo: 'foo' },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-02-22 08:21:09 +00:00
|
|
|
it('runs callback for wrong elements', () => {
|
2023-04-21 08:25:48 +00:00
|
|
|
let err: z.ZodError | undefined = undefined;
|
|
|
|
const Schema = LooseRecord(
|
|
|
|
z.object({ foo: z.object({ bar: z.string() }) }),
|
|
|
|
{
|
|
|
|
onError: ({ error }) => {
|
|
|
|
err = error;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
2023-02-22 08:21:09 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
const res = Schema.parse({
|
|
|
|
aaa: { foo: { bar: 42 } },
|
|
|
|
bbb: { foo: { baz: 'asdf' } },
|
|
|
|
ccc: { foo: { bar: 'baz' } },
|
2023-02-22 08:21:09 +00:00
|
|
|
});
|
2023-02-22 14:45:26 +00:00
|
|
|
|
2023-04-21 08:25:48 +00:00
|
|
|
expect(res).toEqual({ ccc: { foo: { bar: 'baz' } } });
|
|
|
|
expect(err).toMatchObject({
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected string, received number',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'number',
|
|
|
|
path: ['aaa', 'foo', 'bar'],
|
|
|
|
},
|
|
|
|
{
|
|
|
|
message: 'Required',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'undefined',
|
|
|
|
path: ['bbb', 'foo', 'bar'],
|
|
|
|
},
|
|
|
|
],
|
2023-02-22 14:45:26 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
describe('Json', () => {
|
2023-04-07 14:53:57 +00:00
|
|
|
it('parses json', () => {
|
2023-04-17 08:01:23 +00:00
|
|
|
const Schema = Json.pipe(z.object({ foo: z.literal('bar') }));
|
|
|
|
|
|
|
|
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
|
|
|
|
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected string, received number',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'number',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid literal value, expected "bar"',
|
|
|
|
code: 'invalid_literal',
|
|
|
|
expected: 'bar',
|
|
|
|
received: 'foo',
|
|
|
|
path: ['foo'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected object, received array',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'object',
|
|
|
|
received: 'array',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('{{{}}}')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid JSON',
|
|
|
|
code: 'custom',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
});
|
2023-04-17 08:01:23 +00:00
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
describe('Json5', () => {
|
|
|
|
it('parses JSON5', () => {
|
|
|
|
const Schema = Json5.pipe(z.object({ foo: z.literal('bar') }));
|
|
|
|
|
|
|
|
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
|
|
|
|
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected string, received number',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'number',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid literal value, expected "bar"',
|
|
|
|
code: 'invalid_literal',
|
|
|
|
expected: 'bar',
|
|
|
|
received: 'foo',
|
|
|
|
path: ['foo'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected object, received array',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'object',
|
|
|
|
received: 'array',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
|
2023-04-17 08:01:23 +00:00
|
|
|
expect(Schema.safeParse('{{{}}}')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid JSON5',
|
|
|
|
code: 'custom',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
2023-04-07 14:53:57 +00:00
|
|
|
});
|
|
|
|
});
|
2023-06-04 15:06:43 +00:00
|
|
|
|
|
|
|
describe('UtcDate', () => {
|
|
|
|
it('parses date', () => {
|
|
|
|
expect(UtcDate.parse('2020-04-04').toString()).toBe(
|
|
|
|
'2020-04-04T00:00:00.000Z'
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('rejects invalid date', () => {
|
|
|
|
expect(() => UtcDate.parse('foobar')).toThrow();
|
|
|
|
});
|
|
|
|
});
|
2023-06-29 16:22:26 +00:00
|
|
|
|
2023-08-07 06:04:54 +00:00
|
|
|
describe('Yaml', () => {
|
|
|
|
const Schema = Yaml.pipe(
|
|
|
|
z.object({ foo: z.array(z.object({ bar: z.literal('baz') })) })
|
|
|
|
);
|
|
|
|
|
|
|
|
it('parses valid yaml', () => {
|
|
|
|
expect(Schema.parse('foo:\n- bar: baz')).toEqual({
|
|
|
|
foo: [{ bar: 'baz' }],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws error for non-string', () => {
|
|
|
|
expect(Schema.safeParse(42)).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Expected string, received number',
|
|
|
|
code: 'invalid_type',
|
|
|
|
expected: 'string',
|
|
|
|
received: 'number',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws error for invalid yaml', () => {
|
|
|
|
expect(Schema.safeParse('clearly: "invalid" "yaml"')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid YAML',
|
|
|
|
code: 'custom',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-08-18 08:02:17 +00:00
|
|
|
|
|
|
|
describe('Toml', () => {
|
|
|
|
const Schema = Toml.pipe(
|
|
|
|
z.object({ foo: z.object({ bar: z.literal('baz') }) })
|
|
|
|
);
|
|
|
|
|
|
|
|
it('parses valid toml', () => {
|
|
|
|
const content = codeBlock`
|
|
|
|
[foo]
|
|
|
|
bar = "baz"
|
|
|
|
`;
|
|
|
|
expect(Schema.parse(content)).toEqual({
|
|
|
|
foo: { bar: 'baz' },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws error for invalid schema', () => {
|
|
|
|
const content = codeBlock`
|
|
|
|
[foo]
|
|
|
|
bar = "brb"
|
|
|
|
`;
|
|
|
|
expect(Schema.safeParse(content)).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
received: 'brb',
|
|
|
|
code: 'invalid_literal',
|
|
|
|
expected: 'baz',
|
|
|
|
path: ['foo', 'bar'],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws error for invalid toml', () => {
|
|
|
|
expect(Schema.safeParse('clearly_invalid')).toMatchObject({
|
|
|
|
error: {
|
|
|
|
issues: [
|
|
|
|
{
|
|
|
|
message: 'Invalid TOML',
|
|
|
|
code: 'custom',
|
|
|
|
path: [],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2022-09-27 10:00:35 +00:00
|
|
|
});
|