Skip to content

Commit 4f29a25

Browse files
committed
Refactor: consolidate field logic in struct.
1 parent 24ae859 commit 4f29a25

2 files changed

Lines changed: 255 additions & 154 deletions

File tree

internal/generate/types.go

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,64 @@ type TypeTemplate struct {
3838

3939
// TypeFields holds the information for each type field
4040
type TypeFields struct {
41-
Description string
42-
Name string
43-
Type string
44-
SerializationInfo string
41+
Schema *openapi3.SchemaRef
42+
Name string
43+
Type string
44+
MarshalKey string
45+
Required bool
46+
47+
// FallbackDescription generates a generic description for the field when the Schema doesn't have one.
48+
// TODO: Drop this, since generated descriptions don't contain useful information.
49+
FallbackDescription bool
50+
51+
// OmitDirective overrides the derived omit directive for the field.
52+
// TODO: Drop this; we should set omit directives consistently rather than using overrides.
53+
OmitDirective string
54+
}
55+
56+
// Description returns the formatted description comment for this field.
57+
func (f TypeFields) Description() string {
58+
if f.Schema == nil {
59+
return ""
60+
}
61+
if f.Schema.Value.Description != "" {
62+
return fmt.Sprintf("// %s is %s", f.Name, toLowerFirstLetter(
63+
strings.ReplaceAll(f.Schema.Value.Description, "\n", "\n// ")))
64+
}
65+
if f.FallbackDescription {
66+
return fmt.Sprintf("// %s is the type definition for a %s.", f.Name, f.Name)
67+
}
68+
return ""
69+
}
70+
71+
// StructTag returns the JSON/YAML struct tags for this field.
72+
func (f TypeFields) StructTag() string {
73+
// Derive the omit directive.
74+
var omitDirective string
75+
switch {
76+
case f.OmitDirective != "":
77+
// Explicit override.
78+
omitDirective = f.OmitDirective
79+
case f.Schema == nil:
80+
// Special case: no schema (e.g., Body field).
81+
omitDirective = "omitempty"
82+
case f.Required || isNullableArray(f.Schema):
83+
// Required or nullable array: no directive (always serialize).
84+
omitDirective = ""
85+
case slices.Contains(omitzeroTypes(), f.Type):
86+
// Special types: omitzero.
87+
omitDirective = "omitzero"
88+
default:
89+
omitDirective = "omitempty"
90+
}
91+
92+
// Build the tag value.
93+
tagValue := f.MarshalKey
94+
if omitDirective != "" {
95+
tagValue = f.MarshalKey + "," + omitDirective
96+
}
97+
98+
return fmt.Sprintf("`json:\"%s\" yaml:\"%s\"`", tagValue, tagValue)
4599
}
46100

47101
// EnumTemplate holds the information for enum types
@@ -129,18 +183,18 @@ func constructParamTypes(paths map[string]*openapi3.PathItem) []TypeTemplate {
129183
}
130184

131185
paramName := strcase.ToCamel(p.Value.Name)
186+
paramType := convertToValidGoType("", "", p.Value.Schema)
132187
field := TypeFields{
133-
Name: paramName,
134-
Type: convertToValidGoType("", "", p.Value.Schema),
188+
Name: paramName,
189+
Type: paramType,
190+
MarshalKey: p.Value.Name,
191+
Schema: nil, // nil so StructTag always uses omitempty
135192
}
136193

137194
if p.Value.Required {
138195
requiredFields = requiredFields + fmt.Sprintf("\n// - %s", paramName)
139196
}
140197

141-
serInfo := fmt.Sprintf("`json:\"%s,omitempty\" yaml:\"%s,omitempty\"`", p.Value.Name, p.Value.Name)
142-
field.SerializationInfo = serInfo
143-
144198
fields = append(fields, field)
145199
}
146200
if o.RequestBody != nil {
@@ -151,17 +205,19 @@ func constructParamTypes(paths map[string]*openapi3.PathItem) []TypeTemplate {
151205
// TODO: Handle other mime types in a more idiomatic way
152206
if mt != "application/json" {
153207
field = TypeFields{
154-
Name: "Body",
155-
Type: "io.Reader",
156-
SerializationInfo: "`json:\"body,omitempty\" yaml:\"body,omitempty\"`",
208+
Name: "Body",
209+
Type: "io.Reader",
210+
MarshalKey: "body",
211+
Schema: nil, // no schema for non-JSON body
157212
}
158213
break
159214
}
160215

161216
field = TypeFields{
162-
Name: "Body",
163-
Type: "*" + convertToValidGoType("", "", r.Schema),
164-
SerializationInfo: "`json:\"body,omitempty\" yaml:\"body,omitempty\"`",
217+
Name: "Body",
218+
Type: "*" + convertToValidGoType("", "", r.Schema),
219+
MarshalKey: "body",
220+
Schema: nil, // Body uses special serialization
165221
}
166222
}
167223
// Body is always a required field
@@ -329,10 +385,10 @@ func writeTypes(f *os.File, typeCollection []TypeTemplate, typeValidationCollect
329385
if tt.Fields != nil {
330386
fmt.Fprint(f, " {\n")
331387
for _, ft := range tt.Fields {
332-
if ft.Description != "" {
333-
fmt.Fprintf(f, "\t%s\n", splitDocString(ft.Description))
388+
if desc := ft.Description(); desc != "" {
389+
fmt.Fprintf(f, "\t%s\n", splitDocString(desc))
334390
}
335-
fmt.Fprintf(f, "\t%s %s %s\n", ft.Name, ft.Type, ft.SerializationInfo)
391+
fmt.Fprintf(f, "\t%s %s %s\n", ft.Name, ft.Type, ft.StructTag())
336392
}
337393
fmt.Fprint(f, "}\n")
338394
}
@@ -529,29 +585,14 @@ func createTypeObject(schema *openapi3.Schema, name, typeName, description strin
529585
}
530586
}
531587

532-
field := TypeFields{}
533-
if v.Value.Description != "" {
534-
desc := fmt.Sprintf("// %s is %s", strcase.ToCamel(k), toLowerFirstLetter(strings.ReplaceAll(v.Value.Description, "\n", "\n// ")))
535-
field.Description = desc
536-
}
537-
538-
field.Name = strcase.ToCamel(k)
539-
field.Type = typeName
540-
541-
// Configure json/yaml struct tags. By default, omit empty/zero
542-
// values, but retain them for required fields.
543-
//
544-
// TODO: Use `omitzero` rather than `omitempty` on all relevant
545-
// fields: https://github.com/oxidecomputer/oxide.go/issues/290
546-
serInfo := fmt.Sprintf("`json:\"%s,omitempty\" yaml:\"%s,omitempty\"`", k, k)
547-
if isNullableArray(v) || isRequired {
548-
serInfo = fmt.Sprintf("`json:\"%s\" yaml:\"%s\"`", k, k)
549-
} else if slices.Contains(omitzeroTypes(), typeName) {
550-
serInfo = fmt.Sprintf("`json:\"%s,omitzero\" yaml:\"%s,omitzero\"`", k, k)
588+
field := TypeFields{
589+
Name: strcase.ToCamel(k),
590+
Type: typeName,
591+
MarshalKey: k,
592+
Schema: v,
593+
Required: isRequired,
551594
}
552595

553-
field.SerializationInfo = serInfo
554-
555596
fields = append(fields, field)
556597

557598
}
@@ -720,21 +761,24 @@ func createOneOf(s *openapi3.Schema, name, typeName string) ([]TypeTemplate, []E
720761

721762
// Avoids duplication for every enum
722763
if !containsMatchFirstWord(parsedProperties, propertyName) {
723-
field := TypeFields{
724-
Description: formatTypeDescription(propertyName, p.Value),
725-
Name: propertyName,
726-
Type: propertyType,
727-
SerializationInfo: fmt.Sprintf("`json:\"%s,omitempty\" yaml:\"%s,omitempty\"`", prop, prop),
728-
}
729-
730764
// We set the type of a field as "any" if every element of the oneOf property isn't the same
731765
if slices.Contains(genericTypes, prop) {
732-
field.Type = "any"
766+
propertyType = "any"
733767
}
734768

735-
// Check if the field is nullable and use omitzero instead of omitempty.
769+
// Determine omit directive: nullable fields in oneOf use omitzero.
770+
var omitDirective string
736771
if p.Value != nil && p.Value.Nullable {
737-
field.SerializationInfo = fmt.Sprintf("`json:\"%s,omitzero\" yaml:\"%s,omitzero\"`", prop, prop)
772+
omitDirective = "omitzero"
773+
}
774+
775+
field := TypeFields{
776+
Name: propertyName,
777+
Type: propertyType,
778+
MarshalKey: prop,
779+
Schema: p,
780+
FallbackDescription: true,
781+
OmitDirective: omitDirective,
738782
}
739783

740784
fields = append(fields, field)

0 commit comments

Comments
 (0)