Skip to content
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

Fix handling for nested generic types #12

Merged
merged 3 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ jobs:
run: diff <(echo -n) <(gofumpt -d .)

- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.52.2
version: v1.59
args: --verbose --timeout=3m

- name: Test
run: make test
88 changes: 71 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,64 @@ Zod + Generate = Zen

Converts Go structs with go-validator validations to Zod schemas.

Zen supports self-referential types.
Zen supports self-referential types and generic types. Other cyclic types (apart from self referential types) are not supported.
hi-rai marked this conversation as resolved.
Show resolved Hide resolved

## Usage:
## Usage

```go
type Post struct {
Title string `validate:"required"`
}
type User struct {
Name string `validate:"required"`
Nickname *string // pointers become optional
Age int `validate:"min=18"`
Height float64 `validate:"min=0,max=3"`
Tags []string `validate:"min=1"`
Name string `validate:"required"`
Nickname *string // pointers become optional
Age int `validate:"min=18"`
Height float64 `validate:"min=0,max=3"`
Tags []string `validate:"min=1"`
Favourites []struct { // nested structs are kept inline
Name string `validate:"required"`
}
Posts []Post // external structs are emitted as separate exports
}
StructToZodSchema(User{})
fmt.Print(zen.StructToZodSchema(User{}))

// Self referential types are supported
type Tree struct {
Value int
Children []Tree
}
fmt.Print(zen.StructToZodSchema(Tree{}))

// We can also use create a converter and convert multiple types together
c := zen.NewConverter(nil)

// Generic types are also supported
type GenericPair[T any, U any] struct {
First T
Second U
}
type StringIntPair GenericPair[string, int]
c.AddType(StringIntPair{})

// For non-defined types, the type arguments are appended to the generic type
// name to get the type name
c.AddType(GenericPair[int, bool]{})

// Even nested generic types are supported
type PairMap[K comparable, T any, U any] struct {
Items map[K]GenericPair[T, U] `json:"items"`
}
c.AddType(PairMap[string, int, bool]{})

// Now export the generated schemas. Duplicate schemas are skipped
fmt.Print(c.Export())
```

Outputs:

```typescript
export const PostSchema = z.object({
Title: z.string().min(1),
Title: z.string().min(1),
})
export type Post = z.infer<typeof PostSchema>

Expand All @@ -46,9 +77,33 @@ export const UserSchema = z.object({
Posts: PostSchema.array().nullable(),
})
export type User = z.infer<typeof UserSchema>
```

It also works without any validations.
export type Tree = {
Value: number,
Children: Tree[] | null,
}
export const TreeSchema: z.ZodType<Tree> = z.object({
Value: z.number(),
Children: z.lazy(() => TreeSchema).array().nullable(),
})

export const StringIntPairSchema = z.object({
First: z.string(),
Second: z.number(),
})
export type StringIntPair = z.infer<typeof StringIntPairSchema>

export const GenericPairIntBoolSchema = z.object({
First: z.number(),
Second: z.boolean(),
})
export type GenericPairIntBool = z.infer<typeof GenericPairIntBoolSchema>

export const PairMapStringIntBoolSchema = z.object({
items: z.record(z.string(), GenericPairIntBoolSchema).nullable(),
})
export type PairMapStringIntBool = z.infer<typeof PairMapStringIntBoolSchema>
```

### How we use it at Hypersequent

Expand All @@ -61,19 +116,19 @@ It also works without any validations.
converter := zen.NewConverter(make(map[string]zen.CustomFn))

{{range .TypesToGenerate}}
converter.AddType(types.{{.}}{})
converter.AddType(types.{{.}}{})
{{end}}

schema := converter.Export()
```

## Custom Types

You can pass type name mappings to custom conversion functions:
We can pass type name mappings to custom conversion functions:

```go
c := zen.NewConverter(map[string]zen.CustomFn{
"github.com/shopspring/decimal.Decimal": func (c *zen.Converter, t reflect.Type, s, g string, i int) string {
"github.com/shopspring/decimal.Decimal": func (c *zen.Converter, t reflect.Type, v string, i int) string {
// Shopspring's decimal type serialises to a string.
return "z.string()"
},
Expand All @@ -98,11 +153,10 @@ There are some custom types with tests in the "custom" directory.
The function signature for custom type handlers is:

```go
func(c *zen.Converter, t reflect.Type, typeName, genericTypeName string, indentLevel int) string
func(c *Converter, t reflect.Type, validate string, indent int) string
```

You can use the Converter to process nested types. The `genericTypeName` is the name of the `T` in `Generic[T]` and the
indent level is for passing to other converter APIs.
We can use `c` to process nested types. Indent level is for passing to other converter APIs.

## Supported validations

Expand Down
2 changes: 1 addition & 1 deletion custom/decimal/decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

var (
DecimalType = "github.com/shopspring/decimal.Decimal"
DecimalFunc = func(c *zen.Converter, t reflect.Type, s, g string, validate string, i int) string {
DecimalFunc = func(c *zen.Converter, t reflect.Type, validate string, i int) string {
// Shopspring's decimal type serialises to a string.
return "z.string()"
}
Expand Down
4 changes: 2 additions & 2 deletions custom/optional/optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

var (
OptionalType = "4d63.com/optional.Optional"
OptionalFunc = func(c *zen.Converter, t reflect.Type, s string, g string, validate string, i int) string {
return fmt.Sprintf("%s.optional().nullish()", c.ConvertType(t.Elem(), s, validate, i))
OptionalFunc = func(c *zen.Converter, t reflect.Type, validate string, i int) string {
return fmt.Sprintf("%s.optional().nullish()", c.ConvertType(t.Elem(), validate, i))
}
)
Loading
Loading