From d5e3303525b5237cbcaae1a8d09fb80c5b711022 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 09:57:50 -0700 Subject: [PATCH 1/7] Adds `maps.ForEach` --- internal/maps/maps.go | 12 +++++++++ internal/maps/maps_test.go | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 internal/maps/maps.go create mode 100644 internal/maps/maps_test.go diff --git a/internal/maps/maps.go b/internal/maps/maps.go new file mode 100644 index 000000000000..caa1cc633535 --- /dev/null +++ b/internal/maps/maps.go @@ -0,0 +1,12 @@ +package maps + +// ApplyToAll returns a new map containing the results of applying the function `f` to each element of the original map `m`. +func ApplyToAll[K comparable, T, U any](m map[K]T, f func(T) U) map[K]U { + n := make(map[K]U, len(m)) + + for k, v := range m { + n[k] = f(v) + } + + return n +} diff --git a/internal/maps/maps_test.go b/internal/maps/maps_test.go new file mode 100644 index 000000000000..39463729aa34 --- /dev/null +++ b/internal/maps/maps_test.go @@ -0,0 +1,52 @@ +package maps + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestApplyToAll(t *testing.T) { + t.Parallel() + + type testCase struct { + input map[int]string + expected map[int]string + } + tests := map[string]testCase{ + "three elements": { + input: map[int]string{ + 1: "one", + 2: "two", + 3: "3"}, + expected: map[int]string{ + 1: "ONE", + 2: "TWO", + 3: "3"}, + }, + "one element": { + input: map[int]string{ + 123: "abcdEFGH"}, + expected: map[int]string{ + 123: "ABCDEFGH"}, + }, + "zero elements": { + input: map[int]string{}, + expected: map[int]string{}, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := ApplyToAll(test.input, strings.ToUpper) + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} From e9fd4bf14bf1f30986d1474d4838b72375a89a73 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 09:59:34 -0700 Subject: [PATCH 2/7] Fixes marshalling of table item attributes to ignore `null` values when nested --- internal/service/dynamodb/flex.go | 68 ++++++++---- internal/service/dynamodb/flex_test.go | 142 +++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 internal/service/dynamodb/flex_test.go diff --git a/internal/service/dynamodb/flex.go b/internal/service/dynamodb/flex.go index 5509da8f46cd..044bacc93bbb 100644 --- a/internal/service/dynamodb/flex.go +++ b/internal/service/dynamodb/flex.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform-provider-aws/internal/maps" + "github.com/hashicorp/terraform-provider-aws/internal/slices" ) func ExpandTableItemAttributes(input string) (map[string]*dynamodb.AttributeValue, error) { @@ -24,34 +26,58 @@ func ExpandTableItemAttributes(input string) (map[string]*dynamodb.AttributeValu func flattenTableItemAttributes(attrs map[string]*dynamodb.AttributeValue) (string, error) { buf := bytes.NewBufferString("") encoder := json.NewEncoder(buf) - err := encoder.Encode(attrs) + + bar := make(map[string]foo, len(attrs)) + for k, v := range attrs { + bar[k] = foo(*v) + } + err := encoder.Encode(bar) if err != nil { return "", fmt.Errorf("Encoding failed: %s", err) } - var rawData map[string]map[string]interface{} + return buf.String(), nil +} - // Reserialize so we get rid of the nulls - decoder := json.NewDecoder(strings.NewReader(buf.String())) - err = decoder.Decode(&rawData) - if err != nil { - return "", fmt.Errorf("Decoding failed: %s", err) - } +type foo dynamodb.AttributeValue - for _, value := range rawData { - for typeName, typeVal := range value { - if typeVal == nil { - delete(value, typeName) - } - } - } +func (f foo) MarshalJSON() ([]byte, error) { + thing := map[string]any{} - rawBuffer := bytes.NewBufferString("") - rawEncoder := json.NewEncoder(rawBuffer) - err = rawEncoder.Encode(rawData) - if err != nil { - return "", fmt.Errorf("Re-encoding failed: %s", err) + if f.B != nil { + thing["B"] = f.B + } + if f.BS != nil { + thing["BS"] = f.BS + } + if f.BOOL != nil { + thing["BOOL"] = f.BOOL + } + if f.L != nil { + thing["L"] = slices.ApplyToAll(f.L, func(t *dynamodb.AttributeValue) foo { + return foo(*t) + }) + } + if f.M != nil { + thing["M"] = maps.ApplyToAll(f.M, func(t *dynamodb.AttributeValue) foo { + return foo(*t) + }) + } + if f.N != nil { + thing["N"] = f.N + } + if f.NS != nil { + thing["NS"] = f.NS + } + if f.NULL != nil { + thing["NULL"] = f.NULL + } + if f.S != nil { + thing["S"] = f.S + } + if f.SS != nil { + thing["SS"] = f.SS } - return rawBuffer.String(), nil + return json.Marshal(thing) } diff --git a/internal/service/dynamodb/flex_test.go b/internal/service/dynamodb/flex_test.go new file mode 100644 index 000000000000..c9cc6ebfb0ce --- /dev/null +++ b/internal/service/dynamodb/flex_test.go @@ -0,0 +1,142 @@ +package dynamodb + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" +) + +func TestFlattenTableItemAttributes(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + attrs map[string]*dynamodb.AttributeValue + expected string + }{ + "B": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + B: []byte("blob"), + }, + }, + expected: fmt.Sprintf(`{"attr":{"B":"%s"}}`, base64.StdEncoding.EncodeToString([]byte("blob"))), + }, + "BOOL": { + attrs: map[string]*dynamodb.AttributeValue{ + "true": { + BOOL: aws.Bool(true), + }, + "false": { + BOOL: aws.Bool(false), + }, + }, + expected: `{"true":{"BOOL":true},"false":{"BOOL":false}}`, + }, + "BS": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + BS: [][]byte{ + []byte("blob1"), + []byte("blob2"), + }, + }, + }, + expected: fmt.Sprintf(`{"attr":{"BS":["%[1]s","%[2]s"]}}`, + base64.StdEncoding.EncodeToString([]byte("blob1")), + base64.StdEncoding.EncodeToString([]byte("blob2")), + ), + }, + "L": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + L: []*dynamodb.AttributeValue{ + {S: aws.String("one")}, + {N: aws.String("2")}, + }, + }, + }, + expected: `{"attr":{"L":[{"S":"one"},{"N":"2"}]}}`, + }, + "M": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + M: map[string]*dynamodb.AttributeValue{ + "one": {S: aws.String("one")}, + "two": {N: aws.String("2")}, + }, + }, + }, + expected: `{"attr":{"M":{"one":{"S":"one"},"two":{"N":"2"}}}}`, + }, + "N": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + N: aws.String("123"), + }, + }, + expected: `{"attr":{"N":"123"}}`, + }, + "NS": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + NS: aws.StringSlice([]string{"42.2", "-19"}), + }, + }, + expected: `{"attr":{"NS":["42.2","-19"]}}`, + }, + "NULL": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + NULL: aws.Bool(true), + }, + }, + expected: `{"attr":{"NULL":true}}`, + }, + "S": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + S: aws.String("value"), + }, + }, + expected: `{"attr":{"S":"value"}}`, + }, + "SS": { + attrs: map[string]*dynamodb.AttributeValue{ + "attr": { + SS: aws.StringSlice([]string{"one", "two"}), + }, + }, + expected: `{"attr":{"SS":["one","two"]}}`, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, err := flattenTableItemAttributes(tc.attrs) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + e, err := structure.NormalizeJsonString(tc.expected) + if err != nil { + t.Fatalf("normalizing expected JSON: %s", err) + } + + a, err := structure.NormalizeJsonString(actual) + if err != nil { + t.Fatalf("normalizing returned JSON: %s", err) + } + + if a != e { + t.Fatalf("expected\n%s\ngot\n%s", e, a) + } + }) + } +} From 26c06dce40387de7da1a5a435dd29be77c39c13d Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 10:51:55 -0700 Subject: [PATCH 3/7] Suppresses equivalent JSON diffs on DynamoDB Table Items --- internal/service/dynamodb/table_item.go | 8 +- internal/service/dynamodb/table_item_test.go | 104 +++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/internal/service/dynamodb/table_item.go b/internal/service/dynamodb/table_item.go index 23cb96a7dfff..ab1ec945373a 100644 --- a/internal/service/dynamodb/table_item.go +++ b/internal/service/dynamodb/table_item.go @@ -45,9 +45,11 @@ func ResourceTableItem() *schema.Resource { Optional: true, }, "item": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validateTableItem, + Type: schema.TypeString, + Required: true, + ValidateFunc: validateTableItem, + DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs, + DiffSuppressOnRefresh: true, }, }, } diff --git a/internal/service/dynamodb/table_item_test.go b/internal/service/dynamodb/table_item_test.go index 6adf55db9e9b..66613d2e2377 100644 --- a/internal/service/dynamodb/table_item_test.go +++ b/internal/service/dynamodb/table_item_test.go @@ -391,6 +391,85 @@ func TestAccDynamoDBTableItem_disappears(t *testing.T) { }) } +func TestAccDynamoDBTableItem_mapOutOfBandUpdate(t *testing.T) { + ctx := acctest.Context(t) + var conf dynamodb.GetItemOutput + + tableName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + hashKey := "key" + tmpl := `{ + "key": {"S": "something"}, + "value": { + "M": { + "valid_after": { + "N": %[1]q + } + } + }, + "other": { + "N": %[1]q + } +}` + + oldValue := "300" + newValue := "400" + + oldItem := fmt.Sprintf(tmpl, oldValue) + newItem := fmt.Sprintf(tmpl, newValue) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, dynamodb.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableItemDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableItemConfig_map(tableName, hashKey, oldItem), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTableItemExists(ctx, "aws_dynamodb_table_item.test", &conf), + testAccCheckTableItemCount(ctx, tableName, 1), + acctest.CheckResourceAttrEquivalentJSON("aws_dynamodb_table_item.test", "item", oldItem), + acctest.CheckResourceAttrJMES("aws_dynamodb_table_item.test", "item", "value.M.valid_after.N", oldValue), + ), + }, + { + PreConfig: func() { + conn := acctest.Provider.Meta().(*conns.AWSClient).DynamoDBConn() + + attributes, err := tfdynamodb.ExpandTableItemAttributes(newItem) + if err != nil { + t.Fatalf("making out-of-band change: %s", err) + } + + updates := map[string]*dynamodb.AttributeValueUpdate{} + for key, value := range attributes { + if key == hashKey { + continue + } + updates[key] = &dynamodb.AttributeValueUpdate{ + Action: aws.String(dynamodb.AttributeActionPut), + Value: value, + } + } + + newQueryKey := tfdynamodb.BuildTableItemqueryKey(attributes, hashKey, "") + _, err = conn.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{ + AttributeUpdates: updates, + TableName: aws.String(tableName), + Key: newQueryKey, + }) + if err != nil { + t.Fatalf("making out-of-band change: %s", err) + } + }, + Config: testAccTableItemConfig_map(tableName, hashKey, newItem), + PlanOnly: true, + }, + }, + }) +} + func testAccCheckTableItemDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DynamoDBConn() @@ -607,3 +686,28 @@ ITEM } `, tableName, hashKey, rangeKey, item) } + +func testAccTableItemConfig_map(tableName, hashKey, content string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 10 + write_capacity = 10 + hash_key = %[2]q + + attribute { + name = %[2]q + type = "S" + } +} + +resource "aws_dynamodb_table_item" "test" { + table_name = aws_dynamodb_table.test.name + hash_key = aws_dynamodb_table.test.hash_key + + item = < Date: Thu, 13 Apr 2023 11:12:20 -0700 Subject: [PATCH 4/7] Cleanup --- internal/service/dynamodb/flex.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/service/dynamodb/flex.go b/internal/service/dynamodb/flex.go index 044bacc93bbb..e0f22593aa48 100644 --- a/internal/service/dynamodb/flex.go +++ b/internal/service/dynamodb/flex.go @@ -27,11 +27,11 @@ func flattenTableItemAttributes(attrs map[string]*dynamodb.AttributeValue) (stri buf := bytes.NewBufferString("") encoder := json.NewEncoder(buf) - bar := make(map[string]foo, len(attrs)) + a := make(map[string]attributeValue, len(attrs)) for k, v := range attrs { - bar[k] = foo(*v) + a[k] = attributeValue(*v) } - err := encoder.Encode(bar) + err := encoder.Encode(a) if err != nil { return "", fmt.Errorf("Encoding failed: %s", err) } @@ -39,28 +39,28 @@ func flattenTableItemAttributes(attrs map[string]*dynamodb.AttributeValue) (stri return buf.String(), nil } -type foo dynamodb.AttributeValue +type attributeValue dynamodb.AttributeValue -func (f foo) MarshalJSON() ([]byte, error) { +func (f attributeValue) MarshalJSON() ([]byte, error) { thing := map[string]any{} if f.B != nil { thing["B"] = f.B } - if f.BS != nil { - thing["BS"] = f.BS - } if f.BOOL != nil { thing["BOOL"] = f.BOOL } + if f.BS != nil { + thing["BS"] = f.BS + } if f.L != nil { - thing["L"] = slices.ApplyToAll(f.L, func(t *dynamodb.AttributeValue) foo { - return foo(*t) + thing["L"] = slices.ApplyToAll(f.L, func(t *dynamodb.AttributeValue) attributeValue { + return attributeValue(*t) }) } if f.M != nil { - thing["M"] = maps.ApplyToAll(f.M, func(t *dynamodb.AttributeValue) foo { - return foo(*t) + thing["M"] = maps.ApplyToAll(f.M, func(t *dynamodb.AttributeValue) attributeValue { + return attributeValue(*t) }) } if f.N != nil { From 819dc1c1bc6dbdb63117bcb6e5ee04998a62392f Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 11:49:11 -0700 Subject: [PATCH 5/7] Fixes function name --- internal/service/dynamodb/table_item.go | 10 +++++----- internal/service/dynamodb/table_item_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/service/dynamodb/table_item.go b/internal/service/dynamodb/table_item.go index ab1ec945373a..bd02523f04bc 100644 --- a/internal/service/dynamodb/table_item.go +++ b/internal/service/dynamodb/table_item.go @@ -112,7 +112,7 @@ func resourceTableItemUpdate(ctx context.Context, d *schema.ResourceData, meta i if err != nil { return sdkdiag.AppendErrorf(diags, "updating DynamoDB Table Item (%s): %s", d.Id(), err) } - newQueryKey := BuildTableItemqueryKey(attributes, hashKey, rangeKey) + newQueryKey := BuildTableItemQueryKey(attributes, hashKey, rangeKey) updates := map[string]*dynamodb.AttributeValueUpdate{} for key, value := range attributes { @@ -154,7 +154,7 @@ func resourceTableItemUpdate(ctx context.Context, d *schema.ResourceData, meta i // New record is created via UpdateItem in case we're changing hash key // so we need to get rid of the old one - oldQueryKey := BuildTableItemqueryKey(oldAttributes, hashKey, rangeKey) + oldQueryKey := BuildTableItemQueryKey(oldAttributes, hashKey, rangeKey) if !reflect.DeepEqual(oldQueryKey, newQueryKey) { log.Printf("[DEBUG] Deleting old record: %#v", oldQueryKey) _, err := conn.DeleteItemWithContext(ctx, &dynamodb.DeleteItemInput{ @@ -187,7 +187,7 @@ func resourceTableItemRead(ctx context.Context, d *schema.ResourceData, meta int return sdkdiag.AppendErrorf(diags, "reading DynamoDB Table Item (%s): %s", d.Id(), err) } - key := BuildTableItemqueryKey(attributes, hashKey, rangeKey) + key := BuildTableItemQueryKey(attributes, hashKey, rangeKey) result, err := FindTableItem(ctx, conn, tableName, key) if !d.IsNewResource() && tfresource.NotFound(err) { @@ -224,7 +224,7 @@ func resourceTableItemDelete(ctx context.Context, d *schema.ResourceData, meta i } hashKey := d.Get("hash_key").(string) rangeKey := d.Get("range_key").(string) - queryKey := BuildTableItemqueryKey(attributes, hashKey, rangeKey) + queryKey := BuildTableItemQueryKey(attributes, hashKey, rangeKey) _, err = conn.DeleteItemWithContext(ctx, &dynamodb.DeleteItemInput{ Key: queryKey, @@ -312,7 +312,7 @@ func buildTableItemID(tableName string, hashKey string, rangeKey string, attrs m return strings.Join(id, "|") } -func BuildTableItemqueryKey(attrs map[string]*dynamodb.AttributeValue, hashKey string, rangeKey string) map[string]*dynamodb.AttributeValue { +func BuildTableItemQueryKey(attrs map[string]*dynamodb.AttributeValue, hashKey string, rangeKey string) map[string]*dynamodb.AttributeValue { queryKey := map[string]*dynamodb.AttributeValue{ hashKey: attrs[hashKey], } diff --git a/internal/service/dynamodb/table_item_test.go b/internal/service/dynamodb/table_item_test.go index 66613d2e2377..f6575a970652 100644 --- a/internal/service/dynamodb/table_item_test.go +++ b/internal/service/dynamodb/table_item_test.go @@ -453,7 +453,7 @@ func TestAccDynamoDBTableItem_mapOutOfBandUpdate(t *testing.T) { } } - newQueryKey := tfdynamodb.BuildTableItemqueryKey(attributes, hashKey, "") + newQueryKey := tfdynamodb.BuildTableItemQueryKey(attributes, hashKey, "") _, err = conn.UpdateItemWithContext(ctx, &dynamodb.UpdateItemInput{ AttributeUpdates: updates, TableName: aws.String(tableName), @@ -485,7 +485,7 @@ func testAccCheckTableItemDestroy(ctx context.Context) resource.TestCheckFunc { return err } - key := tfdynamodb.BuildTableItemqueryKey(attributes, attrs["hash_key"], attrs["range_key"]) + key := tfdynamodb.BuildTableItemQueryKey(attributes, attrs["hash_key"], attrs["range_key"]) _, err = tfdynamodb.FindTableItem(ctx, conn, attrs["table_name"], key) @@ -523,7 +523,7 @@ func testAccCheckTableItemExists(ctx context.Context, n string, item *dynamodb.G return err } - key := tfdynamodb.BuildTableItemqueryKey(attributes, attrs["hash_key"], attrs["range_key"]) + key := tfdynamodb.BuildTableItemQueryKey(attributes, attrs["hash_key"], attrs["range_key"]) result, err := tfdynamodb.FindTableItem(ctx, conn, attrs["table_name"], key) From fde00ca3d1ff38847507ed6fc61cae04ca679099 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 12:08:37 -0700 Subject: [PATCH 6/7] Adds test for `ExpandTableItemAttributes` --- internal/service/dynamodb/flex_test.go | 164 +++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/internal/service/dynamodb/flex_test.go b/internal/service/dynamodb/flex_test.go index c9cc6ebfb0ce..3db08693a5ad 100644 --- a/internal/service/dynamodb/flex_test.go +++ b/internal/service/dynamodb/flex_test.go @@ -1,6 +1,7 @@ package dynamodb import ( + "bytes" "encoding/base64" "fmt" "testing" @@ -8,8 +9,171 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) +func TestExpandTableItemAttributes(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + input string + expected map[string]*dynamodb.AttributeValue + }{ + "B": { + input: fmt.Sprintf(`{"attr":{"B":"%s"}}`, base64.StdEncoding.EncodeToString([]byte("blob"))), + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + B: []byte("blob"), + }, + }, + }, + "BOOL": { + input: `{"true":{"BOOL":true},"false":{"BOOL":false}}`, + expected: map[string]*dynamodb.AttributeValue{ + "true": { + BOOL: aws.Bool(true), + }, + "false": { + BOOL: aws.Bool(false), + }, + }, + }, + "BS": { + input: fmt.Sprintf(`{"attr":{"BS":["%[1]s","%[2]s"]}}`, + base64.StdEncoding.EncodeToString([]byte("blob1")), + base64.StdEncoding.EncodeToString([]byte("blob2")), + ), + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + BS: [][]byte{ + []byte("blob1"), + []byte("blob2"), + }, + }, + }, + }, + "L": { + input: `{"attr":{"L":[{"S":"one"},{"N":"2"}]}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + L: []*dynamodb.AttributeValue{ + {S: aws.String("one")}, + {N: aws.String("2")}, + }, + }, + }, + }, + "M": { + input: `{"attr":{"M":{"one":{"S":"one"},"two":{"N":"2"}}}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + M: map[string]*dynamodb.AttributeValue{ + "one": {S: aws.String("one")}, + "two": {N: aws.String("2")}, + }, + }, + }, + }, + "N": { + input: `{"attr":{"N":"123"}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + N: aws.String("123"), + }, + }, + }, + "NS": { + input: `{"attr":{"NS":["42.2","-19"]}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + NS: aws.StringSlice([]string{"42.2", "-19"}), + }, + }, + }, + "NULL": { + input: `{"attr":{"NULL":true}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + NULL: aws.Bool(true), + }, + }, + }, + "S": { + input: `{"attr":{"S":"value"}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + S: aws.String("value"), + }, + }, + }, + "SS": { + input: `{"attr":{"SS":["one","two"]}}`, + expected: map[string]*dynamodb.AttributeValue{ + "attr": { + SS: aws.StringSlice([]string{"one", "two"}), + }, + }, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, err := ExpandTableItemAttributes(tc.input) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !maps.EqualFunc(actual, tc.expected, attributeValuesEqual) { + t.Fatalf("expected\n%s\ngot\n%s", tc.expected, actual) + } + }) + } +} + +func attributeValuesEqual(a, b *dynamodb.AttributeValue) bool { + if a.B != nil { + return bytes.Equal(a.B, b.B) + } + if a.BOOL != nil { + return b.BOOL != nil && aws.BoolValue(a.BOOL) == aws.BoolValue(b.BOOL) + } + if a.BS != nil { + return slices.EqualFunc(a.BS, b.BS, func(x, y []byte) bool { + return bytes.Equal(x, y) + }) + } + if a.L != nil { + return slices.EqualFunc(a.L, b.L, attributeValuesEqual) + } + if a.M != nil { + return maps.EqualFunc(a.M, b.M, attributeValuesEqual) + } + if a.N != nil { + return b.N != nil && aws.StringValue(a.N) == aws.StringValue(b.N) + } + if a.NS != nil { + return slices.EqualFunc(a.NS, b.NS, func(x, y *string) bool { + return aws.StringValue(x) == aws.StringValue(y) + }) + } + if a.NULL != nil { + return b.NULL != nil && aws.BoolValue(a.NULL) == aws.BoolValue(b.NULL) + } + if a.S != nil { + return b.S != nil && aws.StringValue(a.S) == aws.StringValue(b.S) + } + if a.SS != nil { + return slices.EqualFunc(a.SS, b.SS, func(x, y *string) bool { + return aws.StringValue(x) == aws.StringValue(y) + }) + } + return false +} + func TestFlattenTableItemAttributes(t *testing.T) { t.Parallel() From 98a034daa3eda917d42eee12363a2d7305388307 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 13 Apr 2023 12:15:37 -0700 Subject: [PATCH 7/7] Adds CHANGELOG entry --- .changelog/30712.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/30712.txt diff --git a/.changelog/30712.txt b/.changelog/30712.txt new file mode 100644 index 000000000000..df5906e8198b --- /dev/null +++ b/.changelog/30712.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_dynamodb_table_item: Would report spurious diffs when List and Map attributes were changed out-of-band +``` \ No newline at end of file