mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat: Add fork of json-schema-to-zod (no-changelog) (#11228)
This commit is contained in:
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"properties": {
|
||||
"allOf": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"anyOf": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oneOf": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"array": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 3
|
||||
},
|
||||
"tuple": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 3
|
||||
},
|
||||
"const": {
|
||||
"const": "xbox"
|
||||
},
|
||||
"enum": {
|
||||
"enum": ["ps4", "ps5"]
|
||||
},
|
||||
"ifThenElse": {
|
||||
"if": {
|
||||
"type": "string"
|
||||
},
|
||||
"then": {
|
||||
"const": "x"
|
||||
},
|
||||
"else": {
|
||||
"enum": [1, 2, 3]
|
||||
}
|
||||
},
|
||||
"null": {
|
||||
"type": "null"
|
||||
},
|
||||
"multiple": {
|
||||
"type": ["array", "boolean"]
|
||||
},
|
||||
"objAdditionalTrue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"objAdditionalFalse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"objAdditionalNumber": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"objAdditionalOnly": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"patternProps": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^x": {
|
||||
"type": "string"
|
||||
},
|
||||
"^y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"z": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
expect.extend({
|
||||
toMatchZod(this: jest.MatcherContext, actual: z.ZodTypeAny, expected: z.ZodTypeAny) {
|
||||
const actualSerialized = JSON.stringify(actual._def, null, 2);
|
||||
const expectedSerialized = JSON.stringify(expected._def, null, 2);
|
||||
const pass = this.equals(actualSerialized, expectedSerialized);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: pass
|
||||
? () => `Expected ${actualSerialized} not to match ${expectedSerialized}`
|
||||
: () => `Expected ${actualSerialized} to match ${expectedSerialized}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace jest {
|
||||
interface Matchers<R, T> {
|
||||
toMatchZod(expected: unknown): T;
|
||||
}
|
||||
}
|
||||
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { JSONSchema4, JSONSchema6Definition, JSONSchema7Definition } from 'json-schema';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { jsonSchemaToZod } from '../src';
|
||||
|
||||
describe('jsonSchemaToZod', () => {
|
||||
test('should accept json schema 7 and 4', () => {
|
||||
const schema = { type: 'string' } as unknown;
|
||||
|
||||
expect(jsonSchemaToZod(schema as JSONSchema4));
|
||||
expect(jsonSchemaToZod(schema as JSONSchema6Definition));
|
||||
expect(jsonSchemaToZod(schema as JSONSchema7Definition));
|
||||
});
|
||||
|
||||
test('can exclude defaults', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
type: 'string',
|
||||
default: 'foo',
|
||||
},
|
||||
{ withoutDefaults: true },
|
||||
),
|
||||
).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('should include describes', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'string',
|
||||
description: 'foo',
|
||||
}),
|
||||
).toMatchZod(z.string().describe('foo'));
|
||||
});
|
||||
|
||||
test('can exclude describes', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
type: 'string',
|
||||
description: 'foo',
|
||||
},
|
||||
{
|
||||
withoutDescribes: true,
|
||||
},
|
||||
),
|
||||
).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('will remove optionality if default is present', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'object',
|
||||
properties: {
|
||||
prop: {
|
||||
type: 'string',
|
||||
default: 'def',
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.object({ prop: z.string().default('def') }));
|
||||
});
|
||||
|
||||
test('will handle falsy defaults', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
).toMatchZod(z.boolean().default(false));
|
||||
});
|
||||
|
||||
test('will ignore undefined as default', () => {
|
||||
expect(
|
||||
jsonSchemaToZod({
|
||||
type: 'null',
|
||||
default: undefined,
|
||||
}),
|
||||
).toMatchZod(z.null());
|
||||
});
|
||||
|
||||
test('should be possible to define a custom parser', () => {
|
||||
expect(
|
||||
jsonSchemaToZod(
|
||||
{
|
||||
allOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean', description: 'foo' }],
|
||||
},
|
||||
{
|
||||
parserOverride: (schema, refs) => {
|
||||
if (
|
||||
refs.path.length === 2 &&
|
||||
refs.path[0] === 'allOf' &&
|
||||
refs.path[1] === 2 &&
|
||||
schema.type === 'boolean' &&
|
||||
schema.description === 'foo'
|
||||
) {
|
||||
return z.null();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
),
|
||||
).toMatchZod(z.intersection(z.string(), z.intersection(z.number(), z.null())));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseAllOf } from '../../src/parsers/parse-all-of';
|
||||
|
||||
describe('parseAllOf', () => {
|
||||
test('should create never if empty', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.never());
|
||||
});
|
||||
|
||||
test('should handle true values', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [{ type: 'string' }, true],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.intersection(z.string(), z.any()));
|
||||
});
|
||||
|
||||
test('should handle false values', () => {
|
||||
expect(
|
||||
parseAllOf(
|
||||
{
|
||||
allOf: [{ type: 'string' }, false],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.intersection(
|
||||
z.string(),
|
||||
z
|
||||
.any()
|
||||
.refine(
|
||||
(value) => !z.any().safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseAnyOf } from '../../src/parsers/parse-any-of';
|
||||
|
||||
describe('parseAnyOf', () => {
|
||||
test('should create a union from two or more schemas', () => {
|
||||
expect(
|
||||
parseAnyOf(
|
||||
{
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.union([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should extract a single schema', () => {
|
||||
expect(parseAnyOf({ anyOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||
z.string(),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return z.any() if array is empty', () => {
|
||||
expect(parseAnyOf({ anyOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseArray } from '../../src/parsers/parse-array';
|
||||
|
||||
describe('parseArray', () => {
|
||||
test('should create tuple with items array', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.tuple([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should create array with items object', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()));
|
||||
});
|
||||
|
||||
test('should create min for minItems', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
minItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()).min(2));
|
||||
});
|
||||
|
||||
test('should create max for maxItems', () => {
|
||||
expect(
|
||||
parseArray(
|
||||
{
|
||||
type: 'array',
|
||||
maxItems: 2,
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.array(z.string()).max(2));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseConst } from '../../src/parsers/parse-const';
|
||||
|
||||
describe('parseConst', () => {
|
||||
test('should handle falsy constants', () => {
|
||||
expect(
|
||||
parseConst({
|
||||
const: false,
|
||||
}),
|
||||
).toMatchZod(z.literal(false));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseEnum } from '../../src/parsers/parse-enum';
|
||||
|
||||
describe('parseEnum', () => {
|
||||
test('should create never with empty enum', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: [],
|
||||
}),
|
||||
).toMatchZod(z.never());
|
||||
});
|
||||
|
||||
test('should create literal with single item enum', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue'],
|
||||
}),
|
||||
).toMatchZod(z.literal('someValue'));
|
||||
});
|
||||
|
||||
test('should create enum array with string enums', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue', 'anotherValue'],
|
||||
}),
|
||||
).toMatchZod(z.enum(['someValue', 'anotherValue']));
|
||||
});
|
||||
test('should create union with mixed enums', () => {
|
||||
expect(
|
||||
parseEnum({
|
||||
enum: ['someValue', 57],
|
||||
}),
|
||||
).toMatchZod(z.union([z.literal('someValue'), z.literal(57)]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseNot } from '../../src/parsers/parse-not';
|
||||
|
||||
describe('parseNot', () => {
|
||||
test('parseNot', () => {
|
||||
expect(
|
||||
parseNot(
|
||||
{
|
||||
not: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.any()
|
||||
.refine(
|
||||
(value) => !z.string().safeParse(value).success,
|
||||
'Invalid input: Should NOT be valid against schema',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||
|
||||
describe('parseNullable', () => {
|
||||
test('parseSchema should not add default twice', () => {
|
||||
expect(
|
||||
parseSchema(
|
||||
{
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.string().nullable().default(null));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseNumber } from '../../src/parsers/parse-number';
|
||||
|
||||
describe('parseNumber', () => {
|
||||
test('should handle integer', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'integer',
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'integer',
|
||||
multipleOf: 1,
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
multipleOf: 1,
|
||||
}),
|
||||
).toMatchZod(z.number().int());
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMinimum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMinimum: true,
|
||||
minimum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().gt(2));
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMinimum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
minimum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().gte(2));
|
||||
});
|
||||
|
||||
test('should handle maximum with exclusiveMaximum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMaximum: true,
|
||||
maximum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().lt(2));
|
||||
});
|
||||
|
||||
test('should handle numeric exclusiveMaximum', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
exclusiveMaximum: 2,
|
||||
}),
|
||||
).toMatchZod(z.number().lt(2));
|
||||
});
|
||||
|
||||
test('should accept errorMessage', () => {
|
||||
expect(
|
||||
parseNumber({
|
||||
type: 'number',
|
||||
format: 'int64',
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 2,
|
||||
multipleOf: 2,
|
||||
errorMessage: {
|
||||
format: 'ayy',
|
||||
multipleOf: 'lmao',
|
||||
exclusiveMinimum: 'deez',
|
||||
maximum: 'nuts',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.number().int('ayy').multipleOf(2, 'lmao').gt(0, 'deez').lte(2, 'nuts'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,904 @@
|
||||
/* eslint-disable n8n-local-rules/no-skipped-tests */
|
||||
import type { JSONSchema7 } from 'json-schema';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import { parseObject } from '../../src/parsers/parse-object';
|
||||
|
||||
describe('parseObject', () => {
|
||||
test('should handle with missing properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.any()));
|
||||
});
|
||||
|
||||
test('should handle with empty properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({}));
|
||||
});
|
||||
|
||||
test('With properties - should handle optional and required properties', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myRequiredString'],
|
||||
properties: {
|
||||
myOptionalString: {
|
||||
type: 'string',
|
||||
},
|
||||
myRequiredString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ myOptionalString: z.string().optional(), myRequiredString: z.string() }),
|
||||
);
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when set to false', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).strict());
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when set to true', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).catchall(z.any()));
|
||||
});
|
||||
|
||||
test('With properties - should handle additionalProperties when provided a schema', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['myString'],
|
||||
properties: {
|
||||
myString: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'number' },
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ myString: z.string() }).catchall(z.number()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when set to false', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.never()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when set to true', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.any()));
|
||||
});
|
||||
|
||||
test('Without properties - should handle additionalProperties when provided a schema', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'number' },
|
||||
},
|
||||
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.record(z.number()));
|
||||
});
|
||||
|
||||
test('Without properties - should include falsy defaults', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
s: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ s: z.string().default('') }));
|
||||
});
|
||||
|
||||
test('eh', () => {
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.object({ a: z.string() })
|
||||
.and(z.union([z.object({ b: z.string() }), z.object({ c: z.string() })])),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.object({ a: z.string() }).and(z.union([z.object({ b: z.string() }), z.any()])));
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.object({ b: z.string() }), z.object({ c: z.string() })];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||
schema.safeParse(x),
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.object({ b: z.string() }), z.any()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||
schema.safeParse(x),
|
||||
),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
required: ['c'],
|
||||
properties: {
|
||||
c: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z
|
||||
.object({ a: z.string() })
|
||||
.and(z.intersection(z.object({ b: z.string() }), z.object({ c: z.string() }))),
|
||||
);
|
||||
|
||||
expect(
|
||||
parseObject(
|
||||
{
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
allOf: [
|
||||
{
|
||||
required: ['b'],
|
||||
properties: {
|
||||
b: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.object({ a: z.string() }).and(z.intersection(z.object({ b: z.string() }), z.any())),
|
||||
);
|
||||
});
|
||||
|
||||
const run = (zodSchema: z.ZodTypeAny, data: unknown) => zodSchema.safeParse(data);
|
||||
|
||||
test('Functional tests - run', () => {
|
||||
expect(run(z.string(), 'hello')).toEqual({
|
||||
success: true,
|
||||
data: 'hello',
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z.object({ a: z.string(), b: z.number().optional() });
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { a: 'hello' })).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
a: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
expect(run(result, { a: 'hello', b: 123 })).toEqual({
|
||||
success: true,
|
||||
data: {
|
||||
a: 'hello',
|
||||
b: 123,
|
||||
},
|
||||
});
|
||||
|
||||
expect(run(result, { b: 'hello', x: true })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'string',
|
||||
received: 'undefined',
|
||||
path: ['a'],
|
||||
message: 'Required',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'number',
|
||||
received: 'string',
|
||||
path: ['b'],
|
||||
message: 'Expected number, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties and additionalProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'boolean' },
|
||||
};
|
||||
|
||||
const expected = z.object({ a: z.string(), b: z.number().optional() }).catchall(z.boolean());
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { b: 'hello', x: 'true' })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'string',
|
||||
received: 'undefined',
|
||||
path: ['a'],
|
||||
message: 'Required',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'number',
|
||||
received: 'string',
|
||||
path: ['b'],
|
||||
message: 'Expected number, received string',
|
||||
},
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'boolean',
|
||||
received: 'string',
|
||||
path: ['x'],
|
||||
message: 'Expected boolean, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties and single-item patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.array(z.any()))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { a: 'a', b: 2, '.': [] })).toEqual({
|
||||
success: true,
|
||||
data: { a: 'a', b: 2, '.': [] },
|
||||
});
|
||||
|
||||
expect(run(result, { a: 'a', b: 2, '.': '[]' })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
code: 'invalid_type',
|
||||
expected: 'array',
|
||||
received: 'string',
|
||||
path: ['.'],
|
||||
message: 'Expected array, received string',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - properties, additionalProperties and patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
additionalProperties: { type: 'boolean' },
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
let evaluated = ['a', 'b'].includes(key);
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\\\,'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evaluated) {
|
||||
const result = z.boolean().safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: must match catchall schema',
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - additionalProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' },
|
||||
};
|
||||
|
||||
const expected = z.record(z.boolean());
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - additionalProperties and patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' },
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.record(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
let evaluated = false;
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\\\,'))) {
|
||||
evaluated = true;
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evaluated) {
|
||||
const result = z.boolean().safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: must match catchall schema',
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
|
||||
expect(run(result, { x: true, '.': [], ',': [] })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
path: [','],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||
params: {
|
||||
issues: [
|
||||
{
|
||||
code: 'too_small',
|
||||
minimum: 1,
|
||||
type: 'array',
|
||||
inclusive: true,
|
||||
exact: false,
|
||||
message: 'Array must contain at least 1 element(s)',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
test('Functional tests - single-item patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z.record(z.array(z.any())).superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - patternProperties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.record(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\,'))) {
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(run(result, { '.': [] })).toEqual({
|
||||
success: true,
|
||||
data: { '.': [] },
|
||||
});
|
||||
|
||||
expect(run(result, { ',': [] })).toEqual({
|
||||
success: false,
|
||||
error: new ZodError([
|
||||
{
|
||||
path: [','],
|
||||
code: 'custom',
|
||||
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||
params: {
|
||||
issues: [
|
||||
{
|
||||
code: 'too_small',
|
||||
minimum: 1,
|
||||
type: 'array',
|
||||
inclusive: true,
|
||||
exact: false,
|
||||
message: 'Array must contain at least 1 element(s)',
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
|
||||
test('Functional tests - patternProperties and properties', () => {
|
||||
const schema: JSONSchema7 & { type: 'object' } = {
|
||||
type: 'object',
|
||||
required: ['a'],
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
patternProperties: {
|
||||
'\\.': { type: 'array' },
|
||||
'\\,': { type: 'array', minItems: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const expected = z
|
||||
.object({ a: z.string(), b: z.number().optional() })
|
||||
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||
.superRefine((value, ctx) => {
|
||||
for (const key in value) {
|
||||
if (key.match(new RegExp('\\.'))) {
|
||||
const result = z.array(z.any()).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (key.match(new RegExp('\\,'))) {
|
||||
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||
if (!result.success) {
|
||||
ctx.addIssue({
|
||||
path: [...ctx.path, key],
|
||||
code: 'custom',
|
||||
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||
params: {
|
||||
issues: result.error.issues,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||
|
||||
expect(result).toMatchZod(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseOneOf } from '../../src/parsers/parse-one-of';
|
||||
|
||||
describe('parseOneOf', () => {
|
||||
test('should create a union from two or more schemas', () => {
|
||||
expect(
|
||||
parseOneOf(
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.string(), z.number()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should extract a single schema', () => {
|
||||
expect(parseOneOf({ oneOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||
z.string(),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return z.any() if array is empty', () => {
|
||||
expect(parseOneOf({ oneOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||
|
||||
describe('parseSchema', () => {
|
||||
test('should be usable without providing refs', () => {
|
||||
expect(parseSchema({ type: 'string' })).toMatchZod(z.string());
|
||||
});
|
||||
|
||||
test('should return a seen and processed ref', () => {
|
||||
const seen = new Map();
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prop: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(parseSchema(schema, { seen, path: [] }));
|
||||
expect(parseSchema(schema, { seen, path: [] }));
|
||||
});
|
||||
|
||||
test('should be possible to describe a readonly schema', () => {
|
||||
expect(parseSchema({ type: 'string', readOnly: true })).toMatchZod(z.string().readonly());
|
||||
});
|
||||
|
||||
test('should handle nullable', () => {
|
||||
expect(
|
||||
parseSchema(
|
||||
{
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
{ path: [], seen: new Map() },
|
||||
),
|
||||
).toMatchZod(z.string().nullable());
|
||||
});
|
||||
|
||||
test('should handle enum', () => {
|
||||
expect(parseSchema({ enum: ['someValue', 57] })).toMatchZod(
|
||||
z.union([z.literal('someValue'), z.literal(57)]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple type', () => {
|
||||
expect(parseSchema({ type: ['string', 'number'] })).toMatchZod(
|
||||
z.union([z.string(), z.number()]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle if-then-else type', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
if: { type: 'string' },
|
||||
then: { type: 'number' },
|
||||
else: { type: 'boolean' },
|
||||
}),
|
||||
).toMatchZod(
|
||||
z.union([z.number(), z.boolean()]).superRefine((value, ctx) => {
|
||||
const result = z.string().safeParse(value).success
|
||||
? z.number().safeParse(value)
|
||||
: z.boolean().safeParse(value);
|
||||
if (!result.success) {
|
||||
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle anyOf', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
}),
|
||||
).toMatchZod(z.union([z.string(), z.number()]));
|
||||
});
|
||||
|
||||
test('should handle oneOf', () => {
|
||||
expect(
|
||||
parseSchema({
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{ type: 'number' },
|
||||
],
|
||||
}),
|
||||
).toMatchZod(
|
||||
z.any().superRefine((x, ctx) => {
|
||||
const schemas = [z.string(), z.number()];
|
||||
const errors = schemas.reduce<z.ZodError[]>(
|
||||
(errors, schema) =>
|
||||
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||
[],
|
||||
);
|
||||
if (schemas.length - errors.length !== 1) {
|
||||
ctx.addIssue({
|
||||
path: ctx.path,
|
||||
code: 'invalid_union',
|
||||
unionErrors: errors,
|
||||
message: 'Invalid input: Should pass single schema',
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseString } from '../../src/parsers/parse-string';
|
||||
|
||||
describe('parseString', () => {
|
||||
const run = (schema: z.ZodString, data: unknown) => schema.safeParse(data);
|
||||
|
||||
test('DateTime format', () => {
|
||||
const datetime = '2018-11-13T20:20:39Z';
|
||||
|
||||
const code = parseString({
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
errorMessage: { format: 'hello' },
|
||||
});
|
||||
|
||||
expect(code).toMatchZod(z.string().datetime({ offset: true, message: 'hello' }));
|
||||
|
||||
expect(run(code, datetime)).toEqual({ success: true, data: datetime });
|
||||
});
|
||||
|
||||
test('email', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
}),
|
||||
).toMatchZod(z.string().email());
|
||||
});
|
||||
|
||||
test('ip', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ip',
|
||||
}),
|
||||
).toMatchZod(z.string().ip());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ipv6',
|
||||
}),
|
||||
).toMatchZod(z.string().ip({ version: 'v6' }));
|
||||
});
|
||||
|
||||
test('uri', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
}),
|
||||
).toMatchZod(z.string().url());
|
||||
});
|
||||
|
||||
test('uuid', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
}),
|
||||
).toMatchZod(z.string().uuid());
|
||||
});
|
||||
|
||||
test('time', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'time',
|
||||
}),
|
||||
).toMatchZod(z.string().time());
|
||||
});
|
||||
|
||||
test('date', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
}),
|
||||
).toMatchZod(z.string().date());
|
||||
});
|
||||
|
||||
test('duration', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'duration',
|
||||
}),
|
||||
).toMatchZod(z.string().duration());
|
||||
});
|
||||
|
||||
test('base64', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
contentEncoding: 'base64',
|
||||
}),
|
||||
).toMatchZod(z.string().base64());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
contentEncoding: 'base64',
|
||||
errorMessage: {
|
||||
contentEncoding: 'x',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.string().base64('x'));
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
}),
|
||||
).toMatchZod(z.string().base64());
|
||||
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
errorMessage: {
|
||||
format: 'x',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(z.string().base64('x'));
|
||||
});
|
||||
|
||||
test('should accept errorMessage', () => {
|
||||
expect(
|
||||
parseString({
|
||||
type: 'string',
|
||||
format: 'ipv4',
|
||||
pattern: 'x',
|
||||
minLength: 1,
|
||||
maxLength: 2,
|
||||
errorMessage: {
|
||||
format: 'ayy',
|
||||
pattern: 'lmao',
|
||||
minLength: 'deez',
|
||||
maxLength: 'nuts',
|
||||
},
|
||||
}),
|
||||
).toMatchZod(
|
||||
z
|
||||
.string()
|
||||
.ip({ version: 'v4', message: 'ayy' })
|
||||
.regex(new RegExp('x'), 'lmao')
|
||||
.min(1, 'deez')
|
||||
.max(2, 'nuts'),
|
||||
);
|
||||
});
|
||||
});
|
||||
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { half } from '../../src/utils/half';
|
||||
|
||||
describe('half', () => {
|
||||
test('half', () => {
|
||||
const [a, b] = half(['A', 'B', 'C', 'D', 'E']);
|
||||
|
||||
if (1 < 0) {
|
||||
// type should be string
|
||||
a[0].endsWith('');
|
||||
}
|
||||
|
||||
expect(a).toEqual(['A', 'B']);
|
||||
expect(b).toEqual(['C', 'D', 'E']);
|
||||
});
|
||||
});
|
||||
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { omit } from '../../src/utils/omit';
|
||||
|
||||
describe('omit', () => {
|
||||
test('omit', () => {
|
||||
const input = {
|
||||
a: true,
|
||||
b: true,
|
||||
};
|
||||
|
||||
omit(
|
||||
input,
|
||||
'b',
|
||||
// @ts-expect-error
|
||||
'c',
|
||||
);
|
||||
|
||||
const output = omit(input, 'b');
|
||||
|
||||
// @ts-expect-error
|
||||
output.b;
|
||||
|
||||
expect(output.a).toBe(true);
|
||||
|
||||
// @ts-expect-error
|
||||
expect(output.b).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user