Skip to content

feat: minimal implementation to support valibot #667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: support interface type
  • Loading branch information
MH4GF committed May 31, 2024
commit 63c56afac5ba57f8a40eaa77337cf2351baa8615
31 changes: 31 additions & 0 deletions src/valibot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
GraphQLSchema,
InputObjectTypeDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
NameNode,
ObjectTypeDefinitionNode,
TypeNode,
Expand All @@ -16,6 +17,7 @@ import { BaseSchemaVisitor } from '../schema_visitor';
import type { Visitor } from '../visitor';
import { buildApiForValibot, formatDirectiveConfig } from '../directive';
import {
InterfaceTypeDefinitionBuilder,
ObjectTypeDefinitionBuilder,
isInput,
isListType,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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':
Expand Down
262 changes: 212 additions & 50 deletions tests/valibot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -631,6 +638,13 @@ describe('valibot', () => {
expect(result.content).toMatchInlineSnapshot(`
"

export function SquareSchema(): v.GenericSchema<Square> {
return v.object({
__typename: v.optional(v.literal('Square')),
size: v.nullish(v.number())
})
}

export function CircleSchema(): v.GenericSchema<Circle> {
return v.object({
__typename: v.optional(v.literal('Circle')),
Expand All @@ -639,24 +653,24 @@ describe('valibot', () => {
}

export function ShapeSchema() {
return CircleSchema()
return v.union([CircleSchema(), SquareSchema()])
}

export function GeometrySchema(): v.GenericSchema<Geometry> {
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(
Expand All @@ -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<Circle> {
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(
Expand All @@ -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<Circle> {
return v.object({
__typename: v.optional(v.literal('Circle')),
radius: v.nullish(v.number())
})
}

export function SquareSchema(): v.GenericSchema<Square> {
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<Geometry> {
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 {
Expand Down Expand Up @@ -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<User>');
});
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<Book> {
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<Book> {
return v.object({
author: v.nullish(AuthorSchema()),
title: v.nullish(v.string())
})
}

export function AuthorSchema(): v.GenericSchema<Author> {
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<Book> {
return v.object({
title: v.string(),
author: AuthorSchema()
})
}

export function TextbookSchema(): v.GenericSchema<Textbook> {
return v.object({
__typename: v.optional(v.literal('Textbook')),
title: v.string(),
author: AuthorSchema(),
courses: v.array(v.string())
})
}

export function ColoringBookSchema(): v.GenericSchema<ColoringBook> {
return v.object({
__typename: v.optional(v.literal('ColoringBook')),
title: v.string(),
author: AuthorSchema(),
colors: v.array(v.string())
})
}

export function AuthorSchema(): v.GenericSchema<Author> {
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')
})