import { codeBlock } from 'common-tags'; import { z } from 'zod/v4'; import { logger } from '~test/util.ts'; import { DeepNullish, Ini, Json, Json5, Jsonc, LooseArray, LooseRecord, MultidocYaml, NotCircular, Nullish, Toml, UtcDate, Yaml, isEmailAdress, multidocYaml, withDebugMessage, withDepType, withTraceMessage, } from './index.ts'; describe('util/schema-utils/index', () => { describe('LooseArray', () => { it('parses array', () => { const s = LooseArray(z.string()); expect(s.parse(['foo', 'bar'])).toEqual(['foo', 'bar']); }); it('drops wrong items', () => { const s = LooseArray(z.string()); expect(s.parse(['foo', 123, null, undefined, []])).toEqual(['foo']); }); it('runs callback for wrong elements', () => { let err: z.ZodError | undefined = undefined; const Schema = LooseArray(z.string(), { onError: ({ error }) => { err = error; }, }); const res = Schema.parse(['foo', 123, 'bar']); expect(res).toEqual(['foo', 'bar']); expect(err).toMatchObject({ issues: [ { message: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', path: [1], }, ], }); }); }); describe('LooseRecord', () => { it('parses record', () => { const s = LooseRecord(z.string()); expect(s.parse({ foo: 'bar' })).toEqual({ foo: 'bar' }); }); it('drops wrong items', () => { const s = LooseRecord(z.string()); expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' }); }); it('supports key schema', () => { const s = LooseRecord( z .string() .refine((x) => x === 'bar') .transform((x) => x.toUpperCase()), z.string().transform((x) => x.toUpperCase()), ); expect(s.parse({ foo: 'foo', bar: 'bar' })).toEqual({ BAR: 'BAR' }); }); 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' }, }); }); it('runs callback for wrong elements', () => { let err: z.ZodError | undefined = undefined; const Schema = LooseRecord( z.object({ foo: z.object({ bar: z.string() }) }), { onError: ({ error }) => { err = error; }, }, ); const res = Schema.parse({ aaa: { foo: { bar: 42 } }, bbb: { foo: { baz: 'asdf' } }, ccc: { foo: { bar: 'baz' } }, }); expect(res).toEqual({ ccc: { foo: { bar: 'baz' } } }); expect(err).toMatchObject({ issues: [ { message: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', path: ['aaa', 'foo', 'bar'], }, { message: 'Invalid input: expected string, received undefined', code: 'invalid_type', expected: 'string', path: ['bbb', 'foo', 'bar'], }, ], }); }); }); describe('Nullish', () => { it('converts null to undefined', () => { const s = z.object({ a: Nullish(z.string()) }); expect(s.parse({ a: null })).toEqual({}); }); it('converts undefined to undefined (key absent)', () => { const s = z.object({ a: Nullish(z.string()) }); expect(s.parse({})).toEqual({}); }); it('preserves valid string values', () => { const s = z.object({ a: Nullish(z.string()) }); expect(s.parse({ a: 'hello' })).toEqual({ a: 'hello' }); }); it('rejects wrong types', () => { const s = Nullish(z.string()); expect(() => s.parse(42)).toThrow( 'Invalid input: expected string, received number', ); }); }); describe('DeepNullish', () => { describe('optional leaf', () => { const s = DeepNullish(z.object({ a: z.string().optional() })); it.each` input | expected ${{ a: null }} | ${{}} ${{}} | ${{}} ${{ a: 'hello' }} | ${{ a: 'hello' }} `('parses $input', ({ input, expected }) => { expect(s.parse(input)).toEqual(expected); }); it('rejects wrong types', () => { expect(() => s.parse({ a: 42 })).toThrow( 'Invalid input: expected string, received number', ); }); }); it.each` input | expected ${{ outer: { inner: null } }} | ${{ outer: {} }} ${{ outer: null }} | ${{}} `('recurses into nested objects: $input', ({ input, expected }) => { const s = DeepNullish( z.object({ outer: z.object({ inner: z.string().optional() }).optional(), }), ); expect(s.parse(input)).toEqual(expected); }); it('recurses into record valueType', () => { const s = DeepNullish(z.record(z.string(), z.string().optional())); const result = s.parse({ key: null }); expect(result.key).toBeUndefined(); }); it('recurses into array element', () => { const s = DeepNullish(z.array(z.string().optional())); expect(s.parse([null, 'hello'])).toEqual([undefined, 'hello']); }); it.each` input | expected ${{ val: { a: null } }} | ${{ val: {} }} ${{ val: { a: 'x' } }} | ${{ val: { a: 'x' } }} ${{ val: 42 }} | ${{ val: 42 }} `('recurses into union options: $input', ({ input, expected }) => { const s = DeepNullish( z.object({ val: z.union([z.object({ a: z.string().optional() }), z.number()]), }), ); expect(s.parse(input)).toEqual(expected); }); it.each` input | expected ${{}} | ${{ y: false }} ${{ y: null }} | ${{ y: false }} ${{ y: true }} | ${{ y: true }} `('handles default over optional: $input', ({ input, expected }) => { const s = DeepNullish( z.object({ y: z.boolean().optional().default(false) }), ); expect(s.parse(input)).toEqual(expected); }); it.each` input | expected ${{ a: null }} | ${{ a: null }} ${{ a: 'hello' }} | ${{ a: 'hello' }} `( 'preserves nullable fields (null not stripped): $input', ({ input, expected }) => { const s = DeepNullish(z.object({ a: z.nullable(z.string()) })); expect(s.parse(input)).toEqual(expected); }, ); describe('recurses into pipe output side', () => { const s = DeepNullish(Json.pipe(z.object({ a: z.string().optional() }))); it('parses valid input', () => { expect(s.parse('{"a":"x"}')).toEqual({ a: 'x' }); }); it('strips optional inside pipe', () => { expect(s.parse('{"a":null}')).toEqual({}); expect(s.parse('{}')).toEqual({}); }); it('still rejects invalid JSON', () => { expect(s.safeParse('{{{')).toMatchObject({ success: false }); }); }); describe('Loose* schemas remain opaque (closure boundary)', () => { it('LooseArray element optionals are not rewritten, but cleaned up', () => { const s = DeepNullish( LooseArray(z.object({ b: z.string().optional() })), ); expect(s.parse([{ b: null }])).toEqual([]); expect(s.parse([{ b: 'x' }])).toEqual([{ b: 'x' }]); }); it('LooseRecord value optionals are not rewritten, but cleaned up', () => { const s = DeepNullish( LooseRecord(z.object({ b: z.string().optional() })), ); expect(s.parse({ k: { b: null } })).toEqual({}); expect(s.parse({ k: { b: null }, j: { b: '' } })).toEqual({ j: { b: '' }, }); expect(s.parse({ k: { b: 'x' } })).toEqual({ k: { b: 'x' } }); }); }); it('passes through leaf types unchanged', () => { expect(DeepNullish(z.string()).parse('hello')).toBe('hello'); }); it('rejects wrong types on leaf', () => { expect(() => DeepNullish(z.string()).parse(42)).toThrow( 'Invalid input: expected string, received number', ); }); it.each` input | expected ${{ a: null }} | ${{}} ${{ a: 'x' }} | ${{ a: 'x' }} `( 'is idempotent when schema already uses Nullish: $input', ({ input, expected }) => { const s = DeepNullish(z.object({ a: Nullish(z.string()) })); expect(s.parse(input)).toEqual(expected); }, ); }); describe('Json', () => { it('parses json', () => { 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: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected "bar"', code: 'invalid_value', values: ['bar'], path: ['foo'], }, ], }, success: false, }); expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected object, received array', code: 'invalid_type', expected: 'object', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{{{}}}')).toMatchObject({ error: { issues: [ { message: 'Invalid JSON', code: 'custom', path: [], }, ], }, success: false, }); }); }); 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: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected "bar"', code: 'invalid_value', values: ['bar'], path: ['foo'], }, ], }, success: false, }); expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected object, received array', code: 'invalid_type', expected: 'object', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{{{}}}')).toMatchObject({ error: { issues: [ { message: 'Invalid JSON5', code: 'custom', path: [], }, ], }, success: false, }); }); }); describe('Jsonc', () => { it('parses JSONC', () => { const Schema = Jsonc.pipe(z.object({ foo: z.literal('bar') })); expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' }); expect(Schema.safeParse(42)).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected "bar"', code: 'invalid_value', values: ['bar'], path: ['foo'], }, ], }, success: false, }); expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected object, received array', code: 'invalid_type', expected: 'object', path: [], }, ], }, success: false, }); expect(Schema.safeParse('{')).toMatchObject({ error: { issues: [ { message: 'Invalid JSONC', code: 'custom', path: [], }, ], }, success: false, }); }); }); 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('Invalid date'); }); }); 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: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', 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, }); }); }); describe('MultidocYaml', () => { const Schema = MultidocYaml.pipe( z.array( z.object({ foo: z.number(), }), ), ); it('parses valid yaml', () => { expect( Schema.parse(codeBlock` foo: 111 --- foo: 222 `), ).toEqual([{ foo: 111 }, { foo: 222 }]); }); it('throws error for non-string', () => { expect(Schema.safeParse(42)).toMatchObject({ error: { issues: [ { message: 'Invalid input: expected string, received number', code: 'invalid_type', expected: 'string', 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, }); }); }); describe('multidocYaml()', () => { const Schema = multidocYaml().pipe( z.array( z.object({ foo: z.number(), }), ), ); it('parses valid yaml', () => { expect( Schema.parse(codeBlock` foo: 111 --- foo: 222 `), ).toEqual([{ foo: 111 }, { foo: 222 }]); }); it('rejects invalid yaml', () => { expect(multidocYaml().safeParse(': invalid: yaml: {')).toMatchObject({ success: false, error: { issues: [{ code: 'custom', message: 'Invalid YAML' }] }, }); }); }); describe('withDepType()', () => { const Base = z.array( z.object({ depName: z.string(), depType: z.string().optional() }), ); it('sets depType on all deps when force=true (default)', () => { const result = withDepType(Base, 'devDependencies').parse([ { depName: 'foo' }, { depName: 'bar', depType: 'existing' }, ]); expect(result).toEqual([ { depName: 'foo', depType: 'devDependencies' }, { depName: 'bar', depType: 'devDependencies' }, ]); }); it('preserves existing depType when force=false', () => { const result = withDepType(Base, 'devDependencies', false).parse([ { depName: 'foo' }, { depName: 'bar', depType: 'existing' }, ]); expect(result).toEqual([ { depName: 'foo', depType: 'devDependencies' }, { depName: 'bar', depType: 'existing' }, ]); }); }); describe('isEmailAdress()', () => { it('returns true for valid email', () => { expect(isEmailAdress('user@example.com')).toBe(true); }); it('returns false for invalid email', () => { expect(isEmailAdress('not-an-email')).toBe(false); }); it('returns true for GitHub app style email', () => { expect(isEmailAdress('211370388+foo[bot]@users.noreply.github.com')).toBe( true, ); }); }); 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: [ { code: 'invalid_value', values: ['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, }); }); }); describe('Ini', () => { const Schema = Ini.pipe( z.object({ foo: z.object({ bar: z.literal('baz') }) }), ); it('parses valid ini', () => { const content = codeBlock` [foo] bar = "baz" `; expect(Schema.parse(content)).toEqual({ foo: { bar: 'baz' }, }); }); it('throws error for invalid schema', () => { expect(Schema.safeParse('clearly_invalid')).toMatchObject({ error: { issues: [ { code: 'invalid_type', expected: 'object', path: ['foo'], }, ], }, success: false, }); }); }); describe('logging utils', () => { it('logs debug message and returns fallback value', () => { const Str = z .string() .catch(withDebugMessage('default string', 'Debug message')); const result = Str.parse(42); expect(result).toBe('default string'); expect(logger.logger.debug).toHaveBeenCalledWith( { err: expect.objectContaining({ issues: expect.any(Array) }) }, 'Debug message', ); }); it('logs trace message and returns fallback value', () => { const Str = z .string() .catch(withTraceMessage('default string', 'Trace message')); const result = Str.parse(42); expect(result).toBe('default string'); expect(logger.logger.trace).toHaveBeenCalledWith( { err: expect.objectContaining({ issues: expect.any(Array) }) }, 'Trace message', ); }); }); describe('NotCircular', () => { const Schema = NotCircular.pipe(z.any()); it.each` value ${undefined} ${null} ${123} ${'string'} ${true} `('allows non-circular primitive $value', ({ value }) => { expect(Schema.parse(value)).toBe(value); }); it.each` value ${[1, 2, 3]} ${[{ a: 1 }, { b: 2 }]} ${[[1, 2], [3, 4]]} `('allows non-circular array $value', ({ value }) => { expect(Schema.parse(value)).toEqual(value); }); it.each` value ${{ a: 1, b: 2 }} ${{ a: { b: 1 }, c: { d: 2 } }} `('allows non-circular object $value', ({ value }) => { expect(Schema.parse(value)).toEqual(value); }); it('allows objects reuse', () => { const reused = { value: 42 }; const obj = { foo: reused, bar: reused, }; expect(Schema.parse(obj)).toEqual({ foo: { value: 42 }, bar: { value: 42 }, }); }); it('rejects circular objects', () => { const obj: any = { a: 1 }; obj.self = obj; expect(Schema.safeParse(obj)).toMatchObject({ success: false, error: { issues: [ { code: 'custom', message: 'values cannot be circular data structures', path: [], }, ], }, }); }); it('rejects circular arrays', () => { const arr: any[] = [1, 2, 3]; arr.push(arr); expect(Schema.safeParse(arr)).toMatchObject({ success: false, error: { issues: [ { code: 'custom', message: 'values cannot be circular data structures', path: [], }, ], }, }); }); it('rejects deeply nested circular references', () => { const obj: any = { a: { b: { c: { d: {}, }, }, }, }; obj.a.b.c.d.circular = obj.a; expect(Schema.safeParse(obj)).toMatchObject({ success: false, error: { issues: [ { code: 'custom', message: 'values cannot be circular data structures', path: [], }, ], }, }); }); it('can be combined with other schema types', () => { const Obj = z.object({ data: NotCircular.pipe(z.any()), }); expect(Obj.parse({ data: { a: 1, b: 2 } })).toEqual({ data: { a: 1, b: 2 }, }); const obj: any = { a: 1 }; obj.self = obj; expect(Obj.safeParse({ data: obj })).toMatchObject({ success: false, error: { issues: [ { code: 'custom', message: 'values cannot be circular data structures', path: ['data'], }, ], }, }); }); }); });