Skip to content

Commit

Permalink
Fix handling for nested generic types (#12)
Browse files Browse the repository at this point in the history
* Fix handling for nested generic types. For non-defined generic types,
  the type arguments are appended to the generated schema names
* Only keep necessary types/methods/functions as exported
* Update README
* Remove name and generic types arguments from CustomFn type. Their use
  was unclear and if required, can be derived from the type argument
* Update golangci-lint-action and golangci-lint
  • Loading branch information
hi-rai authored Aug 2, 2024
1 parent e8efb8f commit d0bf6eb
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 97 deletions.
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
89 changes: 72 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,65 @@ 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
as they are not supported by zod itself.

## 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 +78,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 +117,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 +154,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

0 comments on commit d0bf6eb

Please sign in to comment.