diff --git a/cmd/go-sdk-gen/main.go b/cmd/go-sdk-gen/main.go index 53664f3..418838d 100644 --- a/cmd/go-sdk-gen/main.go +++ b/cmd/go-sdk-gen/main.go @@ -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)) diff --git a/pkg/builder/out.go b/pkg/builder/out.go index 7c8b280..9f17f75 100644 --- a/pkg/builder/out.go +++ b/pkg/builder/out.go @@ -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) diff --git a/pkg/builder/transform.go b/pkg/builder/transform.go index 8d3e9af..8bd5a9e 100644 --- a/pkg/builder/transform.go +++ b/pkg/builder/transform.go @@ -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. @@ -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, }) @@ -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, }) @@ -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...) @@ -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, diff --git a/pkg/builder/types.go b/pkg/builder/types.go index 5991888..01ea8b9 100644 --- a/pkg/builder/types.go +++ b/pkg/builder/types.go @@ -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 { diff --git a/templates/nullable.go.tmpl b/templates/nullable.go.tmpl new file mode 100644 index 0000000..29bbd2e --- /dev/null +++ b/templates/nullable.go.tmpl @@ -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) }