Skip to content

Commit

Permalink
autoflex: wire up option functions, add IgnoreFieldNames option (#36437)
Browse files Browse the repository at this point in the history
This change implements AutoFlex options, allowing custom options to be passed to `flex.Expand` or `flex.Flatten`. The changes include:

- Adding a new `AutoFlexOptions` struct, which stores all configurable options
- Embedding the `AutoFlexOptions` struct into the `autoExpand` and `autoFlatten` flexer structs
- Adjusting the `AutoFlexOptionsFunc` signature to accept a pointer to `AutoFlexOptions`

As a first use case, the `IgnoreFieldNames` option was added. This option is a slice of strings, with a default value of `[]string{"Tags"}`. This replaces the previously hardcoded logic which skipped processing of any fields named `Tags`, and instead makes this configurable. Test cases have been added to exercise the behavior with default options and overrides to remove defaults and add custom values.
  • Loading branch information
jar-b authored May 2, 2024
1 parent 8d11201 commit 376e1a7
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 24 deletions.
30 changes: 24 additions & 6 deletions internal/framework/flex/auto_expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ import (
// target data type) are copied.
func Expand(ctx context.Context, tfObject, apiObject any, optFns ...AutoFlexOptionsFunc) diag.Diagnostics {
var diags diag.Diagnostics
expander := &autoExpander{}

for _, optFn := range optFns {
optFn(expander)
}
expander := newAutoExpander(optFns)

diags.Append(autoFlexConvert(ctx, tfObject, apiObject, expander)...)
if diags.HasError() {
Expand All @@ -42,7 +38,29 @@ func Expand(ctx context.Context, tfObject, apiObject any, optFns ...AutoFlexOpti
return diags
}

type autoExpander struct{}
type autoExpander struct {
Options AutoFlexOptions
}

// newAutoExpander initializes an auto-expander with defaults that can be overridden
// via functional options
func newAutoExpander(optFns []AutoFlexOptionsFunc) *autoExpander {
o := AutoFlexOptions{
ignoredFieldNames: DefaultIgnoredFieldNames,
}

for _, optFn := range optFns {
optFn(&o)
}

return &autoExpander{
Options: o,
}
}

func (expander autoExpander) getOptions() AutoFlexOptions {
return expander.Options
}

// convert converts a single Plugin Framework value to its AWS API equivalent.
func (expander autoExpander) convert(ctx context.Context, valFrom, vTo reflect.Value) diag.Diagnostics {
Expand Down
85 changes: 84 additions & 1 deletion internal/framework/flex/auto_expand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -913,13 +913,96 @@ func TestExpandComplexNestedBlockWithStringEnum(t *testing.T) {
runAutoExpandTestCases(ctx, t, testCases)
}

func TestExpandOptions(t *testing.T) {
t.Parallel()

type tf01 struct {
Field1 types.Bool `tfsdk:"field1"`
Tags fwtypes.MapValueOf[types.String] `tfsdk:"tags"`
}
type aws01 struct {
Field1 bool
Tags map[string]string
}

ctx := context.Background()
testCases := autoFlexTestCases{
{
TestName: "empty source with tags",
Source: &tf01{},
Target: &aws01{},
WantTarget: &aws01{},
},
{
TestName: "ignore tags by default",
Source: &tf01{
Field1: types.BoolValue(true),
Tags: fwtypes.NewMapValueOfMust[types.String](
ctx,
map[string]attr.Value{
"foo": types.StringValue("bar"),
},
),
},
Target: &aws01{},
WantTarget: &aws01{Field1: true},
},
{
TestName: "include tags with option override",
Options: []AutoFlexOptionsFunc{
func(opts *AutoFlexOptions) {
opts.SetIgnoredFields([]string{})
},
},
Source: &tf01{
Field1: types.BoolValue(true),
Tags: fwtypes.NewMapValueOfMust[types.String](
ctx,
map[string]attr.Value{
"foo": types.StringValue("bar"),
},
),
},
Target: &aws01{},
WantTarget: &aws01{
Field1: true,
Tags: map[string]string{"foo": "bar"},
},
},
{
TestName: "ignore custom field",
Options: []AutoFlexOptionsFunc{
func(opts *AutoFlexOptions) {
opts.SetIgnoredFields([]string{"Field1"})
},
},
Source: &tf01{
Field1: types.BoolValue(true),
Tags: fwtypes.NewMapValueOfMust[types.String](
ctx,
map[string]attr.Value{
"foo": types.StringValue("bar"),
},
),
},
Target: &aws01{},
WantTarget: &aws01{
Tags: map[string]string{"foo": "bar"},
},
},
}
runAutoExpandTestCases(ctx, t, testCases)
}

type autoFlexTestCase struct {
Context context.Context //nolint:containedctx // testing context use
Options []AutoFlexOptionsFunc
TestName string
Source any
Target any
WantErr bool
WantTarget any
WantDiff bool
}

type autoFlexTestCases []autoFlexTestCase
Expand All @@ -937,7 +1020,7 @@ func runAutoExpandTestCases(ctx context.Context, t *testing.T, testCases autoFle
testCtx = testCase.Context
}

err := Expand(testCtx, testCase.Source, testCase.Target)
err := Expand(testCtx, testCase.Source, testCase.Target, testCase.Options...)
gotErr := err != nil

if gotErr != testCase.WantErr {
Expand Down
30 changes: 24 additions & 6 deletions internal/framework/flex/auto_flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@ import (
// suitable target data type) are copied.
func Flatten(ctx context.Context, apiObject, tfObject any, optFns ...AutoFlexOptionsFunc) diag.Diagnostics {
var diags diag.Diagnostics
flattener := &autoFlattener{}

for _, optFn := range optFns {
optFn(flattener)
}
flattener := newAutoFlattener(optFns)

diags.Append(autoFlexConvert(ctx, apiObject, tfObject, flattener)...)
if diags.HasError() {
Expand All @@ -46,7 +42,29 @@ func Flatten(ctx context.Context, apiObject, tfObject any, optFns ...AutoFlexOpt
return diags
}

type autoFlattener struct{}
type autoFlattener struct {
Options AutoFlexOptions
}

// newAutoFlattener initializes an auto-flattener with defaults that can be overridden
// via functional options
func newAutoFlattener(optFns []AutoFlexOptionsFunc) *autoFlattener {
o := AutoFlexOptions{
ignoredFieldNames: DefaultIgnoredFieldNames,
}

for _, optFn := range optFns {
optFn(&o)
}

return &autoFlattener{
Options: o,
}
}

func (flattener autoFlattener) getOptions() AutoFlexOptions {
return flattener.Options
}

// convert converts a single AWS API value to its Plugin Framework equivalent.
func (flattener autoFlattener) convert(ctx context.Context, vFrom, vTo reflect.Value) diag.Diagnostics {
Expand Down
107 changes: 105 additions & 2 deletions internal/framework/flex/auto_flatten_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,107 @@ func TestFlattenComplexNestedBlockWithFloat64(t *testing.T) {
runAutoFlattenTestCases(ctx, t, testCases)
}

func TestFlattenOptions(t *testing.T) {
t.Parallel()

type tf01 struct {
Field1 types.Bool `tfsdk:"field1"`
Tags fwtypes.MapValueOf[types.String] `tfsdk:"tags"`
}
type aws01 struct {
Field1 bool
Tags map[string]string
}

// For test cases below where a field of `MapValue` type is ignored, the
// result of `cmp.Diff` is intentionally not checked.
//
// When a target contains an ignored field of a `MapValue` type, the resulting
// target will contain a zero value, which, because the `elementType` is nil, will
// always return `false` from the `Equal` method, even when compared with another
// zero value. In practice, this zeroed `MapValue` would be overwritten
// by a subsequent step (ie. transparent tagging), and the temporary invalid
// state of the zeroed `MapValue` will not appear in the final state.
//
// Example expected diff:
// unexpected diff (+wanted, -got): &flex.tf01{
// Field1: s"false",
// - Tags: types.MapValueOf[github.com/hashicorp/terraform-plugin-framework/types/basetypes.StringValue]{},
// + Tags: types.MapValueOf[github.com/hashicorp/terraform-plugin-framework/types/basetypes.StringValue]{MapValue: basetypes.MapValue{elementType: basetypes.StringType{}}},
// }
ctx := context.Background()
testCases := autoFlexTestCases{
{
TestName: "empty source with tags",
Source: &aws01{},
Target: &tf01{},
WantTarget: &tf01{
Field1: types.BoolValue(false),
Tags: fwtypes.NewMapValueOfNull[types.String](ctx),
},
WantDiff: true, // Ignored MapValue type, expect diff
},
{
TestName: "ignore tags by default",
Source: &aws01{
Field1: true,
Tags: map[string]string{"foo": "bar"},
},
Target: &tf01{},
WantTarget: &tf01{
Field1: types.BoolValue(true),
Tags: fwtypes.NewMapValueOfNull[types.String](ctx),
},
WantDiff: true, // Ignored MapValue type, expect diff
},
{
TestName: "include tags with option override",
Options: []AutoFlexOptionsFunc{
func(opts *AutoFlexOptions) {
opts.SetIgnoredFields([]string{})
},
},
Source: &aws01{
Field1: true,
Tags: map[string]string{"foo": "bar"},
},
Target: &tf01{},
WantTarget: &tf01{
Field1: types.BoolValue(true),
Tags: fwtypes.NewMapValueOfMust[types.String](
ctx,
map[string]attr.Value{
"foo": types.StringValue("bar"),
},
),
},
},
{
TestName: "ignore custom field",
Options: []AutoFlexOptionsFunc{
func(opts *AutoFlexOptions) {
opts.SetIgnoredFields([]string{"Field1"})
},
},
Source: &aws01{
Field1: true,
Tags: map[string]string{"foo": "bar"},
},
Target: &tf01{},
WantTarget: &tf01{
Field1: types.BoolNull(),
Tags: fwtypes.NewMapValueOfMust[types.String](
ctx,
map[string]attr.Value{
"foo": types.StringValue("bar"),
},
),
},
},
}
runAutoFlattenTestCases(ctx, t, testCases)
}

func runAutoFlattenTestCases(ctx context.Context, t *testing.T, testCases autoFlexTestCases) {
t.Helper()

Expand All @@ -1065,7 +1166,7 @@ func runAutoFlattenTestCases(ctx context.Context, t *testing.T, testCases autoFl
testCtx = testCase.Context
}

err := Flatten(testCtx, testCase.Source, testCase.Target)
err := Flatten(testCtx, testCase.Source, testCase.Target, testCase.Options...)
gotErr := err != nil

if gotErr != testCase.WantErr {
Expand All @@ -1079,7 +1180,9 @@ func runAutoFlattenTestCases(ctx context.Context, t *testing.T, testCases autoFl
t.Errorf("err = %q", err)
}
} else if diff := cmp.Diff(testCase.Target, testCase.WantTarget, cmpopts.SortSlices(less)); diff != "" {
t.Errorf("unexpected diff (+wanted, -got): %s", diff)
if !testCase.WantDiff {
t.Errorf("unexpected diff (+wanted, -got): %s", diff)
}
}
})
}
Expand Down
Loading

0 comments on commit 376e1a7

Please sign in to comment.