Skip to content

Commit 63c56af

Browse files
committed
feat: support interface type
1 parent 7aa54a9 commit 63c56af

File tree

2 files changed

+243
-50
lines changed

2 files changed

+243
-50
lines changed

src/valibot/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
GraphQLSchema,
66
InputObjectTypeDefinitionNode,
77
InputValueDefinitionNode,
8+
InterfaceTypeDefinitionNode,
89
NameNode,
910
ObjectTypeDefinitionNode,
1011
TypeNode,
@@ -16,6 +17,7 @@ import { BaseSchemaVisitor } from '../schema_visitor';
1617
import type { Visitor } from '../visitor';
1718
import { buildApiForValibot, formatDirectiveConfig } from '../directive';
1819
import {
20+
InterfaceTypeDefinitionBuilder,
1921
ObjectTypeDefinitionBuilder,
2022
isInput,
2123
isListType,
@@ -51,6 +53,34 @@ export class ValibotSchemaVisitor extends BaseSchemaVisitor {
5153
};
5254
}
5355

56+
get InterfaceTypeDefinition() {
57+
return {
58+
leave: InterfaceTypeDefinitionBuilder(this.config.withObjectType, (node: InterfaceTypeDefinitionNode) => {
59+
const visitor = this.createVisitor('output');
60+
const name = visitor.convertName(node.name.value);
61+
this.importTypes.push(name);
62+
63+
// Building schema for field arguments.
64+
const argumentBlocks = this.buildTypeDefinitionArguments(node, visitor);
65+
const appendArguments = argumentBlocks ? `\n${argumentBlocks}` : '';
66+
67+
// Building schema for fields.
68+
const shape = node.fields?.map(field => generateFieldValibotSchema(this.config, visitor, field, 2)).join(',\n');
69+
70+
switch (this.config.validationSchemaExportType) {
71+
default:
72+
return (
73+
new DeclarationBlock({})
74+
.export()
75+
.asKind('function')
76+
.withName(`${name}Schema(): v.GenericSchema<${name}>`)
77+
.withBlock([indent(`return v.object({`), shape, indent('})')].join('\n')).string + appendArguments
78+
);
79+
}
80+
}),
81+
};
82+
}
83+
5484
get ObjectTypeDefinition() {
5585
return {
5686
leave: ObjectTypeDefinitionBuilder(this.config.withObjectType, (node: ObjectTypeDefinitionNode) => {
@@ -220,6 +250,7 @@ function generateNameNodeValibotSchema(config: ValidationSchemaPluginConfig, vis
220250
const converter = visitor.getNameNodeConverter(node);
221251

222252
switch (converter?.targetKind) {
253+
case 'InterfaceTypeDefinition':
223254
case 'InputObjectTypeDefinition':
224255
case 'ObjectTypeDefinition':
225256
case 'UnionTypeDefinition':

tests/valibot.spec.ts

Lines changed: 212 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,19 @@ describe('valibot', () => {
610610
`)
611611
});
612612
})
613-
it('correctly reference generated union types', async () => {
613+
it('generate union types with single element', async () => {
614614
const schema = buildSchema(/* GraphQL */ `
615+
type Square {
616+
size: Int
617+
}
615618
type Circle {
616619
radius: Int
617620
}
618-
union Shape = Circle
621+
union Shape = Circle | Square
622+
623+
type Geometry {
624+
shape: Shape
625+
}
619626
`);
620627

621628
const result = await plugin(
@@ -631,6 +638,13 @@ describe('valibot', () => {
631638
expect(result.content).toMatchInlineSnapshot(`
632639
"
633640
641+
export function SquareSchema(): v.GenericSchema<Square> {
642+
return v.object({
643+
__typename: v.optional(v.literal('Square')),
644+
size: v.nullish(v.number())
645+
})
646+
}
647+
634648
export function CircleSchema(): v.GenericSchema<Circle> {
635649
return v.object({
636650
__typename: v.optional(v.literal('Circle')),
@@ -639,24 +653,24 @@ describe('valibot', () => {
639653
}
640654
641655
export function ShapeSchema() {
642-
return CircleSchema()
656+
return v.union([CircleSchema(), SquareSchema()])
657+
}
658+
659+
export function GeometrySchema(): v.GenericSchema<Geometry> {
660+
return v.object({
661+
__typename: v.optional(v.literal('Geometry')),
662+
shape: v.nullish(ShapeSchema())
663+
})
643664
}
644665
"
645666
`)
646667
});
647-
it('generate enum union types', async () => {
668+
it('correctly reference generated union types', async () => {
648669
const schema = buildSchema(/* GraphQL */ `
649-
enum PageType {
650-
PUBLIC
651-
BASIC_AUTH
652-
}
653-
654-
enum MethodType {
655-
GET
656-
POST
670+
type Circle {
671+
radius: Int
657672
}
658-
659-
union AnyType = PageType | MethodType
673+
union Shape = Circle
660674
`);
661675

662676
const result = await plugin(
@@ -671,29 +685,33 @@ describe('valibot', () => {
671685

672686
expect(result.content).toMatchInlineSnapshot(`
673687
"
674-
export const PageTypeSchema = v.enum_(PageType);
675688
676-
export const MethodTypeSchema = v.enum_(MethodType);
689+
export function CircleSchema(): v.GenericSchema<Circle> {
690+
return v.object({
691+
__typename: v.optional(v.literal('Circle')),
692+
radius: v.nullish(v.number())
693+
})
694+
}
677695
678-
export function AnyTypeSchema() {
679-
return v.union([PageTypeSchema, MethodTypeSchema])
696+
export function ShapeSchema() {
697+
return CircleSchema()
680698
}
681699
"
682700
`)
683701
});
684-
it('generate union types with single element, export as const', async () => {
702+
it('generate enum union types', async () => {
685703
const schema = buildSchema(/* GraphQL */ `
686-
type Square {
687-
size: Int
688-
}
689-
type Circle {
690-
radius: Int
704+
enum PageType {
705+
PUBLIC
706+
BASIC_AUTH
691707
}
692-
union Shape = Circle | Square
693708
694-
type Geometry {
695-
shape: Shape
709+
enum MethodType {
710+
GET
711+
POST
696712
}
713+
714+
union AnyType = PageType | MethodType
697715
`);
698716

699717
const result = await plugin(
@@ -702,41 +720,23 @@ describe('valibot', () => {
702720
{
703721
schema: 'valibot',
704722
withObjectType: true,
705-
validationSchemaExportType: 'const',
706723
},
707724
{},
708725
);
709726

710727
expect(result.content).toMatchInlineSnapshot(`
711728
"
729+
export const PageTypeSchema = v.enum_(PageType);
712730
713-
export function CircleSchema(): v.GenericSchema<Circle> {
714-
return v.object({
715-
__typename: v.optional(v.literal('Circle')),
716-
radius: v.nullish(v.number())
717-
})
718-
}
719-
720-
export function SquareSchema(): v.GenericSchema<Square> {
721-
return v.object({
722-
__typename: v.optional(v.literal('Square')),
723-
size: v.nullish(v.number())
724-
})
725-
}
726-
727-
export function ShapeSchema() {
728-
return v.union([CircleSchema(), SquareSchema()])
729-
}
731+
export const MethodTypeSchema = v.enum_(MethodType);
730732
731-
export function GeometrySchema(): v.GenericSchema<Geometry> {
732-
return v.object({
733-
__typename: v.optional(v.literal('Geometry')),
734-
shape: v.nullish(ShapeSchema())
735-
})
733+
export function AnyTypeSchema() {
734+
return v.union([PageTypeSchema, MethodTypeSchema])
736735
}
737736
"
738737
`)
739738
});
739+
it.todo('generate union types with single element, export as const')
740740
it('with object arguments', async () => {
741741
const schema = buildSchema(/* GraphQL */ `
742742
type MyType {
@@ -778,4 +778,166 @@ describe('valibot', () => {
778778
"
779779
`)
780780
});
781+
describe('with InterfaceType', () => {
782+
it('not generate if withObjectType false', async () => {
783+
const schema = buildSchema(/* GraphQL */ `
784+
interface User {
785+
id: ID!
786+
name: String
787+
}
788+
`);
789+
const result = await plugin(
790+
schema,
791+
[],
792+
{
793+
schema: 'valibot',
794+
withObjectType: false,
795+
},
796+
{},
797+
);
798+
expect(result.content).not.toContain('export function UserSchema(): v.GenericSchema<User>');
799+
});
800+
it('generate if withObjectType true', async () => {
801+
const schema = buildSchema(/* GraphQL */ `
802+
interface Book {
803+
title: String
804+
}
805+
`);
806+
const result = await plugin(
807+
schema,
808+
[],
809+
{
810+
schema: 'valibot',
811+
withObjectType: true,
812+
},
813+
{},
814+
);
815+
expect(result.content).toMatchInlineSnapshot(`
816+
"
817+
818+
export function BookSchema(): v.GenericSchema<Book> {
819+
return v.object({
820+
title: v.nullish(v.string())
821+
})
822+
}
823+
"
824+
`)
825+
});
826+
it('generate interface type contains interface type', async () => {
827+
const schema = buildSchema(/* GraphQL */ `
828+
interface Book {
829+
author: Author
830+
title: String
831+
}
832+
833+
interface Author {
834+
books: [Book]
835+
name: String
836+
}
837+
`);
838+
const result = await plugin(
839+
schema,
840+
[],
841+
{
842+
schema: 'valibot',
843+
withObjectType: true,
844+
},
845+
{},
846+
);
847+
expect(result.content).toMatchInlineSnapshot(`
848+
"
849+
850+
export function BookSchema(): v.GenericSchema<Book> {
851+
return v.object({
852+
author: v.nullish(AuthorSchema()),
853+
title: v.nullish(v.string())
854+
})
855+
}
856+
857+
export function AuthorSchema(): v.GenericSchema<Author> {
858+
return v.object({
859+
books: v.nullish(v.array(v.nullable(BookSchema()))),
860+
name: v.nullish(v.string())
861+
})
862+
}
863+
"
864+
`)
865+
});
866+
it('generate object type contains interface type', async () => {
867+
const schema = buildSchema(/* GraphQL */ `
868+
interface Book {
869+
title: String!
870+
author: Author!
871+
}
872+
873+
type Textbook implements Book {
874+
title: String!
875+
author: Author!
876+
courses: [String!]!
877+
}
878+
879+
type ColoringBook implements Book {
880+
title: String!
881+
author: Author!
882+
colors: [String!]!
883+
}
884+
885+
type Author {
886+
books: [Book!]
887+
name: String
888+
}
889+
`);
890+
const result = await plugin(
891+
schema,
892+
[],
893+
{
894+
schema: 'valibot',
895+
withObjectType: true,
896+
},
897+
{},
898+
);
899+
expect(result.content).toMatchInlineSnapshot(`
900+
"
901+
902+
export function BookSchema(): v.GenericSchema<Book> {
903+
return v.object({
904+
title: v.string(),
905+
author: AuthorSchema()
906+
})
907+
}
908+
909+
export function TextbookSchema(): v.GenericSchema<Textbook> {
910+
return v.object({
911+
__typename: v.optional(v.literal('Textbook')),
912+
title: v.string(),
913+
author: AuthorSchema(),
914+
courses: v.array(v.string())
915+
})
916+
}
917+
918+
export function ColoringBookSchema(): v.GenericSchema<ColoringBook> {
919+
return v.object({
920+
__typename: v.optional(v.literal('ColoringBook')),
921+
title: v.string(),
922+
author: AuthorSchema(),
923+
colors: v.array(v.string())
924+
})
925+
}
926+
927+
export function AuthorSchema(): v.GenericSchema<Author> {
928+
return v.object({
929+
__typename: v.optional(v.literal('Author')),
930+
books: v.nullish(v.array(BookSchema())),
931+
name: v.nullish(v.string())
932+
})
933+
}
934+
"
935+
`)
936+
});
937+
})
938+
it.todo('properly generates custom directive values')
939+
it.todo('exports as const instead of func')
940+
it.todo('generate both input & type, export as const')
941+
it.todo('issue #394')
942+
it.todo('issue #394')
781943
})

0 commit comments

Comments
 (0)