From 28d02963ab411eb5912bc26ffd4bd29d249be26d Mon Sep 17 00:00:00 2001 From: MH4GF Date: Wed, 29 May 2024 23:33:22 +0900 Subject: [PATCH 01/20] chore: add valibot to devDependencies --- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/package.json b/package.json index f2814210..41de0008 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "ts-dedent": "^2.2.0", "ts-jest": "29.1.4", "typescript": "5.4.5", + "valibot": "0.31.0-rc.6", "vitest": "^1.0.0", "yup": "1.4.0", "zod": "3.23.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14d062fb..54fcb084 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: typescript: specifier: 5.4.5 version: 5.4.5 + valibot: + specifier: 0.31.0-rc.6 + version: 0.31.0-rc.6 vitest: specifier: ^1.0.0 version: 1.6.0(@types/node@20.12.12) @@ -3698,6 +3701,9 @@ packages: resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} engines: {node: '>=10.12.0'} + valibot@0.31.0-rc.6: + resolution: {integrity: sha512-NW4mnZsSyLCj2TweTPBuo7jzhZywh3C2M0T5UU53po2jhg/0k7B63pQTt3hkcdZl2JkSZEYHliKzO/2OsLFqlQ==} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -8351,6 +8357,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 + valibot@0.31.0-rc.6: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 From fb5c8e00f15b3fb4ee1db37cda08faefbe8a47c0 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Wed, 29 May 2024 23:52:21 +0900 Subject: [PATCH 02/20] feat: support non-null and defined --- src/config.ts | 2 +- src/index.ts | 3 ++ src/valibot/index.ts | 109 ++++++++++++++++++++++++++++++++++++++++++ tests/valibot.spec.ts | 34 +++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/valibot/index.ts create mode 100644 tests/valibot.spec.ts diff --git a/src/config.ts b/src/config.ts index 6475f9fc..ca6d7502 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript'; -export type ValidationSchema = 'yup' | 'zod' | 'myzod'; +export type ValidationSchema = 'yup' | 'zod' | 'myzod' | 'valibot'; export type ValidationSchemaExportType = 'function' | 'const'; export interface DirectiveConfig { diff --git a/src/index.ts b/src/index.ts index 4bd6008a..22497a6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { MyZodSchemaVisitor } from './myzod/index'; import type { SchemaVisitor } from './types'; import { YupSchemaVisitor } from './yup/index'; import { ZodSchemaVisitor } from './zod/index'; +import { ValibotSchemaVisitor } from './valibot'; export const plugin: PluginFunction = ( schema: GraphQLSchema, @@ -33,6 +34,8 @@ function schemaVisitor(schema: GraphQLSchema, config: ValidationSchemaPluginConf return new ZodSchemaVisitor(schema, config); else if (config?.schema === 'myzod') return new MyZodSchemaVisitor(schema, config); + else if (config?.schema === 'valibot') + return new ValibotSchemaVisitor(schema, config); return new YupSchemaVisitor(schema, config); } diff --git a/src/valibot/index.ts b/src/valibot/index.ts new file mode 100644 index 00000000..adf7c2a8 --- /dev/null +++ b/src/valibot/index.ts @@ -0,0 +1,109 @@ +import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; +import type { + FieldDefinitionNode, + GraphQLSchema, + InputObjectTypeDefinitionNode, + InputValueDefinitionNode, + NameNode, + TypeNode, +} from 'graphql'; + +import type { ValidationSchemaPluginConfig } from '../config'; +import { BaseSchemaVisitor } from '../schema_visitor'; +import type { Visitor } from '../visitor'; +import { + isNamedType, + isNonNullType, +} from './../graphql'; + +export class ValibotSchemaVisitor extends BaseSchemaVisitor { + constructor(schema: GraphQLSchema, config: ValidationSchemaPluginConfig) { + super(schema, config); + } + + importValidationSchema(): string { + return `import * as v from 'valibot'`; + } + + initialEmit(): string { + return ''; + } + + get InputObjectTypeDefinition() { + return { + leave: (node: InputObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('input'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + return this.buildInputFields(node.fields ?? [], visitor, name); + }, + }; + } + + protected buildInputFields( + fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], + visitor: Visitor, + name: string, + ) { + const shape = fields.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string; + } + } +} + +function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, indentCount: number): string { + const gen = generateFieldTypeValibotSchema(config, visitor, field, field.type); + return indent(`${field.name.value}: ${maybeLazy(field.type, gen)}`, indentCount); +} + +function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { + if (isNonNullType(type)) { + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); + return maybeLazy(type.type, gen); + } + if (isNamedType(type)) { + const gen = generateNameNodeValibotSchema(config, visitor, type.name); + + if (isNonNullType(parentType)) + return gen; + } + console.warn('unhandled type:', type); + return ''; +} + +function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { + const converter = visitor.getNameNodeConverter(node); + + switch (converter?.targetKind) { + default: + if (converter?.targetKind) + console.warn('Unknown targetKind', converter?.targetKind); + + return valibot4Scalar(config, visitor, node.value); + } +} + +function maybeLazy(type: TypeNode, schema: string): string { + return schema; +} + +function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { + const tsType = visitor.getScalarType(scalarName); + switch (tsType) { + case 'string': + return `v.string()`; + case 'number': + return `v.number()`; + case 'boolean': + return `v.boolean()`; + } + console.warn('unhandled scalar name:', scalarName); + return 'v.any()'; +} diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts new file mode 100644 index 00000000..d7015415 --- /dev/null +++ b/tests/valibot.spec.ts @@ -0,0 +1,34 @@ +import { buildSchema } from 'graphql'; + +import { plugin } from '../src/index'; + +describe('valibot', () => { + it('non-null and defined', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID! + b: String! + c: Boolean! + d: Int! + e: Float! + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.string(), + b: v.string(), + c: v.boolean(), + d: v.number(), + e: v.number() + }) + } + " + `); + }) +}) From 6cead63b7a0409ce4d9ca530f06b61dc4b2c3b5b Mon Sep 17 00:00:00 2001 From: MH4GF Date: Wed, 29 May 2024 23:55:55 +0900 Subject: [PATCH 03/20] feat: support nullish --- src/valibot/index.ts | 2 ++ tests/valibot.spec.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index adf7c2a8..e6b206a5 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -73,6 +73,8 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isNonNullType(parentType)) return gen; + + return `v.nullish(${gen})`; } console.warn('unhandled type:', type); return ''; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index d7015415..9661bc06 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -31,4 +31,34 @@ describe('valibot', () => { " `); }) + it('nullish', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: ID + b: String + c: Boolean + d: Int + e: Float + z: String! # no defined check + } + `); + const scalars = { + ID: 'string', + } + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.nullish(v.string()), + c: v.nullish(v.boolean()), + d: v.nullish(v.number()), + e: v.nullish(v.number()), + z: v.string() + }) + } + " + `); + }) }) From df636049ad4e2b1e5a16f5dfda6219409ecbf0e5 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 00:00:13 +0900 Subject: [PATCH 04/20] feat: support array --- src/valibot/index.ts | 11 +++++++++++ tests/valibot.spec.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index e6b206a5..b561f178 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -12,6 +12,7 @@ import type { ValidationSchemaPluginConfig } from '../config'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { + isListType, isNamedType, isNonNullType, } from './../graphql'; @@ -64,12 +65,22 @@ function generateFieldValibotSchema(config: ValidationSchemaPluginConfig, visito } function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, field: InputValueDefinitionNode | FieldDefinitionNode, type: TypeNode, parentType?: TypeNode): string { + if (isListType(type)) { + const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); + const arrayGen = `v.array(${maybeLazy(type.type, gen)})`; + if (!isNonNullType(parentType)) { + return `v.nullish(${arrayGen})`; + } + return arrayGen; + } if (isNonNullType(type)) { const gen = generateFieldTypeValibotSchema(config, visitor, field, type.type, type); return maybeLazy(type.type, gen); } if (isNamedType(type)) { const gen = generateNameNodeValibotSchema(config, visitor, type.name); + if (isListType(parentType)) + return `v.nullable(${gen})`; if (isNonNullType(parentType)) return gen; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 9661bc06..0aa96bed 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -61,4 +61,32 @@ describe('valibot', () => { " `); }) + it('array', async () => { + const schema = buildSchema(/* GraphQL */ ` + input PrimitiveInput { + a: [String] + b: [String!] + c: [String!]! + d: [[String]] + e: [[String]!] + f: [[String]!]! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export function PrimitiveInputSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.array(v.nullable(v.string()))), + b: v.nullish(v.array(v.string())), + c: v.array(v.string()), + d: v.nullish(v.array(v.nullish(v.array(v.nullable(v.string()))))), + e: v.nullish(v.array(v.array(v.nullable(v.string())))), + f: v.array(v.array(v.nullable(v.string()))) + }) + } + " + `); + }) }) From 657ee60549454bdf4742298d676d99128be14d45 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 00:05:32 +0900 Subject: [PATCH 05/20] feat: support ref input --- src/valibot/index.ts | 10 ++++++++++ tests/valibot.spec.ts | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index b561f178..8e397498 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -12,6 +12,7 @@ import type { ValidationSchemaPluginConfig } from '../config'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { + isInput, isListType, isNamedType, isNonNullType, @@ -95,6 +96,12 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InputObjectTypeDefinition': + // using switch-case rather than if-else to allow for future expansion + switch (config.validationSchemaExportType) { + default: + return `${converter.convertName()}Schema()`; + } default: if (converter?.targetKind) console.warn('Unknown targetKind', converter?.targetKind); @@ -104,6 +111,9 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis } function maybeLazy(type: TypeNode, schema: string): string { + if (isNamedType(type) && isInput(type.name.value)) + return `v.lazy(() => ${schema})`; + return schema; } diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 0aa96bed..2d35a376 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -89,4 +89,40 @@ describe('valibot', () => { " `); }) -}) + it('ref input object', async () => { + const schema = buildSchema(/* GraphQL */ ` + input AInput { + b: BInput! + } + input BInput { + c: CInput! + } + input CInput { + a: AInput! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export function AInputSchema(): v.GenericSchema { + return v.object({ + b: v.lazy(() => BInputSchema()) + }) + } + + export function BInputSchema(): v.GenericSchema { + return v.object({ + c: v.lazy(() => CInputSchema()) + }) + } + + export function CInputSchema(): v.GenericSchema { + return v.object({ + a: v.lazy(() => AInputSchema()) + }) + } + " + `); + }) +}) \ No newline at end of file From 0e81efc2293b394cd5646889a4350763d16fa125 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:15:16 +0900 Subject: [PATCH 06/20] feat: support enum --- src/valibot/index.ts | 28 +++++++++++++++++++++++++++- tests/valibot.spec.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 8e397498..118b8448 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -1,5 +1,6 @@ import { DeclarationBlock, indent } from '@graphql-codegen/visitor-plugin-common'; import type { + EnumTypeDefinitionNode, FieldDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, @@ -28,7 +29,11 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { } initialEmit(): string { - return ''; + return ( + `\n${[ + ...this.enumDeclarations, + ].join('\n')}` + ); } get InputObjectTypeDefinition() { @@ -42,6 +47,25 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { }; } + get EnumTypeDefinition() { + return { + leave: (node: EnumTypeDefinitionNode) => { + const visitor = this.createVisitor('both'); + const enumname = visitor.convertName(node.name.value); + this.importTypes.push(enumname); + + // hoist enum declarations + this.enumDeclarations.push( + new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.enum_(${enumname})`).string, + ); + }, + }; + } + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, @@ -102,6 +126,8 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis default: return `${converter.convertName()}Schema()`; } + case 'EnumTypeDefinition': + return `${converter.convertName()}Schema`; default: if (converter?.targetKind) console.warn('Unknown targetKind', converter?.targetKind); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 2d35a376..c93d9242 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -19,6 +19,7 @@ describe('valibot', () => { const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); expect(result.content).toMatchInlineSnapshot(` " + export function PrimitiveInputSchema(): v.GenericSchema { return v.object({ a: v.string(), @@ -48,6 +49,7 @@ describe('valibot', () => { const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); expect(result.content).toMatchInlineSnapshot(` " + export function PrimitiveInputSchema(): v.GenericSchema { return v.object({ a: v.nullish(v.string()), @@ -76,6 +78,7 @@ describe('valibot', () => { const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); expect(result.content).toMatchInlineSnapshot(` " + export function PrimitiveInputSchema(): v.GenericSchema { return v.object({ a: v.nullish(v.array(v.nullable(v.string()))), @@ -105,6 +108,7 @@ describe('valibot', () => { const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); expect(result.content).toMatchInlineSnapshot(` " + export function AInputSchema(): v.GenericSchema { return v.object({ b: v.lazy(() => BInputSchema()) @@ -125,4 +129,29 @@ describe('valibot', () => { " `); }) -}) \ No newline at end of file + it.todo('nested input object') + it('enum', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + input PageInput { + pageType: PageType! + } + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(PageType); + + export function PageInputSchema(): v.GenericSchema { + return v.object({ + pageType: PageTypeSchema + }) + } + " + `); + }) +}) From 998de8817cfa035c7218e025ee4b1af23fd95365 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:19:58 +0900 Subject: [PATCH 07/20] test: add test for camelcase --- tests/valibot.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index c93d9242..dc63b6aa 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -154,4 +154,31 @@ describe('valibot', () => { " `); }) + it('camelcase', async () => { + const schema = buildSchema(/* GraphQL */ ` + input HTTPInput { + method: HTTPMethod + url: URL! + } + enum HTTPMethod { + GET + POST + } + scalar URL # unknown scalar, should be any + `); + const scalars = undefined + const result = await plugin(schema, [], { schema: 'valibot', scalars }, {}); + expect(result.content).toMatchInlineSnapshot(` + " + export const HttpMethodSchema = v.enum_(HttpMethod); + + export function HttpInputSchema(): v.GenericSchema { + return v.object({ + method: v.nullish(HttpMethodSchema), + url: v.any() + }) + } + " + `); + }) }) From 1bc9066295bc866d78f2cd5186d82ecf7046ef8e Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:21:06 +0900 Subject: [PATCH 08/20] test: add test for scalars --- tests/valibot.spec.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index dc63b6aa..4dcbfdcb 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -181,4 +181,37 @@ describe('valibot', () => { " `); }) + it('with scalars', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: Text! + times: Count! + } + scalar Count + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + scalars: { + Text: 'string', + Count: 'number', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string(), + times: v.number() + }) + } + " + `); + }); }) From 2484a0602a9af75dc39ef37218ddacab68dc8250 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:23:28 +0900 Subject: [PATCH 09/20] test: add test for importFrom --- tests/valibot.spec.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 4dcbfdcb..b0628c44 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -214,4 +214,36 @@ describe('valibot', () => { " `); }); + it('with importFrom', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as v from 'valibot'", + "import { Say } from './types'", + ] + `); + expect(result.content).toMatchInlineSnapshot(` + " + + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string() + }) + } + " + `); + }); }) From 5a567db1df13ecad543ad0436a6988971c17c05d Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:24:45 +0900 Subject: [PATCH 10/20] test: add test with importFrom & useTypeImports --- tests/valibot.spec.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index b0628c44..a47080ba 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -238,6 +238,39 @@ describe('valibot', () => { expect(result.content).toMatchInlineSnapshot(` " + export function SaySchema(): v.GenericSchema { + return v.object({ + phrase: v.string() + }) + } + " + `); + }); + it('with importFrom & useTypeImports', async () => { + const schema = buildSchema(/* GraphQL */ ` + input Say { + phrase: String! + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + importFrom: './types', + useTypeImports: true, + }, + {}, + ); + expect(result.prepend).toMatchInlineSnapshot(` + [ + "import * as v from 'valibot'", + "import type { Say } from './types'", + ] + `); + expect(result.content).toMatchInlineSnapshot(` + " + export function SaySchema(): v.GenericSchema { return v.object({ phrase: v.string() From c8e7c07fb0010e3059aeece922a2ac092ec5cded Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:28:03 +0900 Subject: [PATCH 11/20] feat: support for enumsAsTypes --- src/valibot/index.ts | 17 ++++++++++++----- tests/valibot.spec.ts | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 118b8448..dd198f2b 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -56,11 +56,18 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { // hoist enum declarations this.enumDeclarations.push( - new DeclarationBlock({}) - .export() - .asKind('const') - .withName(`${enumname}Schema`) - .withContent(`v.enum_(${enumname})`).string, + this.config.enumsAsTypes + ? new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.picklist([${node.values?.map(enumOption => `'${enumOption.name.value}'`).join(', ')}])`) + .string + : new DeclarationBlock({}) + .export() + .asKind('const') + .withName(`${enumname}Schema`) + .withContent(`v.enum_(${enumname})`).string, ); }, }; diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index a47080ba..2d8fd7bc 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -279,4 +279,26 @@ describe('valibot', () => { " `); }); + it('with enumsAsTypes', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + enumsAsTypes: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.picklist([\'PUBLIC\', \'BASIC_AUTH\']); + " + `); + }); }) From ac18b28a756ce0c3d26a20ea903c1849705c73d5 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:31:37 +0900 Subject: [PATCH 12/20] feat: support scalarSchemas --- src/valibot/index.ts | 3 +++ tests/valibot.spec.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index dd198f2b..4cc823b7 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -151,6 +151,9 @@ function maybeLazy(type: TypeNode, schema: string): string { } function valibot4Scalar(config: ValidationSchemaPluginConfig, visitor: Visitor, scalarName: string): string { + if (config.scalarSchemas?.[scalarName]) + return config.scalarSchemas[scalarName]; + const tsType = visitor.getScalarType(scalarName); switch (tsType) { case 'string': diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 2d8fd7bc..05fea880 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -301,4 +301,41 @@ describe('valibot', () => { " `); }); + it.todo('with notAllowEmptyString') + it.todo('with notAllowEmptyString issue #386') + it('with scalarSchemas', async () => { + const schema = buildSchema(/* GraphQL */ ` + input ScalarsInput { + date: Date! + email: Email + str: String! + } + scalar Date + scalar Email + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + scalarSchemas: { + Date: 'v.date()', + Email: 'v.string([v.email()])', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function ScalarsInputSchema(): v.GenericSchema { + return v.object({ + date: v.date(), + email: v.nullish(v.string([v.email()])), + str: v.string() + }) + } + " + `) + }); }) From 2731e1fd5b0679bf5304b11d1a32b7fdd8009337 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 12:17:39 +0900 Subject: [PATCH 13/20] refactor: change buildApiFromDirectiveArguments return type to string[] --- src/directive.ts | 5 ++--- tests/directive.spec.ts | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/directive.ts b/src/directive.ts index 71683287..25f9895e 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -115,7 +115,7 @@ export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyA .map((directive) => { const directiveName = directive.name.value; const argsConfig = config[directiveName]; - return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); + return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []).join(''); }) .join('') } @@ -132,7 +132,7 @@ function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstV return `.${schemaApi}(${schemaApiArgs.join(', ')})`; } -function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string { +function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { return args .map((arg) => { const argName = arg.name.value; @@ -142,7 +142,6 @@ function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, arg return buildApiSchema(validationSchema, arg.value); }) - .join(''); } function buildApiFromDirectiveObjectArguments(config: FormattedDirectiveObjectArguments, argValue: ConstValueNode): string { diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index 25bf1658..a17ce01a 100644 --- a/tests/directive.spec.ts +++ b/tests/directive.spec.ts @@ -352,7 +352,7 @@ describe('format directive config', () => { config: FormattedDirectiveArguments args: ReadonlyArray } - want: string + want: string[] }[] = [ { name: 'string', @@ -364,7 +364,7 @@ describe('format directive config', () => { msg: `"hello"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`], }, { name: 'string with additional stuff', @@ -376,7 +376,7 @@ describe('format directive config', () => { startWith: `"hello"`, }), }, - want: `.matched("^hello")`, + want: [`.matched("^hello")`], }, { name: 'number', @@ -388,7 +388,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: `.min(1)`, + want: [`.min(1)`], }, { name: 'boolean', @@ -401,7 +401,7 @@ describe('format directive config', () => { enabled: `true`, }), }, - want: `.strict(true)`, + want: [`.strict(true)`], }, { name: 'list', @@ -413,7 +413,7 @@ describe('format directive config', () => { minLength: `[1, "message"]`, }), }, - want: `.min(1, "message")`, + want: [`.min(1, "message")`], }, { name: 'object in list', @@ -425,7 +425,7 @@ describe('format directive config', () => { matches: `["hello", {message:"message", excludeEmptyString:true}]`, }), }, - want: `.matches("hello", {"message":"message","excludeEmptyString":true})`, + want: [`.matches("hello", {"message":"message","excludeEmptyString":true})`], }, { name: 'two arguments but matched to first argument', @@ -438,7 +438,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("hello")`, + want: [`.required("hello")`, ``], }, { name: 'two arguments but matched to second argument', @@ -451,7 +451,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: `.required("world")`, + want: [``, `.required("world")`], }, { name: 'two arguments matched all', @@ -465,7 +465,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: `.required("message").min(1)`, + want: [`.required("message")`, `.min(1)`], }, { name: 'argument matches validation schema api', @@ -479,7 +479,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.url()`, + want: [`.url()`], }, { name: 'argument matched argument but doesn\'t match api', @@ -493,7 +493,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: ``, + want: [``], }, { name: 'complex', @@ -509,7 +509,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: `.required("message").url()`, + want: [`.required("message")`, `.url()`], }, { name: 'complex 2', @@ -525,7 +525,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: `.required("message")`, + want: [`.required("message")`, ``], }, ]; for (const tc of cases) { From 3c3f73a212f9bcf944656b4c7aa86d189b1f4ca9 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Sun, 26 May 2024 12:18:41 +0900 Subject: [PATCH 14/20] feat: adds buildApiForValibot() --- src/directive.ts | 33 ++++++++++++++++++++++ tests/directive.spec.ts | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/directive.ts b/src/directive.ts index 25f9895e..22607403 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -120,6 +120,39 @@ export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyA .join('') } +// This function generates `[v.minLength(100), v.email()]` +// NOTE: valibot's API is not a method chain, so it is prepared separately from buildApi. +// +// config +// { +// 'constraint': { +// 'minLength': ['minLength', '$1'], +// 'format': { +// 'uri': ['url', '$2'], +// 'email': ['email', '$2'], +// } +// } +// } +// +// GraphQL schema +// ```graphql +// input ExampleInput { +// email: String! @required(msg: "message") @constraint(minLength: 100, format: "email") +// } +// ``` +// +// FIXME: v.required() is not supported yet. v.required() is classified as `Methods` and must wrap the schema. ex) `v.required(v.object({...}))` +export function buildApiForValibot(config: FormattedDirectiveConfig, directives: ReadonlyArray): string[] { + return directives + .filter(directive => config[directive.name.value] !== undefined) + .map((directive) => { + const directiveName = directive.name.value; + const argsConfig = config[directiveName]; + const apis = buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); + return apis.map(api => `v${api}`); + }).flat() +} + function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstValueNode): string { if (!validationSchema) return ''; diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index a17ce01a..c40e55e3 100644 --- a/tests/directive.spec.ts +++ b/tests/directive.spec.ts @@ -9,6 +9,7 @@ import type { } from '../src/directive'; import { buildApi, + buildApiForValibot, exportedForTesting, formatDirectiveConfig, formatDirectiveObjectArguments, @@ -603,4 +604,64 @@ describe('format directive config', () => { }); } }); + + describe('buildApiForValibot', () => { + const cases: { + name: string + args: { + config: FormattedDirectiveConfig + args: ReadonlyArray + } + want: string[] + }[] = [ + { + name: 'valid', + args: { + config: { + constraint: { + minLength: ['minLength', '$1'], + format: { + uri: ['url'], + email: ['email'], + }, + }, + }, + args: [ + // @constraint(minLength: 100, format: "email") + buildConstDirectiveNodes('constraint', { + minLength: `100`, + format: `"email"`, + }), + ], + }, + want: [`v.minLength(100)`, `v.email()`], + }, + { + name: 'enum', + args: { + config: { + constraint: { + format: { + URI: ['uri'], + }, + }, + }, + args: [ + // @constraint(format: EMAIL) + buildConstDirectiveNodes('constraint', { + format: 'URI', + }), + ], + }, + want: [`v.uri()`], + }, + ]; + for (const tc of cases) { + it(tc.name, () => { + const { config, args } = tc.args; + const got = buildApiForValibot(config, args); + expect(got).toStrictEqual(tc.want); + }); + } + }); }); From 48b2aa713e6848d434819ee7ff3ba81191270ea4 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 30 May 2024 09:43:25 +0900 Subject: [PATCH 15/20] feat: support directives --- src/valibot/index.ts | 23 ++++++++++++-- tests/valibot.spec.ts | 71 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 4cc823b7..bd799e17 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -12,6 +12,7 @@ import type { import type { ValidationSchemaPluginConfig } from '../config'; import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; +import { buildApiForValibot, formatDirectiveConfig } from '../directive'; import { isInput, isListType, @@ -114,15 +115,33 @@ function generateFieldTypeValibotSchema(config: ValidationSchemaPluginConfig, vi if (isListType(parentType)) return `v.nullable(${gen})`; + const actions = actionsFromDirectives(config, field); + if (isNonNullType(parentType)) - return gen; + return pipeSchemaAndActions(gen, actions); ; - return `v.nullish(${gen})`; + return `v.nullish(${pipeSchemaAndActions(gen, actions)})`; } console.warn('unhandled type:', type); return ''; } +function actionsFromDirectives(config: ValidationSchemaPluginConfig, field: InputValueDefinitionNode | FieldDefinitionNode): string[] { + if (config.directives && field.directives) { + const formatted = formatDirectiveConfig(config.directives); + return buildApiForValibot(formatted, field.directives); + } + + return []; +} + +function pipeSchemaAndActions(schema: string, actions: string[]): string { + if (actions.length === 0) + return schema; + + return `v.pipe(${schema}, ${actions.join(', ')})`; +} + function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, visitor: Visitor, node: NameNode): string { const converter = visitor.getNameNodeConverter(node); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 05fea880..51fdcc3f 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -338,4 +338,75 @@ describe('valibot', () => { " `) }); + it.todo('with typesPrefix') + it.todo('with typesSuffix') + it.todo('with default input values') + describe('issues #19', () => { + it('string field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String @constraint(minLength: 1, maxLength: 5000) + } + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + directives: { + constraint: { + minLength: ['minLength', '$1', 'Please input more than $1'], + maxLength: ['maxLength', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + profile: v.nullish(v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000"))) + }) + } + " + `) + }); + + it('not null field', async () => { + const schema = buildSchema(/* GraphQL */ ` + input UserCreateInput { + profile: String! @constraint(minLength: 1, maxLength: 5000) + } + directive @constraint(minLength: Int!, maxLength: Int!) on INPUT_FIELD_DEFINITION + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + directives: { + constraint: { + minLength: ['minLength', '$1', 'Please input more than $1'], + maxLength: ['maxLength', '$1', 'Please input less than $1'], + }, + }, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + profile: v.pipe(v.string(), v.minLength(1, "Please input more than 1"), v.maxLength(5000, "Please input less than 5000")) + }) + } + " + `) + }); + }) }) From 7aa54a9a750d4990f0e3e59ff99e13be8259d541 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Fri, 31 May 2024 22:19:16 +0900 Subject: [PATCH 16/20] feat: support object type and union type --- src/valibot/index.ts | 78 +++++++++ tests/valibot.spec.ts | 369 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index bd799e17..0a934d4b 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -6,7 +6,9 @@ import type { InputObjectTypeDefinitionNode, InputValueDefinitionNode, NameNode, + ObjectTypeDefinitionNode, TypeNode, + UnionTypeDefinitionNode, } from 'graphql'; import type { ValidationSchemaPluginConfig } from '../config'; @@ -14,6 +16,7 @@ import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { buildApiForValibot, formatDirectiveConfig } from '../directive'; import { + ObjectTypeDefinitionBuilder, isInput, isListType, isNamedType, @@ -48,6 +51,41 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { }; } + get ObjectTypeDefinition() { + return { + leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock( + [ + indent(`return v.object({`), + indent(`__typename: v.optional(v.literal('${node.name.value}')),`, 2), + shape, + indent('})'), + ].join('\n'), + ).string + appendArguments + ); + } + }), + }; + } + get EnumTypeDefinition() { return { leave: (node: EnumTypeDefinitionNode) => { @@ -74,6 +112,42 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { }; } + get UnionTypeDefinition() { + return { + leave: (node: UnionTypeDefinitionNode) => { + if (!node.types || !this.config.withObjectType) + return; + const visitor = this.createVisitor('output'); + const unionName = visitor.convertName(node.name.value); + const unionElements = node.types + .map((t) => { + const element = visitor.convertName(t.name.value); + const typ = visitor.getType(t.name.value); + if (typ?.astNode?.kind === 'EnumTypeDefinition') + return `${element}Schema`; + + switch (this.config.validationSchemaExportType) { + default: + return `${element}Schema()`; + } + }) + .join(', '); + const unionElementsCount = node.types.length ?? 0; + + const union = unionElementsCount > 1 ? `v.union([${unionElements}])` : unionElements; + + switch (this.config.validationSchemaExportType) { + default: + return new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${unionName}Schema()`) + .withBlock(indent(`return ${union}`)).string; + } + }, + }; + } + protected buildInputFields( fields: readonly (FieldDefinitionNode | InputValueDefinitionNode)[], visitor: Visitor, @@ -147,6 +221,8 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis switch (converter?.targetKind) { case 'InputObjectTypeDefinition': + case 'ObjectTypeDefinition': + case 'UnionTypeDefinition': // using switch-case rather than if-else to allow for future expansion switch (config.validationSchemaExportType) { default: @@ -154,6 +230,8 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis } case 'EnumTypeDefinition': return `${converter.convertName()}Schema`; + case 'ScalarTypeDefinition': + return valibot4Scalar(config, visitor, node.value); default: if (converter?.targetKind) console.warn('Unknown targetKind', converter?.targetKind); diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 51fdcc3f..28b129ee 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -408,5 +408,374 @@ describe('valibot', () => { " `) }); + it.todo('list field') + describe('pR #112', () => { + it.todo('with notAllowEmptyString') + it.todo('without notAllowEmptyString') + }) + describe('with withObjectType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + type User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); + }); + it('generate object type contains object type', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Book { + author: Author + title: String + } + + type Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Book')), + author: v.nullish(AuthorSchema()), + title: v.nullish(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Author')), + books: v.nullish(v.array(v.nullable(BookSchema()))), + name: v.nullish(v.string()) + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + it('generate both input & type', async () => { + const schema = buildSchema(/* GraphQL */ ` + scalar Date + scalar Email + input UserCreateInput { + name: String! + date: Date! + email: Email! + } + input UsernameUpdateInput { + updateInputId: ID! + updateName: String! + } + type User { + id: ID! + name: String + age: Int + email: Email + isMember: Boolean + createdAt: Date! + } + + type Mutation { + _empty: String + } + + type Query { + _empty: String + } + + type Subscription { + _empty: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + scalarSchemas: { + Date: 'v.date()', + Email: 'v.pipe(v.string(), v.email())', + }, + scalars: { + ID: { + input: 'number', + output: 'string', + }, + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function UserCreateInputSchema(): v.GenericSchema { + return v.object({ + name: v.string(), + date: v.date(), + email: v.pipe(v.string(), v.email()) + }) + } + + export function UsernameUpdateInputSchema(): v.GenericSchema { + return v.object({ + updateInputId: v.number(), + updateName: v.string() + }) + } + + export function UserSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('User')), + id: v.string(), + name: v.nullish(v.string()), + age: v.nullish(v.number()), + email: v.nullish(v.pipe(v.string(), v.email())), + isMember: v.nullish(v.boolean()), + createdAt: v.date() + }) + } + " + `) + + for (const wantNotContain of ['Query', 'Mutation', 'Subscription']) + expect(result.content).not.toContain(wantNotContain); + }); + }) + it('generate union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return v.union([CircleSchema(), SquareSchema()]) + } + " + `) + }); }) + it('correctly reference generated union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Circle { + radius: Int + } + union Shape = Circle + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return CircleSchema() + } + " + `) + }); + it('generate enum union types', async () => { + const schema = buildSchema(/* GraphQL */ ` + enum PageType { + PUBLIC + BASIC_AUTH + } + + enum MethodType { + GET + POST + } + + union AnyType = PageType | MethodType + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + export const PageTypeSchema = v.enum_(PageType); + + export const MethodTypeSchema = v.enum_(MethodType); + + export function AnyTypeSchema() { + return v.union([PageTypeSchema, MethodTypeSchema]) + } + " + `) + }); + it('generate union types with single element, export as const', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } + type Circle { + radius: Int + } + union Shape = Circle | Square + + type Geometry { + shape: Shape + } + `); + + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + validationSchemaExportType: 'const', + }, + {}, + ); + + expect(result.content).toMatchInlineSnapshot(` + " + + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } + + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + + export function ShapeSchema() { + return v.union([CircleSchema(), SquareSchema()]) + } + + export function GeometrySchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Geometry')), + shape: v.nullish(ShapeSchema()) + }) + } + " + `) + }); + it('with object arguments', async () => { + const schema = buildSchema(/* GraphQL */ ` + type MyType { + foo(a: String, b: Int!, c: Boolean, d: Float!, e: Text): String + } + scalar Text + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + scalars: { + Text: 'string', + }, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function MyTypeSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('MyType')), + foo: v.nullish(v.string()) + }) + } + + export function MyTypeFooArgsSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.number(), + c: v.nullish(v.boolean()), + d: v.number(), + e: v.nullish(v.string()) + }) + } + " + `) + }); }) From 63c56afac5ba57f8a40eaa77337cf2351baa8615 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Fri, 31 May 2024 22:27:22 +0900 Subject: [PATCH 17/20] feat: support interface type --- src/valibot/index.ts | 31 +++++ tests/valibot.spec.ts | 262 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 243 insertions(+), 50 deletions(-) diff --git a/src/valibot/index.ts b/src/valibot/index.ts index 0a934d4b..04841cd6 100644 --- a/src/valibot/index.ts +++ b/src/valibot/index.ts @@ -5,6 +5,7 @@ import type { GraphQLSchema, InputObjectTypeDefinitionNode, InputValueDefinitionNode, + InterfaceTypeDefinitionNode, NameNode, ObjectTypeDefinitionNode, TypeNode, @@ -16,6 +17,7 @@ import { BaseSchemaVisitor } from '../schema_visitor'; import type { Visitor } from '../visitor'; import { buildApiForValibot, formatDirectiveConfig } from '../directive'; import { + InterfaceTypeDefinitionBuilder, ObjectTypeDefinitionBuilder, isInput, isListType, @@ -51,6 +53,34 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor { }; } + get InterfaceTypeDefinition() { + return { + leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => { + const visitor = this.createVisitor('output'); + const name = visitor.convertName(node.name.value); + this.importTypes.push(name); + + // Building schema for field arguments. + const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor); + const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : ''; + + // Building schema for fields. + const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n'); + + switch (this.config.validationSchemaExportType) { + default: + return ( + new DeclarationBlock({}) + .export() + .asKind('function') + .withName(`${name}Schema(): v.GenericSchema<${name}>`) + .withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string + appendArguments + ); + } + }), + }; + } + get ObjectTypeDefinition() { return { leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => { @@ -220,6 +250,7 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis const converter = visitor.getNameNodeConverter(node); switch (converter?.targetKind) { + case 'InterfaceTypeDefinition': case 'InputObjectTypeDefinition': case 'ObjectTypeDefinition': case 'UnionTypeDefinition': diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 28b129ee..03e0f0bb 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -610,12 +610,19 @@ describe('valibot', () => { `) }); }) - it('correctly reference generated union types', async () => { + it('generate union types with single element', async () => { const schema = buildSchema(/* GraphQL */ ` + type Square { + size: Int + } type Circle { radius: Int } - union Shape = Circle + union Shape = Circle | Square + + type Geometry { + shape: Shape + } `); const result = await plugin( @@ -631,6 +638,13 @@ describe('valibot', () => { expect(result.content).toMatchInlineSnapshot(` " + export function SquareSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Square')), + size: v.nullish(v.number()) + }) + } + export function CircleSchema(): v.GenericSchema { return v.object({ __typename: v.optional(v.literal('Circle')), @@ -639,24 +653,24 @@ describe('valibot', () => { } export function ShapeSchema() { - return CircleSchema() + return v.union([CircleSchema(), SquareSchema()]) + } + + export function GeometrySchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Geometry')), + shape: v.nullish(ShapeSchema()) + }) } " `) }); - it('generate enum union types', async () => { + it('correctly reference generated union types', async () => { const schema = buildSchema(/* GraphQL */ ` - enum PageType { - PUBLIC - BASIC_AUTH - } - - enum MethodType { - GET - POST + type Circle { + radius: Int } - - union AnyType = PageType | MethodType + union Shape = Circle `); const result = await plugin( @@ -671,29 +685,33 @@ describe('valibot', () => { expect(result.content).toMatchInlineSnapshot(` " - export const PageTypeSchema = v.enum_(PageType); - export const MethodTypeSchema = v.enum_(MethodType); + export function CircleSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Circle')), + radius: v.nullish(v.number()) + }) + } - export function AnyTypeSchema() { - return v.union([PageTypeSchema, MethodTypeSchema]) + export function ShapeSchema() { + return CircleSchema() } " `) }); - it('generate union types with single element, export as const', async () => { + it('generate enum union types', async () => { const schema = buildSchema(/* GraphQL */ ` - type Square { - size: Int - } - type Circle { - radius: Int + enum PageType { + PUBLIC + BASIC_AUTH } - union Shape = Circle | Square - type Geometry { - shape: Shape + enum MethodType { + GET + POST } + + union AnyType = PageType | MethodType `); const result = await plugin( @@ -702,41 +720,23 @@ describe('valibot', () => { { schema: 'valibot', withObjectType: true, - validationSchemaExportType: 'const', }, {}, ); expect(result.content).toMatchInlineSnapshot(` " + export const PageTypeSchema = v.enum_(PageType); - export function CircleSchema(): v.GenericSchema { - return v.object({ - __typename: v.optional(v.literal('Circle')), - radius: v.nullish(v.number()) - }) - } - - export function SquareSchema(): v.GenericSchema { - return v.object({ - __typename: v.optional(v.literal('Square')), - size: v.nullish(v.number()) - }) - } - - export function ShapeSchema() { - return v.union([CircleSchema(), SquareSchema()]) - } + export const MethodTypeSchema = v.enum_(MethodType); - export function GeometrySchema(): v.GenericSchema { - return v.object({ - __typename: v.optional(v.literal('Geometry')), - shape: v.nullish(ShapeSchema()) - }) + export function AnyTypeSchema() { + return v.union([PageTypeSchema, MethodTypeSchema]) } " `) }); + it.todo('generate union types with single element, export as const') it('with object arguments', async () => { const schema = buildSchema(/* GraphQL */ ` type MyType { @@ -778,4 +778,166 @@ describe('valibot', () => { " `) }); + describe('with InterfaceType', () => { + it('not generate if withObjectType false', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface User { + id: ID! + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: false, + }, + {}, + ); + expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema'); + }); + it('generate if withObjectType true', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + title: v.nullish(v.string()) + }) + } + " + `) + }); + it('generate interface type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + author: Author + title: String + } + + interface Author { + books: [Book] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + author: v.nullish(AuthorSchema()), + title: v.nullish(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + books: v.nullish(v.array(v.nullable(BookSchema()))), + name: v.nullish(v.string()) + }) + } + " + `) + }); + it('generate object type contains interface type', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Book { + title: String! + author: Author! + } + + type Textbook implements Book { + title: String! + author: Author! + courses: [String!]! + } + + type ColoringBook implements Book { + title: String! + author: Author! + colors: [String!]! + } + + type Author { + books: [Book!] + name: String + } + `); + const result = await plugin( + schema, + [], + { + schema: 'valibot', + withObjectType: true, + }, + {}, + ); + expect(result.content).toMatchInlineSnapshot(` + " + + export function BookSchema(): v.GenericSchema { + return v.object({ + title: v.string(), + author: AuthorSchema() + }) + } + + export function TextbookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Textbook')), + title: v.string(), + author: AuthorSchema(), + courses: v.array(v.string()) + }) + } + + export function ColoringBookSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('ColoringBook')), + title: v.string(), + author: AuthorSchema(), + colors: v.array(v.string()) + }) + } + + export function AuthorSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Author')), + books: v.nullish(v.array(BookSchema())), + name: v.nullish(v.string()) + }) + } + " + `) + }); + }) + it.todo('properly generates custom directive values') + it.todo('exports as const instead of func') + it.todo('generate both input & type, export as const') + it.todo('issue #394') + it.todo('issue #394') }) From 9f0a5ec7177e195c42bffa995e8076f2d233306a Mon Sep 17 00:00:00 2001 From: MH4GF Date: Fri, 31 May 2024 22:28:24 +0900 Subject: [PATCH 18/20] test: add generate codes and type-check script --- codegen.yml | 23 +++++++ example/valibot/schemas.ts | 130 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 154 insertions(+) create mode 100644 example/valibot/schemas.ts diff --git a/codegen.yml b/codegen.yml index f3170225..17e5e3e5 100644 --- a/codegen.yml +++ b/codegen.yml @@ -81,3 +81,26 @@ generates: email: email scalars: ID: string + example/valibot/schemas.ts: + plugins: + - ./dist/main/index.js: + schema: valibot + importFrom: ../types + withObjectType: true + directives: + # Write directives like + # + # directive: + # arg1: schemaApi + # arg2: ["schemaApi2", "Hello $1"] + # + # See more examples in `./tests/directive.spec.ts` + # https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/blob/main/tests/directive.spec.ts + constraint: + minLength: minLength + # Replace $1 with specified `startsWith` argument value of the constraint directive + startsWith: [regex, /^$1/, message] + format: + email: email + scalars: + ID: string diff --git a/example/valibot/schemas.ts b/example/valibot/schemas.ts new file mode 100644 index 00000000..14c8bac1 --- /dev/null +++ b/example/valibot/schemas.ts @@ -0,0 +1,130 @@ +import * as v from 'valibot' +import { Admin, AttributeInput, ButtonComponentType, ComponentInput, DropDownComponentInput, EventArgumentInput, EventInput, EventOptionType, Guest, HttpInput, HttpMethod, LayoutInput, MyType, MyTypeFooArgs, Namer, PageInput, PageType, User } from '../types' + +export const ButtonComponentTypeSchema = v.enum_(ButtonComponentType); + +export const EventOptionTypeSchema = v.enum_(EventOptionType); + +export const HttpMethodSchema = v.enum_(HttpMethod); + +export const PageTypeSchema = v.enum_(PageType); + +export function AdminSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Admin')), + lastModifiedAt: v.nullish(v.any()) + }) +} + +export function AttributeInputSchema(): v.GenericSchema { + return v.object({ + key: v.nullish(v.string()), + val: v.nullish(v.string()) + }) +} + +export function ComponentInputSchema(): v.GenericSchema { + return v.object({ + child: v.lazy(() => v.nullish(ComponentInputSchema())), + childrens: v.nullish(v.array(v.lazy(() => v.nullable(ComponentInputSchema())))), + event: v.lazy(() => v.nullish(EventInputSchema())), + name: v.string(), + type: ButtonComponentTypeSchema + }) +} + +export function DropDownComponentInputSchema(): v.GenericSchema { + return v.object({ + dropdownComponent: v.lazy(() => v.nullish(ComponentInputSchema())), + getEvent: v.lazy(() => EventInputSchema()) + }) +} + +export function EventArgumentInputSchema(): v.GenericSchema { + return v.object({ + name: v.pipe(v.string(), v.minLength(5)), + value: v.pipe(v.string(), v.regex(/^foo/, "message")) + }) +} + +export function EventInputSchema(): v.GenericSchema { + return v.object({ + arguments: v.array(v.lazy(() => EventArgumentInputSchema())), + options: v.nullish(v.array(EventOptionTypeSchema)) + }) +} + +export function GuestSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('Guest')), + lastLoggedIn: v.nullish(v.any()) + }) +} + +export function HttpInputSchema(): v.GenericSchema { + return v.object({ + method: v.nullish(HttpMethodSchema), + url: v.any() + }) +} + +export function LayoutInputSchema(): v.GenericSchema { + return v.object({ + dropdown: v.lazy(() => v.nullish(DropDownComponentInputSchema())) + }) +} + +export function MyTypeSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('MyType')), + foo: v.nullish(v.string()) + }) +} + +export function MyTypeFooArgsSchema(): v.GenericSchema { + return v.object({ + a: v.nullish(v.string()), + b: v.number(), + c: v.nullish(v.boolean()), + d: v.number() + }) +} + +export function NamerSchema(): v.GenericSchema { + return v.object({ + name: v.nullish(v.string()) + }) +} + +export function PageInputSchema(): v.GenericSchema { + return v.object({ + attributes: v.nullish(v.array(v.lazy(() => AttributeInputSchema()))), + date: v.nullish(v.any()), + height: v.number(), + id: v.string(), + layout: v.lazy(() => LayoutInputSchema()), + pageType: PageTypeSchema, + postIDs: v.nullish(v.array(v.string())), + show: v.boolean(), + tags: v.nullish(v.array(v.nullable(v.string()))), + title: v.string(), + width: v.number() + }) +} + +export function UserSchema(): v.GenericSchema { + return v.object({ + __typename: v.optional(v.literal('User')), + createdAt: v.nullish(v.any()), + email: v.nullish(v.string()), + id: v.nullish(v.string()), + kind: v.nullish(UserKindSchema()), + name: v.nullish(v.string()), + password: v.nullish(v.string()), + updatedAt: v.nullish(v.any()) + }) +} + +export function UserKindSchema() { + return v.union([AdminSchema(), GuestSchema()]) +} diff --git a/package.json b/package.json index 41de0008..d5c62fce 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "type-check:yup": "tsc --strict --skipLibCheck --noEmit example/yup/schemas.ts", "type-check:zod": "tsc --strict --skipLibCheck --noEmit example/zod/schemas.ts", "type-check:myzod": "tsc --strict --skipLibCheck --noEmit example/myzod/schemas.ts", + "type-check:valibot": "tsc --strict --skipLibCheck --noEmit example/valibot/schemas.ts", "test": "vitest run", "build": "run-p build:*", "build:main": "tsc -p tsconfig.main.json", From 75c4fbea40edcce0873fafca1b6b5fafd2cba9ea Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 6 Jun 2024 10:12:37 +0900 Subject: [PATCH 19/20] chore: remove duplicated test case --- tests/valibot.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/valibot.spec.ts b/tests/valibot.spec.ts index 03e0f0bb..61f5a535 100644 --- a/tests/valibot.spec.ts +++ b/tests/valibot.spec.ts @@ -939,5 +939,4 @@ describe('valibot', () => { it.todo('exports as const instead of func') it.todo('generate both input & type, export as const') it.todo('issue #394') - it.todo('issue #394') }) From b9cb35b5b4dc640931910fab5ffaa087a42c54c6 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Thu, 6 Jun 2024 15:02:38 +0900 Subject: [PATCH 20/20] refactor: add _buildApiFromDirectiveArguments() --- src/directive.ts | 10 +++++++--- tests/directive.spec.ts | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/directive.ts b/src/directive.ts index 3763ddbf..f2918423 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -115,7 +115,7 @@ export function buildApi(config: FormattedDirectiveConfig, directives: ReadonlyA .map((directive) => { const directiveName = directive.name.value; const argsConfig = config[directiveName]; - return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []).join(''); + return buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); }) .join('') } @@ -148,7 +148,7 @@ export function buildApiForValibot(config: FormattedDirectiveConfig, directives: .map((directive) => { const directiveName = directive.name.value; const argsConfig = config[directiveName]; - const apis = buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); + const apis = _buildApiFromDirectiveArguments(argsConfig, directive.arguments ?? []); return apis.map(api => `v${api}`); }).flat() } @@ -165,7 +165,11 @@ function buildApiSchema(validationSchema: string[] | undefined, argValue: ConstV return `.${schemaApi}(${schemaApiArgs.join(', ')})`; } -function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { +function buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string { + return _buildApiFromDirectiveArguments(config, args).join(''); +} + +function _buildApiFromDirectiveArguments(config: FormattedDirectiveArguments, args: ReadonlyArray): string[] { return args .map((arg) => { const argName = arg.name.value; diff --git a/tests/directive.spec.ts b/tests/directive.spec.ts index c40e55e3..d5b3e4e6 100644 --- a/tests/directive.spec.ts +++ b/tests/directive.spec.ts @@ -353,7 +353,7 @@ describe('format directive config', () => { config: FormattedDirectiveArguments args: ReadonlyArray } - want: string[] + want: string }[] = [ { name: 'string', @@ -365,7 +365,7 @@ describe('format directive config', () => { msg: `"hello"`, }), }, - want: [`.required("hello")`], + want: `.required("hello")`, }, { name: 'string with additional stuff', @@ -377,7 +377,7 @@ describe('format directive config', () => { startWith: `"hello"`, }), }, - want: [`.matched("^hello")`], + want: `.matched("^hello")`, }, { name: 'number', @@ -389,7 +389,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: [`.min(1)`], + want: `.min(1)`, }, { name: 'boolean', @@ -402,7 +402,7 @@ describe('format directive config', () => { enabled: `true`, }), }, - want: [`.strict(true)`], + want: `.strict(true)`, }, { name: 'list', @@ -414,7 +414,7 @@ describe('format directive config', () => { minLength: `[1, "message"]`, }), }, - want: [`.min(1, "message")`], + want: `.min(1, "message")`, }, { name: 'object in list', @@ -426,7 +426,7 @@ describe('format directive config', () => { matches: `["hello", {message:"message", excludeEmptyString:true}]`, }), }, - want: [`.matches("hello", {"message":"message","excludeEmptyString":true})`], + want: `.matches("hello", {"message":"message","excludeEmptyString":true})`, }, { name: 'two arguments but matched to first argument', @@ -439,7 +439,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: [`.required("hello")`, ``], + want: `.required("hello")`, }, { name: 'two arguments but matched to second argument', @@ -452,7 +452,7 @@ describe('format directive config', () => { msg2: `"world"`, }), }, - want: [``, `.required("world")`], + want: `.required("world")`, }, { name: 'two arguments matched all', @@ -466,7 +466,7 @@ describe('format directive config', () => { minLength: `1`, }), }, - want: [`.required("message")`, `.min(1)`], + want: `.required("message").min(1)`, }, { name: 'argument matches validation schema api', @@ -480,7 +480,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: [`.url()`], + want: `.url()`, }, { name: 'argument matched argument but doesn\'t match api', @@ -494,7 +494,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: [``], + want: ``, }, { name: 'complex', @@ -510,7 +510,7 @@ describe('format directive config', () => { format: `"uri"`, }), }, - want: [`.required("message")`, `.url()`], + want: `.required("message").url()`, }, { name: 'complex 2', @@ -526,7 +526,7 @@ describe('format directive config', () => { format: `"uuid"`, }), }, - want: [`.required("message")`, ``], + want: `.required("message")`, }, ]; for (const tc of cases) {