Skip to content

Commit df29785

Browse files
authored
omit defaults in yaml serialization loop (#15701)
1 parent c890c3f commit df29785

File tree

8 files changed

+555
-85
lines changed

8 files changed

+555
-85
lines changed

mmv1/api/async.go

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,26 @@ package api
1515

1616
import (
1717
"log"
18+
"reflect"
1819
"strings"
1920

21+
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/utils"
2022
"golang.org/x/exp/slices"
2123
"gopkg.in/yaml.v3"
2224
)
2325

2426
// Base class from which other Async classes can inherit.
2527
type Async struct {
28+
// Describes an operation, one of "OpAsync", "PollAsync"
29+
Type string `yaml:"type,omitempty"`
30+
2631
// Describes an operation
27-
Operation *Operation
32+
Operation *Operation `yaml:"operation,omitempty"`
2833

2934
// The list of methods where operations are used.
30-
Actions []string
31-
32-
// Describes an operation, one of "OpAsync", "PollAsync"
33-
Type string
34-
35-
OpAsync `yaml:",inline"`
35+
Actions []string `yaml:"actions,omitempty"`
3636

37+
OpAsync `yaml:",inline"`
3738
PollAsync `yaml:",inline"`
3839
}
3940

@@ -70,11 +71,11 @@ func NewAsync() *Async {
7071

7172
// Represents an asynchronous operation definition
7273
type OpAsync struct {
73-
Result OpAsyncResult
74+
Result OpAsyncResult `yaml:"result,omitempty"`
7475

7576
// If true, include project as an argument to OperationWaitTime.
7677
// It is intended for resources that calculate project/region from a selflink field
77-
IncludeProject bool `yaml:"include_project"`
78+
IncludeProject bool `yaml:"include_project,omitempty"`
7879
}
7980

8081
type OpAsyncOperation struct {
@@ -110,26 +111,57 @@ type PollAsync struct {
110111
TargetOccurrences int `yaml:"target_occurrences,omitempty"`
111112
}
112113

114+
// newAsyncWithDefaults returns an Async object with default values set.
115+
func newAsyncWithDefaults() Async {
116+
a := Async{
117+
Actions: []string{"create", "delete", "update"},
118+
Type: "OpAsync",
119+
}
120+
return a
121+
}
122+
113123
func (a *Async) UnmarshalYAML(value *yaml.Node) error {
114-
a.Actions = []string{"create", "delete", "update"}
124+
// Start with a struct containing all the default values.
125+
*a = newAsyncWithDefaults()
126+
115127
type asyncAlias Async
116128
aliasObj := (*asyncAlias)(a)
117129

118-
err := value.Decode(aliasObj)
119-
if err != nil {
130+
if err := value.Decode(aliasObj); err != nil {
120131
return err
121132
}
122133

123-
if a.Type == "" {
124-
a.Type = "OpAsync"
125-
}
126134
if a.Type == "PollAsync" && a.TargetOccurrences == 0 {
127135
a.TargetOccurrences = 1
128136
}
129137

130138
return nil
131139
}
132140

141+
// MarshalYAML implements a custom marshaller for the Async struct.
142+
// It omits fields that are set to their default values.
143+
func (a *Async) MarshalYAML() (interface{}, error) {
144+
// Use a type alias to prevent infinite recursion during marshaling.
145+
type asyncAlias Async
146+
147+
// Create a defaults object that reflects the defaults for the current object's state.
148+
defaults := newAsyncWithDefaults()
149+
150+
// Use the generic helper for simple types. It returns a pointer to a clone.
151+
clone, err := utils.OmitDefaultsForMarshaling(*a, defaults)
152+
if err != nil {
153+
return nil, err
154+
}
155+
clonePtr := clone.(*Async)
156+
157+
// The helper ignores slices, so we handle `Actions` manually on the clone.
158+
if reflect.DeepEqual(clonePtr.Actions, defaults.Actions) {
159+
clonePtr.Actions = nil
160+
}
161+
162+
return (*asyncAlias)(clonePtr), nil
163+
}
164+
133165
func (a *Async) Validate() {
134166
if a.Type == "OpAsync" {
135167
if a.Operation == nil {

mmv1/api/resource.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ type Resource struct {
218218

219219
// Examples in documentation. Backed by generated tests, and have
220220
// corresponding OiCS walkthroughs.
221-
Examples []*resource.Examples
221+
Examples []*resource.Examples `yaml:"examples,omitempty"`
222222

223223
// Samples for generating tests and documentation
224224
Samples []*resource.Sample `yaml:"samples,omitempty"`
@@ -240,6 +240,8 @@ type Resource struct {
240240

241241
Timeouts *Timeouts `yaml:"timeouts,omitempty"`
242242

243+
Async *Async `yaml:"async,omitempty"`
244+
243245
// An array of function names that determine whether an error is retryable.
244246
ErrorRetryPredicates []string `yaml:"error_retry_predicates,omitempty"`
245247

@@ -305,8 +307,6 @@ type Resource struct {
305307
// Add a deprecation message for a resource that's been deprecated in the API.
306308
DeprecationMessage string `yaml:"deprecation_message,omitempty"`
307309

308-
Async *Async
309-
310310
// Tag autogen resources so that we can track them. In the future this will
311311
// control if a resource is continuously generated from public OpenAPI docs
312312
AutogenStatus string `yaml:"autogen_status"`
@@ -428,7 +428,35 @@ func (r *Resource) UnmarshalYAML(value *yaml.Node) error {
428428
return nil
429429
}
430430

431-
func (r *Resource) SetDefault(product *Product) {
431+
// // MarshalYAML implements a custom marshaller to omit dynamic default values.
432+
func (r *Resource) MarshalYAML() (interface{}, error) {
433+
type resourceAlias Resource
434+
435+
defaults := Resource{}
436+
437+
// Pre-populate fields needed for shallow default calculation.
438+
defaults.Name = r.Name
439+
defaults.ApiResourceTypeKind = r.ApiResourceTypeKind
440+
defaults.SelfLink = r.SelfLink
441+
defaults.MinVersion = r.MinVersion
442+
// Calculate shallow defaults.
443+
defaults.setShallowDefaults()
444+
defaults.Name = ""
445+
defaults.ApiResourceTypeKind = ""
446+
defaults.SelfLink = ""
447+
defaults.MinVersion = ""
448+
449+
clone, err := utils.OmitDefaultsForMarshaling(*r, defaults)
450+
if err != nil {
451+
return nil, err
452+
}
453+
454+
return (*resourceAlias)(clone.(*Resource)), nil
455+
}
456+
457+
// SetShallowDefaults calculates and sets default values for the immediate fields
458+
// of this Resource, without recursing into its children (Properties, etc.).
459+
func (r *Resource) setShallowDefaults() {
432460
if r.CreateVerb == "" {
433461
r.CreateVerb = "POST"
434462
}
@@ -462,20 +490,25 @@ func (r *Resource) SetDefault(product *Product) {
462490
}
463491
}
464492

493+
if r.IamPolicy != nil && r.IamPolicy.MinVersion == "" {
494+
r.IamPolicy.MinVersion = r.MinVersion
495+
}
496+
if r.Timeouts == nil {
497+
r.Timeouts = NewTimeouts() // This only sets defaults if Timeouts is nil
498+
}
499+
}
500+
501+
// SetDefault sets default values for this Resource and all its properties.
502+
func (r *Resource) SetDefault(product *Product) {
503+
r.setShallowDefaults() // Set defaults for the current level.
504+
465505
r.ProductMetadata = product
466506
for _, property := range r.AllProperties() {
467507
property.SetDefault(r)
468508
}
469509
for _, vf := range r.VirtualFields {
470510
vf.SetDefault(r)
471511
}
472-
if r.IamPolicy != nil && r.IamPolicy.MinVersion == "" {
473-
r.IamPolicy.MinVersion = r.MinVersion
474-
}
475-
if r.Timeouts == nil {
476-
r.Timeouts = NewTimeouts()
477-
}
478-
479512
}
480513

481514
func (r *Resource) Validate() {

mmv1/api/resource/examples.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strings"
2626
"text/template"
2727

28+
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/utils"
2829
"github.com/GoogleCloudPlatform/magic-modules/mmv1/google"
2930
"gopkg.in/yaml.v3"
3031
)
@@ -196,30 +197,25 @@ func (e *Examples) UnmarshalYAML(value *yaml.Node) error {
196197
}
197198

198199
// MarshalYAML implements a custom marshaller for the Examples struct.
199-
// It omits the ConfigPath field if it's equal to its default generated value.
200+
// It uses a generic helper to omit fields that are set to their default values.
200201
func (e *Examples) MarshalYAML() (interface{}, error) {
201-
// 1. Calculate the default value for ConfigPath based on the Name field.
202-
// This logic must mirror the one in UnmarshalYAML.
203-
defaultPath := ""
202+
// Use a type alias to prevent infinite recursion.
203+
type exampleAlias Examples
204+
205+
// Create a defaults object by unmarshalling an empty node, which populates defaults.
206+
// Here, we can create one manually based on the unmarshal logic.
207+
defaults := Examples{}
204208
if e.Name != "" {
205-
defaultPath = DefaultConfigPath(e.Name)
209+
defaults.ConfigPath = DefaultConfigPath(e.Name)
206210
}
207211

208-
// 2. Create a shallow copy to avoid modifying the original struct.
209-
// This prevents the marshaling operation from having side effects.
210-
clone := *e
211-
212-
// 3. If the current ConfigPath matches the default, clear it.
213-
// The `omitempty` tag will then cause the YAML marshaler to skip it.
214-
if clone.ConfigPath == defaultPath {
215-
clone.ConfigPath = ""
212+
// Use the generic helper to create a clone with default values zeroed out.
213+
clone, err := utils.OmitDefaultsForMarshaling(*e, defaults)
214+
if err != nil {
215+
return nil, err
216216
}
217217

218-
// 4. Use a type alias to prevent infinite recursion.
219-
// The alias `exampleAlias` does not have this MarshalYAML method,
220-
// so calling the marshaler on it will use the default struct marshaler.
221-
type exampleAlias Examples
222-
return (*exampleAlias)(&clone), nil
218+
return (*exampleAlias)(clone.(*Examples)), nil
223219
}
224220

225221
// DefaultConfigPath returns the default path for an example's Terraform config.

mmv1/api/resource/iam_policy.go

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"log"
1818
"slices"
1919

20+
"github.com/GoogleCloudPlatform/magic-modules/mmv1/api/utils"
2021
"gopkg.in/yaml.v3"
2122
)
2223

@@ -26,7 +27,7 @@ import (
2627
// See: https://cloud.google.com/iam/docs/overview
2728
type IamPolicy struct {
2829
// boolean of if this binding should be generated
29-
Exclude bool
30+
Exclude bool `yaml:"exclude,omitempty"`
3031

3132
// boolean of if this binding should be generated
3233
ExcludeTgc bool `yaml:"exclude_tgc,omitempty"`
@@ -81,7 +82,7 @@ type IamPolicy struct {
8182

8283
// Resource name may need a custom diff suppress function. Default is to use
8384
// CompareSelfLinkOrResourceName
84-
CustomDiffSuppress *string `yaml:"custom_diff_suppress"`
85+
CustomDiffSuppress *string `yaml:"custom_diff_suppress,omitempty"`
8586

8687
// Some resources (IAP) use fields named differently from the parent resource.
8788
// We need to use the parent's attributes to create an IAM policy, but they may not be
@@ -90,7 +91,7 @@ type IamPolicy struct {
9091
// config with the test/example attributes of the IAM resource.
9192
ExampleConfigBody string `yaml:"example_config_body,omitempty"`
9293

93-
SampleConfigBody string `yaml:"sample_config_body"`
94+
SampleConfigBody string `yaml:"sample_config_body,omitempty"`
9495

9596
// How the API supports IAM conditions
9697
IamConditionsRequestType string `yaml:"iam_conditions_request_type,omitempty"`
@@ -123,26 +124,53 @@ type IamPolicy struct {
123124
DeprecationMessage string `yaml:"deprecation_message,omitempty"`
124125
}
125126

127+
// newIamPolicyWithDefaults returns an IamPolicy object with default values set.
128+
func newIamPolicyWithDefaults() IamPolicy {
129+
return IamPolicy{
130+
MethodNameSeparator: "/",
131+
FetchIamPolicyVerb: "GET",
132+
FetchIamPolicyMethod: "getIamPolicy",
133+
SetIamPolicyVerb: "POST",
134+
SetIamPolicyMethod: "setIamPolicy",
135+
WrappedPolicyObj: true,
136+
AllowedIamRole: "roles/viewer",
137+
ParentResourceAttribute: "id",
138+
SubstituteZoneValue: true,
139+
}
140+
}
141+
142+
// UnmarshalYAML implements a custom unmarshaler for the IamPolicy struct.
143+
// It sets default values and then decodes the YAML over them.
126144
func (p *IamPolicy) UnmarshalYAML(value *yaml.Node) error {
127-
p.MethodNameSeparator = "/"
128-
p.FetchIamPolicyVerb = "GET"
129-
p.FetchIamPolicyMethod = "getIamPolicy"
130-
p.SetIamPolicyVerb = "POST"
131-
p.SetIamPolicyMethod = "setIamPolicy"
132-
p.WrappedPolicyObj = true
133-
p.AllowedIamRole = "roles/viewer"
134-
p.ParentResourceAttribute = "id"
135-
p.SubstituteZoneValue = true
145+
// Start with a struct containing all the default values.
146+
*p = newIamPolicyWithDefaults()
136147

148+
// Use a type alias to prevent recursion when decoding.
137149
type iamPolicyAlias IamPolicy
138150
aliasObj := (*iamPolicyAlias)(p)
139151

140-
err := value.Decode(aliasObj)
152+
// Decode the provided YAML, which will overwrite any default values that are specified.
153+
return value.Decode(aliasObj)
154+
}
155+
156+
// MarshalYAML implements a custom marshaller for the IamPolicy struct.
157+
// It uses a generic helper to omit fields that are set to their default values.
158+
func (p *IamPolicy) MarshalYAML() (interface{}, error) {
159+
// Use a type alias to prevent infinite recursion during marshaling.
160+
type iamPolicyAlias IamPolicy
161+
162+
// Create a defaults object and then use the generic helper to create a
163+
// clone with default values zeroed out for marshaling.
164+
defaults := newIamPolicyWithDefaults()
165+
clone, err := utils.OmitDefaultsForMarshaling(*p, defaults)
141166
if err != nil {
142-
return err
167+
return nil, err
143168
}
144169

145-
return nil
170+
// The helper returns an interface{} containing a pointer to the clone.
171+
// We cast it to the alias type to ensure the marshaller uses the default
172+
// behavior for the struct, but without this custom MarshalYAML method.
173+
return (*iamPolicyAlias)(clone.(*IamPolicy)), nil
146174
}
147175

148176
func (p *IamPolicy) Validate(rName string) {

0 commit comments

Comments
 (0)