Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions cmd/go-sdk-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func main() {

func App() *cli.App {
return &cli.App{
Name: "sdkgen",
Usage: "Golang SDK generator.",
Name: "go-sdk-gen",
Usage: "Go SDK generator.",
DefaultCommand: "generate",
Before: func(ctx *cli.Context) error {
logger := slog.New(tint.NewHandler(os.Stderr, nil))
Expand Down
4 changes: 4 additions & 0 deletions pkg/builder/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ func (b *Builder) addBaseFiles(outDir string) error {
source: "version.go",
destination: "client/version.go",
},
{
source: "nullable.go",
destination: "nullable/field.go",
},
} {
fileName := path.Base(file.source)
dest := filepath.Join(outDir, file.destination)
Expand Down
23 changes: 16 additions & 7 deletions pkg/builder/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ func (b *Builder) pathsToResponseTypes(paths *openapi3.Paths) []Writable {
return paramTypes
}

func possiblyNullable(typ string, schema *openapi3.Schema) string {
if schema.Nullable {
return fmt.Sprintf("nullable.Field[%s]", typ)
}
return typ
}

// generateSchemaComponents generates types from schema reference.
// This should be used to generate top-level types, that is - named schemas that are listed
// in `#/components/schemas/` part of the OpenAPI specs.
Expand All @@ -269,28 +276,28 @@ func (b *Builder) generateSchemaComponents(name string, schema *openapi3.SchemaR
case spec.Type.Is("string"):
types = append(types, &TypeDeclaration{
Comment: schemaGodoc(name, spec),
Type: "string",
Type: possiblyNullable("string", spec),
Name: name,
Schema: spec,
})
case spec.Type.Is("integer"):
types = append(types, &TypeDeclaration{
Comment: schemaGodoc(name, spec),
Type: "int64",
Type: possiblyNullable("int64", spec),
Name: name,
Schema: spec,
})
case spec.Type.Is("number"):
types = append(types, &TypeDeclaration{
Comment: schemaGodoc(name, spec),
Type: "float64",
Type: possiblyNullable("float64", spec),
Name: name,
Schema: spec,
})
case spec.Type.Is("boolean"):
types = append(types, &TypeDeclaration{
Comment: schemaGodoc(name, spec),
Type: "bool",
Type: possiblyNullable("bool", spec),
Name: name,
Schema: spec,
})
Expand All @@ -299,7 +306,7 @@ func (b *Builder) generateSchemaComponents(name string, schema *openapi3.SchemaR
types = append(types, itemTypes...)
types = append(types, &TypeDeclaration{
Comment: schemaGodoc(name, spec),
Type: fmt.Sprintf("[]%s", typeName),
Type: possiblyNullable(fmt.Sprintf("[]%s", typeName), spec),
Name: name,
Schema: spec,
})
Expand Down Expand Up @@ -379,13 +386,13 @@ func (b *Builder) genSchema(schema *openapi3.SchemaRef, name string) (string, []
}
return stringx.MakeSingular(name), types
case spec.Type.Is("string"):
return formatStringType(schema.Value), nil
return possiblyNullable(formatStringType(schema.Value), spec), nil
case spec.Type.Is("integer"):
return "int", nil
case spec.Type.Is("number"):
return "float64", nil
case spec.Type.Is("boolean"):
return "bool", nil
return possiblyNullable("bool", spec), nil
case spec.Type.Is("array"):
typeName, schemas := b.genSchema(spec.Items, stringx.MakeSingular(name))
types = append(types, schemas...)
Expand Down Expand Up @@ -462,7 +469,9 @@ func (b *Builder) createFields(properties map[string]*openapi3.SchemaRef, name s
if !slices.Contains(required, property) {
tags = append(tags, "omitempty")
}

optional := !slices.Contains(required, property)

fields = append(fields, StructField{
Name: property,
Type: typeName,
Expand Down
5 changes: 5 additions & 0 deletions pkg/builder/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ func paramToString(name string, param *openapi3.Parameter) string {
return fmt.Sprintf("string(%s)", name)
}

if param.Schema.Value.Nullable {
name = strings.TrimPrefix(name, "*")
return fmt.Sprintf("%s.String()", name)
}

switch {
case param.Schema.Value.Type.Is("string"):
switch param.Schema.Value.Format {
Expand Down
65 changes: 65 additions & 0 deletions templates/nullable.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Code generated by `go-sdk-gen`. DO NOT EDIT.

package nullable

import (
"encoding/json"
"fmt"
)

var _ json.Marshaler = (*field[string])(nil)
var _ json.Unmarshaler = (*field[string])(nil)

// Field is a wrapper for nullable fields to distinguish zero values
// from null or omitted fields.
type field[T any] struct {
Value T
Null bool
Present bool
}

func (f field[T]) IsZero() bool {
return !f.Present
}

func (f field[T]) MarshalJSON() ([]byte, error) {
if f.Null {
return []byte("null"), nil // Explicitly set to null
}
return json.Marshal(f.Value)
}

func (f *field[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
f.Null = true
f.Present = true
var zeroValue T
f.Value = zeroValue // Reset value
return nil
}
f.Present = true
return json.Unmarshal(data, &f.Value)
}

func (f field[T]) String() string {
return fmt.Sprintf("%v", f.Value)
}

// Value is a nullable field helper for constructing a generic nullable field
// with a value.
func Value[T any](value T) field[T] { return field[T]{Value: value, Present: true} }

// Null is a nullable field helper for constructing a generic null fields.
func Null[T any]() field[T] { return field[T]{Null: true, Present: true} }

// Int is a nullable field helper for constructing nullable integers with a value.
func Int(value int) field[int] { return Value(value) }

// String is a nullable field helper for constructing nullable strings with a value.
func String(value string) field[string] { return Value(value) }

// Float is a nullable field helper for constructing nullable floats with a value.
func Float(value float64) field[float64] { return Value(value) }

// Bool is a nullable field helper for constructing nullable bools with a value.
func Bool(value bool) field[bool] { return Value(value) }
Loading