From 15c5bee0552195acd483d96cce1ac780333c07c9 Mon Sep 17 00:00:00 2001 From: ilija42 <57732589+ilija42@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:49:41 +0100 Subject: [PATCH] Add codec wrapper modifier (#905) * Add codec wrapper modifier * Fix WrapperModifierConfig description comment * Improve comments for wrapper modifier --- pkg/codec/config.go | 78 ++++++++ pkg/codec/wrapper.go | 62 ++++++ pkg/codec/wrapper_test.go | 390 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 530 insertions(+) create mode 100644 pkg/codec/wrapper.go create mode 100644 pkg/codec/wrapper_test.go diff --git a/pkg/codec/config.go b/pkg/codec/config.go index 27ac85500..a2fe37101 100644 --- a/pkg/codec/config.go +++ b/pkg/codec/config.go @@ -23,6 +23,7 @@ import ( // - extract element -> [ElementExtractorModifierConfig] // - epoch to time -> [EpochToTimeModifierConfig] // - address to string -> [AddressBytesToStringModifierConfig] +// - field wrapper -> [WrapperModifierConfig] type ModifiersConfig []ModifierConfig func (m *ModifiersConfig) UnmarshalJSON(data []byte) error { @@ -55,6 +56,8 @@ func (m *ModifiersConfig) UnmarshalJSON(data []byte) error { (*m)[i] = &PropertyExtractorConfig{} case ModifierAddressToString: (*m)[i] = &AddressBytesToStringModifierConfig{} + case ModifierWrapper: + (*m)[i] = &ModifiersConfig{} default: return fmt.Errorf("%w: unknown modifier type: %s", types.ErrInvalidConfig, mType) } @@ -88,6 +91,7 @@ const ( ModifierEpochToTime ModifierType = "epoch to time" ModifierExtractProperty ModifierType = "extract property" ModifierAddressToString ModifierType = "address to string" + ModifierWrapper ModifierType = "wrapper" ) type ModifierConfig interface { @@ -248,6 +252,80 @@ func (c *AddressBytesToStringModifierConfig) MarshalJSON() ([]byte, error) { }) } +// WrapperModifierConfig replaces each field based on cfg map keys with a struct containing one field with the value of the original field which has is named based on map values. +// Wrapper modifier does not maintain the original pointers. +// Wrapper modifier config shouldn't edit fields that affect each other since the results are not deterministic. +// +// Example #1: +// +// Based on this input struct: +// type example struct { +// A string +// } +// +// And the wrapper config defined as: +// {"D": "W"} +// +// Result: +// type example struct { +// D +// } +// +// where D is a struct that contains the original value of D under the name W: +// type D struct { +// W string +// } +// +// +// Example #2: +// Wrapper modifier works on any type of field, including nested fields or nested fields in slices etc.! +// +// Based on this input struct: +// type example struct { +// A []B +// } +// +// type B struct { +// C string +// D string +// } +// +// And the wrapper config defined as: +// {"A.C": "E", "A.D": "F"} +// +// Result: +// type example struct { +// A []B +// } +// +// type B struct { +// C type struct { E string } +// D type struct { F string } +// } +// +// Where each element of slice A under fields C.E and D.F retains the values of their respective input slice elements A.C and A.D . +type WrapperModifierConfig struct { + // Fields key defines the fields to be wrapped and the name of the wrapper struct. + // The field becomes a subfield of the wrapper struct where the name of the subfield is map value. + Fields map[string]string +} + +func (r *WrapperModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) { + fields := map[string]string{} + for i, f := range r.Fields { + // using a private variable will make the field not serialize, essentially dropping the field + fields[upperFirstCharacter(f)] = fmt.Sprintf("dropFieldPrivateName-%s", i) + } + return NewWrapperModifier(r.Fields), nil +} + +func (r *WrapperModifierConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(&modifierMarshaller[WrapperModifierConfig]{ + Type: ModifierWrapper, + T: r, + }) +} + type typer struct { Type string } diff --git a/pkg/codec/wrapper.go b/pkg/codec/wrapper.go new file mode 100644 index 000000000..dd1061244 --- /dev/null +++ b/pkg/codec/wrapper.go @@ -0,0 +1,62 @@ +package codec + +import ( + "fmt" + "reflect" +) + +func NewWrapperModifier(fields map[string]string) Modifier { + m := &wrapperModifier{ + modifierBase: modifierBase[string]{ + fields: fields, + onToOffChainType: map[reflect.Type]reflect.Type{}, + offToOnChainType: map[reflect.Type]reflect.Type{}, + }, + } + + m.modifyFieldForInput = func(_ string, field *reflect.StructField, _ string, fieldName string) error { + field.Type = reflect.StructOf([]reflect.StructField{{ + Name: fieldName, + Type: field.Type, + }}) + return nil + } + + return m +} + +type wrapperModifier struct { + modifierBase[string] +} + +func (t *wrapperModifier) TransformToOnChain(offChainValue any, _ string) (any, error) { + return transformWithMaps(offChainValue, t.offToOnChainType, t.fields, unwrapFieldMapAction) +} + +func (t *wrapperModifier) TransformToOffChain(onChainValue any, _ string) (any, error) { + return transformWithMaps(onChainValue, t.onToOffChainType, t.fields, wrapFieldMapAction) +} + +func wrapFieldMapAction(typesMap map[string]any, fieldName string, wrappedFieldName string) error { + field, exists := typesMap[fieldName] + if !exists { + return fmt.Errorf("field %s does not exist", fieldName) + } + + typesMap[fieldName] = map[string]any{wrappedFieldName: field} + return nil +} + +func unwrapFieldMapAction(typesMap map[string]any, fieldName string, wrappedFieldName string) error { + _, exists := typesMap[fieldName] + if !exists { + return fmt.Errorf("field %s does not exist", fieldName) + } + val, isOk := typesMap[fieldName].(map[string]any)[wrappedFieldName] + if !isOk { + return fmt.Errorf("field %s.%s does not exist", fieldName, wrappedFieldName) + } + + typesMap[fieldName] = val + return nil +} diff --git a/pkg/codec/wrapper_test.go b/pkg/codec/wrapper_test.go new file mode 100644 index 000000000..11bf148b6 --- /dev/null +++ b/pkg/codec/wrapper_test.go @@ -0,0 +1,390 @@ +package codec_test + +import ( + "errors" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +func TestWrapper(t *testing.T) { + t.Parallel() + + type testStruct struct { + A string + B int64 + C int64 + } + + type nestedTestStruct struct { + A string + B testStruct + C []testStruct + D string + } + + wrapper := codec.NewWrapperModifier(map[string]string{"A": "X", "C": "Z"}) + invalidWrapper := codec.NewWrapperModifier(map[string]string{"W": "X", "C": "Z"}) + nestedWrapper := codec.NewWrapperModifier(map[string]string{"A": "X", "B.A": "X", "B.C": "Z", "C.A": "X", "C.C": "Z"}) + t.Run("RetypeToOffChain works on slices", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf([]testStruct{}), "") + require.NoError(t, err) + + assert.Equal(t, reflect.Slice, offChainType.Kind()) + assertBasicWrapperTransform(t, offChainType.Elem()) + }) + + t.Run("RetypeToOffChain works on pointers", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf(&testStruct{}), "") + require.NoError(t, err) + + assert.Equal(t, reflect.Pointer, offChainType.Kind()) + assertBasicWrapperTransform(t, offChainType.Elem()) + }) + + t.Run("RetypeToOffChain works on pointers to non structs", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf(&[]testStruct{}), "") + require.NoError(t, err) + + assert.Equal(t, reflect.Pointer, offChainType.Kind()) + assert.Equal(t, reflect.Slice, offChainType.Elem().Kind()) + assertBasicWrapperTransform(t, offChainType.Elem().Elem()) + }) + + t.Run("RetypeToOffChain works on arrays", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf([2]testStruct{}), "") + require.NoError(t, err) + + assert.Equal(t, reflect.Array, offChainType.Kind()) + assert.Equal(t, 2, offChainType.Len()) + assertBasicWrapperTransform(t, offChainType.Elem()) + }) + + t.Run("RetypeToOffChain returns exception if a field is not on the type", func(t *testing.T) { + _, err := invalidWrapper.RetypeToOffChain(reflect.TypeOf(testStruct{}), "") + assert.True(t, errors.Is(err, types.ErrInvalidType)) + }) + + t.Run("RetypeToOffChain works on nested fields", func(t *testing.T) { + offChainType, err := nestedWrapper.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "") + require.NoError(t, err) + assert.Equal(t, 4, offChainType.NumField()) + + f0 := offChainType.Field(0) + f0PreRetype := reflect.TypeOf(nestedTestStruct{}).Field(0) + assert.Equal(t, wrapType("X", f0PreRetype.Type).String(), f0.Type.String()) + assert.Equal(t, "struct { A struct { X string }; B int64; C struct { Z int64 } }", offChainType.Field(1).Type.String()) + + f2 := offChainType.Field(2) + assert.Equal(t, reflect.Slice, f2.Type.Kind()) + assertBasicWrapperTransform(t, f2.Type.Elem()) + f3 := offChainType.Field(3) + assert.Equal(t, reflect.TypeOf(""), f3.Type) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on structs", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf(testStruct{}), "") + require.NoError(t, err) + iOffchain := reflect.Indirect(reflect.New(offChainType)) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + iOffchain.FieldByName("B").SetInt(10) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(20) + + output, err := wrapper.TransformToOnChain(iOffchain.Interface(), "") + require.NoError(t, err) + + expected := testStruct{ + A: "foo", + B: 10, + C: 20, + } + assert.Equal(t, expected, output) + newInput, err := wrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, iOffchain.Interface(), newInput) + }) + + t.Run("TransformToOnChain and TransformToOffChain returns error if input type was not from TransformToOnChain", func(t *testing.T) { + _, err := invalidWrapper.TransformToOnChain(testStruct{}, "") + assert.True(t, errors.Is(err, types.ErrInvalidType)) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on pointers, but doesn't maintain same addresses", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf(&testStruct{}), "") + require.NoError(t, err) + rInput := reflect.New(offChainType.Elem()) + iOffchain := reflect.Indirect(rInput) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + iOffchain.FieldByName("B").SetInt(10) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(20) + + output, err := wrapper.TransformToOnChain(rInput.Interface(), "") + require.NoError(t, err) + + expected := &testStruct{ + A: "foo", + B: 10, + C: 20, + } + assert.Equal(t, expected, output) + + newInput, err := wrapper.TransformToOffChain(output, "") + require.NoError(t, err) + assert.Equal(t, rInput.Interface(), newInput) + + }) + + t.Run("TransformToOnChain and TransformToOffChain works on slices", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf([]testStruct{}), "") + require.NoError(t, err) + rInput := reflect.MakeSlice(offChainType, 2, 2) + iOffchain := rInput.Index(0) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + iOffchain.FieldByName("B").SetInt(10) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(20) + iOffchain = rInput.Index(1) + iOffchain.FieldByName("A").FieldByName("X").SetString("baz") + iOffchain.FieldByName("B").SetInt(15) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(25) + + output, err := wrapper.TransformToOnChain(rInput.Interface(), "") + + require.NoError(t, err) + + expected := []testStruct{ + { + A: "foo", + B: 10, + C: 20, + }, + { + A: "baz", + B: 15, + C: 25, + }, + } + assert.Equal(t, expected, output) + + newInput, err := wrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, rInput.Interface(), newInput) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on nested slices", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf([][]testStruct{}), "") + require.NoError(t, err) + rInput := reflect.MakeSlice(offChainType, 2, 2) + rOuter := rInput.Index(0) + rOuter.Set(reflect.MakeSlice(rOuter.Type(), 2, 2)) + iOffchain := rOuter.Index(0) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + iOffchain.FieldByName("B").SetInt(10) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(20) + iOffchain = rOuter.Index(1) + iOffchain.FieldByName("A").FieldByName("X").SetString("baz") + iOffchain.FieldByName("B").SetInt(15) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(25) + rOuter = rInput.Index(1) + rOuter.Set(reflect.MakeSlice(rOuter.Type(), 2, 2)) + iOffchain = rOuter.Index(0) + iOffchain.FieldByName("A").FieldByName("X").SetString("fooz") + iOffchain.FieldByName("B").SetInt(100) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(200) + iOffchain = rOuter.Index(1) + iOffchain.FieldByName("A").FieldByName("X").SetString("bazz") + iOffchain.FieldByName("B").SetInt(150) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(250) + + output, err := wrapper.TransformToOnChain(rInput.Interface(), "") + + require.NoError(t, err) + + expected := [][]testStruct{ + { + { + A: "foo", + B: 10, + C: 20, + }, + { + A: "baz", + B: 15, + C: 25, + }, + }, + { + { + A: "fooz", + B: 100, + C: 200, + }, + { + A: "bazz", + B: 150, + C: 250, + }, + }, + } + assert.Equal(t, expected, output) + + newInput, err := wrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, rInput.Interface(), newInput) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on pointers to non structs", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf(&[]testStruct{}), "") + require.NoError(t, err) + rInput := reflect.New(offChainType.Elem()) + rElm := reflect.MakeSlice(offChainType.Elem(), 2, 2) + iElm := rElm.Index(0) + iElm.FieldByName("A").FieldByName("X").SetString("foo") + iElm.FieldByName("B").SetInt(10) + iElm.FieldByName("C").FieldByName("Z").SetInt(20) + iElm = rElm.Index(1) + iElm.FieldByName("A").FieldByName("X").SetString("baz") + iElm.FieldByName("B").SetInt(15) + iElm.FieldByName("C").FieldByName("Z").SetInt(25) + reflect.Indirect(rInput).Set(rElm) + + output, err := wrapper.TransformToOnChain(rInput.Interface(), "") + + require.NoError(t, err) + + expected := &[]testStruct{ + { + A: "foo", + B: 10, + C: 20, + }, + { + A: "baz", + B: 15, + C: 25, + }, + } + assert.Equal(t, expected, output) + + newInput, err := wrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, rInput.Interface(), newInput) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on arrays", func(t *testing.T) { + offChainType, err := wrapper.RetypeToOffChain(reflect.TypeOf([2]testStruct{}), "") + require.NoError(t, err) + rInput := reflect.New(offChainType).Elem() + iOffchain := rInput.Index(0) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + iOffchain.FieldByName("B").SetInt(10) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(20) + iOffchain = rInput.Index(1) + iOffchain.FieldByName("A").FieldByName("X").SetString("baz") + iOffchain.FieldByName("B").SetInt(15) + iOffchain.FieldByName("C").FieldByName("Z").SetInt(25) + + output, err := wrapper.TransformToOnChain(rInput.Interface(), "") + + require.NoError(t, err) + + expected := [2]testStruct{ + { + A: "foo", + B: 10, + C: 20, + }, + { + A: "baz", + B: 15, + C: 25, + }, + } + assert.Equal(t, expected, output) + + newInput, err := wrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, rInput.Interface(), newInput) + }) + + t.Run("TransformToOnChain and TransformToOffChain works on nested fields", func(t *testing.T) { + offChainType, err := nestedWrapper.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "") + require.NoError(t, err) + + iOffchain := reflect.Indirect(reflect.New(offChainType)) + iOffchain.FieldByName("A").FieldByName("X").SetString("foo") + rB := iOffchain.FieldByName("B") + assert.Equal(t, "struct { A struct { X string }; B int64; C struct { Z int64 } }", offChainType.Field(1).Type.String()) + + rB.FieldByName("A").FieldByName("X").SetString("foo") + rB.FieldByName("B").SetInt(10) + rB.FieldByName("C").FieldByName("Z").SetInt(20) + + rC := iOffchain.FieldByName("C") + rC.Set(reflect.MakeSlice(rC.Type(), 2, 2)) + iElm := rC.Index(0) + iElm.FieldByName("A").FieldByName("X").SetString("foo") + iElm.FieldByName("B").SetInt(10) + iElm.FieldByName("C").FieldByName("Z").SetInt(20) + iElm = rC.Index(1) + iElm.FieldByName("A").FieldByName("X").SetString("baz") + iElm.FieldByName("B").SetInt(15) + iElm.FieldByName("C").FieldByName("Z").SetInt(25) + + iOffchain.FieldByName("D").SetString("bar") + + output, err := nestedWrapper.TransformToOnChain(iOffchain.Interface(), "") + require.NoError(t, err) + + expected := nestedTestStruct{ + A: "foo", + B: testStruct{ + A: "foo", + B: 10, + C: 20, + }, + C: []testStruct{ + { + A: "foo", + B: 10, + C: 20, + }, + { + A: "baz", + B: 15, + C: 25, + }, + }, + D: "bar", + } + assert.Equal(t, expected, output) + newInput, err := nestedWrapper.TransformToOffChain(expected, "") + require.NoError(t, err) + assert.Equal(t, iOffchain.Interface(), newInput) + }) +} + +func assertBasicWrapperTransform(t *testing.T, offChainType reflect.Type) { + require.Equal(t, 3, offChainType.NumField()) + + f0 := offChainType.Field(0).Type.Field(0) + assert.Equal(t, wrapType(f0.Name, f0.Type).String(), offChainType.Field(0).Type.String()) + + f1 := offChainType.Field(1) + assert.Equal(t, reflect.TypeOf(int64(0)), f1.Type) + + f2 := offChainType.Field(2).Type.Field(0) + assert.Equal(t, wrapType(f2.Name, f2.Type).String(), offChainType.Field(2).Type.String()) +} + +func wrapType(name string, typ reflect.Type) reflect.Type { + wrapped := reflect.StructOf([]reflect.StructField{{ + Name: name, + Type: typ, + }}) + return wrapped +}