From c4128edb3e4e5a79114c6cbaceb46591f622d97d Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Wed, 15 Jan 2025 10:09:49 -0600 Subject: [PATCH] rename modifier functional for nested fields --- pkg/codec/by_item_type_modifier.go | 22 ++-- pkg/codec/encodings/struct.go | 58 +++++++++- pkg/codec/encodings/type_codec.go | 62 +++++++--- pkg/codec/encodings/type_codec_test.go | 31 ++++- pkg/codec/hard_coder.go | 4 +- pkg/codec/modifier_base.go | 149 +++++++++++++++++++++---- pkg/codec/renamer.go | 68 +++++++++-- pkg/codec/renamer_test.go | 39 +++++++ 8 files changed, 376 insertions(+), 57 deletions(-) diff --git a/pkg/codec/by_item_type_modifier.go b/pkg/codec/by_item_type_modifier.go index f68e73e9d..3009858ca 100644 --- a/pkg/codec/by_item_type_modifier.go +++ b/pkg/codec/by_item_type_modifier.go @@ -22,13 +22,17 @@ type byItemTypeModifier struct { modByitemType map[string]Modifier } +// RetypeToOffChain attempts to apply a modifier using the provided itemType. To allow access to nested fields, this +// function applies no modifications if a modifier by the specified name is not found. func (b *byItemTypeModifier) RetypeToOffChain(onChainType reflect.Type, itemType string) (reflect.Type, error) { - mod, ok := b.modByitemType[itemType] + head, tail := extendedItemType(itemType).next() + + mod, ok := b.modByitemType[head] if !ok { return nil, fmt.Errorf("%w: cannot find modifier for %s", types.ErrInvalidType, itemType) } - return mod.RetypeToOffChain(onChainType, itemType) + return mod.RetypeToOffChain(onChainType, tail) } func (b *byItemTypeModifier) TransformToOnChain(offChainValue any, itemType string) (any, error) { @@ -40,13 +44,17 @@ func (b *byItemTypeModifier) TransformToOffChain(onChainValue any, itemType stri } func (b *byItemTypeModifier) transform( - val any, itemType string, transform func(Modifier, any, string) (any, error)) (any, error) { - mod, ok := b.modByitemType[itemType] - if !ok { - return nil, fmt.Errorf("%w: cannot find modifier for %s", types.ErrInvalidType, itemType) + val any, + itemType string, + transform func(Modifier, any, string) (any, error), +) (any, error) { + head, tail := extendedItemType(itemType).next() + + if mod, ok := b.modByitemType[head]; ok { + return transform(mod, val, tail) } - return transform(mod, val, itemType) + return val, nil } var _ Modifier = &byItemTypeModifier{} diff --git a/pkg/codec/encodings/struct.go b/pkg/codec/encodings/struct.go index 946936c45..a7a474777 100644 --- a/pkg/codec/encodings/struct.go +++ b/pkg/codec/encodings/struct.go @@ -3,6 +3,7 @@ package encodings import ( "fmt" "reflect" + "strings" "github.com/smartcontractkit/chainlink-common/pkg/types" ) @@ -24,6 +25,8 @@ func NewStructCodec(fields []NamedTypeCodec) (c TopLevelCodec, err error) { sfs := make([]reflect.StructField, len(fields)) codecFields := make([]TypeCodec, len(fields)) + lookup := make(map[string]int) + for i, field := range fields { ft := field.Codec.GetType() if ft.Kind() != reflect.Pointer { @@ -35,18 +38,22 @@ func NewStructCodec(fields []NamedTypeCodec) (c TopLevelCodec, err error) { Name: field.Name, Type: ft, } + codecFields[i] = field.Codec + lookup[field.Name] = i } return &structCodec{ - fields: codecFields, - tpe: reflect.PointerTo(reflect.StructOf(sfs)), + fields: codecFields, + fieldLookup: lookup, + tpe: reflect.PointerTo(reflect.StructOf(sfs)), }, nil } type structCodec struct { - fields []TypeCodec - tpe reflect.Type + fields []TypeCodec + fieldLookup map[string]int + tpe reflect.Type } func (s *structCodec) Encode(value any, into []byte) ([]byte, error) { @@ -113,3 +120,46 @@ func (s *structCodec) SizeAtTopLevel(numItems int) (int, error) { } return size, nil } + +func (s *structCodec) FieldCodec(itemType string) (TypeCodec, error) { + path := extendedItemType(itemType) + + // itemType could recurse into nested structs + fieldName, tail := path.next() + if fieldName == "" { + return nil, fmt.Errorf("%w: field name required", types.ErrInvalidType) + } + + idx, ok := s.fieldLookup[fieldName] + if !ok { + return nil, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + } + + codec := s.fields[idx] + + if tail == "" { + return codec, nil + } + + structType, ok := codec.(StructTypeCodec) + if !ok { + return nil, fmt.Errorf("%w: extended path not traversable for type %s", types.ErrInvalidType, itemType) + } + + return structType.FieldCodec(tail) +} + +type extendedItemType string + +func (t extendedItemType) next() (string, string) { + if string(t) == "" { + return "", "" + } + + path := strings.Split(string(t), ".") + if len(path) == 1 { + return path[0], "" + } + + return path[0], strings.Join(path[1:], ".") +} diff --git a/pkg/codec/encodings/type_codec.go b/pkg/codec/encodings/type_codec.go index 1807df8c1..5b0d35b28 100644 --- a/pkg/codec/encodings/type_codec.go +++ b/pkg/codec/encodings/type_codec.go @@ -33,6 +33,11 @@ type TopLevelCodec interface { SizeAtTopLevel(numItems int) (int, error) } +type StructTypeCodec interface { + TypeCodec + FieldCodec(string) (TypeCodec, error) +} + // CodecFromTypeCodec maps TypeCodec to types.RemoteCodec, using the key as the itemType // If the TypeCodec is a TopLevelCodec, GetMaxEncodingSize and GetMaxDecodingSize will call SizeAtTopLevel instead of Size. type CodecFromTypeCodec map[string]TypeCodec @@ -45,9 +50,9 @@ type LenientCodecFromTypeCodec map[string]TypeCodec var _ types.RemoteCodec = &LenientCodecFromTypeCodec{} func (c CodecFromTypeCodec) CreateType(itemType string, _ bool) (any, error) { - ntcwt, ok := c[itemType] - if !ok { - return nil, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + ntcwt, err := getCodec(c, itemType) + if err != nil { + return nil, err } tpe := ntcwt.GetType() @@ -59,9 +64,9 @@ func (c CodecFromTypeCodec) CreateType(itemType string, _ bool) (any, error) { } func (c CodecFromTypeCodec) Encode(_ context.Context, item any, itemType string) ([]byte, error) { - ntcwt, ok := c[itemType] - if !ok { - return nil, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + ntcwt, err := getCodec(c, itemType) + if err != nil { + return nil, err } if item != nil { @@ -86,14 +91,15 @@ func (c CodecFromTypeCodec) Encode(_ context.Context, item any, itemType string) } func (c CodecFromTypeCodec) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) { - ntcwt, ok := c[itemType] - if !ok { - return 0, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + ntcwt, err := getCodec(c, itemType) + if err != nil { + return 0, err } if lp, ok := ntcwt.(TopLevelCodec); ok { return lp.SizeAtTopLevel(n) } + return ntcwt.Size(n) } @@ -121,11 +127,16 @@ func (c LenientCodecFromTypeCodec) Decode(ctx context.Context, raw []byte, into return decode(c, raw, into, itemType, false) } +func (c CodecFromTypeCodec) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error) { + return c.GetMaxEncodingSize(ctx, n, itemType) +} + func decode(c map[string]TypeCodec, raw []byte, into any, itemType string, exactSize bool) error { - ntcwt, ok := c[itemType] - if !ok { - return fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + ntcwt, err := getCodec(c, itemType) + if err != nil { + return err } + val, remaining, err := ntcwt.Decode(raw) if err != nil { return err @@ -138,6 +149,29 @@ func decode(c map[string]TypeCodec, raw []byte, into any, itemType string, exact return codec.Convert(reflect.ValueOf(val), reflect.ValueOf(into), nil) } -func (c CodecFromTypeCodec) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error) { - return c.GetMaxEncodingSize(ctx, n, itemType) +func getCodec(c map[string]TypeCodec, itemType string) (TypeCodec, error) { + // itemType could recurse into nested structs + path := extendedItemType(itemType) + + // itemType could recurse into nested structs + head, tail := path.next() + if head == "" { + return nil, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + } + + ntcwt, ok := c[head] + if !ok { + return nil, fmt.Errorf("%w: cannot find type %s", types.ErrInvalidType, itemType) + } + + if tail == "" { + return ntcwt, nil + } + + structType, ok := ntcwt.(StructTypeCodec) + if !ok { + return nil, fmt.Errorf("%w: extended path not traversable for type %s", types.ErrInvalidType, itemType) + } + + return structType.FieldCodec(tail) } diff --git a/pkg/codec/encodings/type_codec_test.go b/pkg/codec/encodings/type_codec_test.go index 874819ff2..f23ed90ec 100644 --- a/pkg/codec/encodings/type_codec_test.go +++ b/pkg/codec/encodings/type_codec_test.go @@ -4,6 +4,7 @@ import ( rawbin "encoding/binary" "math" "reflect" + "strings" "testing" "github.com/smartcontractkit/libocr/bigbigendian" @@ -122,6 +123,34 @@ func TestCodecFromTypeCodecs(t *testing.T) { assert.Equal(t, singleItemSize*2, actual) }) + + t.Run("CreateType works for nested struct values and modifiers", func(t *testing.T) { + itemType := strings.Join([]string{TestItemWithConfigExtra, "AccountStruct", "Account"}, ".") + ts := CreateTestStruct(0, biit) + c := biit.GetCodec(t) + + encoded, err := c.Encode(tests.Context(t), ts.AccountStruct.Account, itemType) + require.NoError(t, err) + + var actual []byte + require.NoError(t, c.Decode(tests.Context(t), encoded, &actual, itemType)) + + assert.Equal(t, ts.AccountStruct.Account, actual) + }) + + t.Run("CreateType works for nested struct values", func(t *testing.T) { + itemType := strings.Join([]string{TestItemType, "NestedDynamicStruct", "Inner", "S"}, ".") + ts := CreateTestStruct(0, biit) + c := biit.GetCodec(t) + + encoded, err := c.Encode(tests.Context(t), ts.NestedDynamicStruct.Inner.S, itemType) + require.NoError(t, err) + + var actual string + require.NoError(t, c.Decode(tests.Context(t), encoded, &actual, itemType)) + + assert.Equal(t, ts.NestedDynamicStruct.Inner.S, actual) + }) } type interfaceTesterBase struct{} @@ -319,7 +348,7 @@ func (b *bigEndianInterfaceTester) GetCodec(t *testing.T) types.Codec { modCodec, err := codec.NewModifierCodec(c, byTypeMod, codec.BigIntHook) require.NoError(t, err) - _, err = mod.RetypeToOffChain(reflect.PointerTo(testStruct.GetType()), TestItemWithConfigExtra) + _, err = mod.RetypeToOffChain(reflect.PointerTo(testStruct.GetType()), "") require.NoError(t, err) return modCodec diff --git a/pkg/codec/hard_coder.go b/pkg/codec/hard_coder.go index 9f946fa04..60e2633e7 100644 --- a/pkg/codec/hard_coder.go +++ b/pkg/codec/hard_coder.go @@ -2,6 +2,7 @@ package codec import ( "fmt" + "log" "reflect" "strings" @@ -81,7 +82,8 @@ func verifyHardCodeKeys(values map[string]any) error { return nil } -func (o *onChainHardCoder) TransformToOnChain(offChainValue any, _ string) (any, error) { +func (o *onChainHardCoder) TransformToOnChain(offChainValue any, itemType string) (any, error) { + log.Println(itemType) return transformWithMaps(offChainValue, o.offToOnChainType, o.onChain, hardCode, o.hooks...) } diff --git a/pkg/codec/modifier_base.go b/pkg/codec/modifier_base.go index 8a092fe9b..c50fe5245 100644 --- a/pkg/codec/modifier_base.go +++ b/pkg/codec/modifier_base.go @@ -17,9 +17,16 @@ type modifierBase[T any] struct { offToOnChainType map[reflect.Type]reflect.Type modifyFieldForInput func(pkgPath string, outputField *reflect.StructField, fullPath string, change T) error addFieldForInput func(pkgPath, name string, change T) reflect.StructField + onChainStructType reflect.Type + offChainStructType reflect.Type } +// RetypeToOffChain sets the on-chain and off-chain types for modifications. If itemType is empty, the type returned +// will be the full off-chain type and all type mappings will be reset. If itemType is not empty, retyping assumes a +// sub-field is expected and the off-chain type of the sub-field is returned with no modifications to internal type +// mappings. func (m *modifierBase[T]) RetypeToOffChain(onChainType reflect.Type, itemType string) (tpe reflect.Type, err error) { + // onChainType could be the entire struct or a sub-field type defer func() { // StructOf can panic if the fields are not valid if r := recover(); r != nil { @@ -27,48 +34,71 @@ func (m *modifierBase[T]) RetypeToOffChain(onChainType reflect.Type, itemType st err = fmt.Errorf("%w: %v", types.ErrInvalidType, r) } }() + + // if itemType is empty, store the type mappings + // if itemType is not empty, assume a sub-field property is expected to be extracted + onChainStructType := onChainType + if itemType != "" { + onChainStructType = m.onChainStructType + } + + // this will only work for the full on-chain struct type unless we cache the individual + // field types too. + if cached, ok := m.onToOffChainType[onChainStructType]; ok { + return typeForPath(cached, itemType) + } + if len(m.fields) == 0 { m.offToOnChainType[onChainType] = onChainType m.onToOffChainType[onChainType] = onChainType - return onChainType, nil - } + m.onChainStructType = onChainType + m.offChainStructType = onChainType - if cached, ok := m.onToOffChainType[onChainType]; ok { - return cached, nil + return typeForPath(onChainType, itemType) } var offChainType reflect.Type - switch onChainType.Kind() { + + // the onChainStructType here should always reference the full on-chain struct type + switch onChainStructType.Kind() { case reflect.Pointer: - elm, err := m.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = m.RetypeToOffChain(onChainStructType.Elem(), itemType); err != nil { return nil, err } offChainType = reflect.PointerTo(elm) case reflect.Slice: - elm, err := m.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = m.RetypeToOffChain(onChainStructType.Elem(), ""); err != nil { return nil, err } offChainType = reflect.SliceOf(elm) case reflect.Array: - elm, err := m.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = m.RetypeToOffChain(onChainStructType.Elem(), ""); err != nil { return nil, err } - offChainType = reflect.ArrayOf(onChainType.Len(), elm) + offChainType = reflect.ArrayOf(onChainStructType.Len(), elm) case reflect.Struct: - return m.getStructType(onChainType) + if offChainType, err = m.getStructType(onChainStructType); err != nil { + return nil, err + } default: - return nil, fmt.Errorf("%w: cannot retype the kind %v", types.ErrInvalidType, onChainType.Kind()) + return nil, fmt.Errorf("%w: cannot retype the kind %v", types.ErrInvalidType, onChainStructType.Kind()) } - m.onToOffChainType[onChainType] = offChainType - m.offToOnChainType[offChainType] = onChainType - return offChainType, nil + m.onToOffChainType[onChainStructType] = offChainType + m.offToOnChainType[offChainType] = onChainStructType + m.onChainStructType = onChainType + m.offChainStructType = offChainType + + return typeForPath(offChainType, itemType) } func (m *modifierBase[T]) getStructType(outputType reflect.Type) (reflect.Type, error) { @@ -78,10 +108,11 @@ func (m *modifierBase[T]) getStructType(outputType reflect.Type) (reflect.Type, } for _, key := range m.subkeysFirst() { + curLocations := filedLocations parts := strings.Split(key, ".") fieldName := parts[len(parts)-1] + parts = parts[:len(parts)-1] - curLocations := filedLocations for _, part := range parts { if curLocations, err = curLocations.populateSubFields(part); err != nil { return nil, err @@ -102,10 +133,7 @@ func (m *modifierBase[T]) getStructType(outputType reflect.Type) (reflect.Type, } } - newStruct := filedLocations.makeNewType() - m.onToOffChainType[outputType] = newStruct - m.offToOnChainType[newStruct] = outputType - return newStruct, nil + return filedLocations.makeNewType(), nil } // subkeysFirst returns a list of keys that will always have a sub-key before the key if both are present @@ -122,6 +150,34 @@ func (m *modifierBase[T]) subkeysFirst() []string { return orderedKeys } +func (m *modifierBase[T]) onToOffChainTyper(onChainType reflect.Type, itemType string) (reflect.Type, error) { + onChainRefType := onChainType + if itemType != "" { + onChainRefType = m.onChainStructType + } + + offChainType, ok := m.onToOffChainType[onChainRefType] + if !ok { + return nil, fmt.Errorf("%w: cannot rename unknown type %v", types.ErrInvalidType, onChainType) + } + + return typeForPath(offChainType, itemType) +} + +func (m *modifierBase[T]) offToOnChainTyper(offChainType reflect.Type, itemType string) (reflect.Type, error) { + offChainRefType := offChainType + if itemType != "" { + offChainRefType = m.offChainStructType + } + + onChainType, ok := m.offToOnChainType[offChainRefType] + if !ok { + return nil, fmt.Errorf("%w: cannot rename unknown type %v", types.ErrInvalidType, offChainType) + } + + return typeForPath(onChainType, itemType) +} + // subkeysLast returns a list of keys that will always have a sub-key after the key if both are present func subkeysLast[T any](fields map[string]T) []string { orderedKeys := make([]string, 0, len(fields)) @@ -130,6 +186,7 @@ func subkeysLast[T any](fields map[string]T) []string { } sort.Strings(orderedKeys) + return orderedKeys } @@ -264,6 +321,39 @@ func doForMapElements[T any](valueMapping map[string]any, fields map[string]T, f return nil } +func typeForPath(from reflect.Type, itemType string) (reflect.Type, error) { + if itemType == "" { + return from, nil + } + + switch from.Kind() { + case reflect.Pointer: + elem, err := typeForPath(from.Elem(), itemType) + if err != nil { + return nil, err + } + + return elem, nil + case reflect.Array, reflect.Slice: + return nil, fmt.Errorf("%w: cannot extract a field from an array or slice", types.ErrInvalidType) + case reflect.Struct: + head, tail := extendedItemType(itemType).next() + + field, ok := from.FieldByName(head) + if !ok { + return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) + } + + if tail == "" { + return field.Type, nil + } + + return typeForPath(field.Type, tail) + default: + return nil, fmt.Errorf("%w: cannot extract a field from kind %s", types.ErrInvalidType, from.Kind()) + } +} + type PathMappingError struct { Err error Path string @@ -276,3 +366,18 @@ func (e PathMappingError) Error() string { func (e PathMappingError) Cause() error { return e.Err } + +type extendedItemType string + +func (t extendedItemType) next() (string, string) { + if string(t) == "" { + return "", "" + } + + path := strings.Split(string(t), ".") + if len(path) == 1 { + return path[0], "" + } + + return path[0], strings.Join(path[1:], ".") +} diff --git a/pkg/codec/renamer.go b/pkg/codec/renamer.go index b2414964a..845abeef4 100644 --- a/pkg/codec/renamer.go +++ b/pkg/codec/renamer.go @@ -2,7 +2,9 @@ package codec import ( "fmt" + "log" "reflect" + "strings" "unicode" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -30,26 +32,72 @@ type renamer struct { modifierBase[string] } -func (r *renamer) TransformToOffChain(onChainValue any, _ string) (any, error) { - rOutput, err := renameTransform(r.onToOffChainType, reflect.ValueOf(onChainValue)) +func (r *renamer) TransformToOffChain(onChainValue any, itemType string) (any, error) { + // itemType references the on-chain type + // remap to the off-chain field name + if itemType != "" { + var ref string + + parts := strings.Split(itemType, ".") + if len(parts) > 0 { + ref = parts[len(parts)-1] + } + + for on, off := range r.fields { + if ref == on { + // B.A -> C == B.C + parts[len(parts)-1] = off + itemType = strings.Join(parts, ".") + + break + } + } + } + + rOutput, err := renameTransform(r.onToOffChainTyper, reflect.ValueOf(onChainValue), itemType) if err != nil { return nil, err } + return rOutput.Interface(), nil } -func (r *renamer) TransformToOnChain(offChainValue any, _ string) (any, error) { - rOutput, err := renameTransform(r.offToOnChainType, reflect.ValueOf(offChainValue)) +func (r *renamer) TransformToOnChain(offChainValue any, itemType string) (any, error) { + log.Println(itemType) + if itemType != "" { + log.Println(itemType) + var ref string + + parts := strings.Split(itemType, ".") + if len(parts) > 0 { + ref = parts[len(parts)-1] + } + + for on, off := range r.fields { + if ref == off { + itemType = on + + break + } + } + } + + rOutput, err := renameTransform(r.offToOnChainTyper, reflect.ValueOf(offChainValue), itemType) if err != nil { return nil, err } + return rOutput.Interface(), nil } -func renameTransform(typeMap map[reflect.Type]reflect.Type, rInput reflect.Value) (reflect.Value, error) { - toType, ok := typeMap[rInput.Type()] - if !ok { - return reflect.Value{}, fmt.Errorf("%w: cannot rename unknown type %v", types.ErrInvalidType, toType) +func renameTransform( + typeFunc func(reflect.Type, string) (reflect.Type, error), + rInput reflect.Value, + itemType string, +) (reflect.Value, error) { + toType, err := typeFunc(rInput.Type(), itemType) + if err != nil { + return reflect.Value{}, err } if toType == rInput.Type() { @@ -70,6 +118,10 @@ func transformNonPointer(toType reflect.Type, rInput reflect.Value) (reflect.Val // make sure the input is addressable ptr := reflect.New(rInput.Type()) reflect.Indirect(ptr).Set(rInput) + + // UnsafePointer is a bit of a Go hack but works because the data types/structure and data for the two types + // are the same. The only change is the names of the fields. changed := reflect.NewAt(toType, ptr.UnsafePointer()).Elem() + return changed, nil } diff --git a/pkg/codec/renamer_test.go b/pkg/codec/renamer_test.go index 55453ff16..62fe47bee 100644 --- a/pkg/codec/renamer_test.go +++ b/pkg/codec/renamer_test.go @@ -385,6 +385,45 @@ func TestRenamer(t *testing.T) { require.NoError(t, err) assert.Equal(t, iOffchain.Interface(), newInput) }) + + t.Run("TransformToOnChain and TransformToOffChain works on nested fields even if the field itself is renamed for path", func(t *testing.T) { + offChainType, err := nestedRenamer.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "") + require.NoError(t, err) + iOffchain := reflect.Indirect(reflect.New(offChainType)) + + iOffchain.FieldByName("X").SetString("foo") + rY := iOffchain.FieldByName("Y") + rY.FieldByName("X").SetString("foo") + rY.FieldByName("B").SetInt(10) + rY.FieldByName("Z").SetInt(20) + + rC := iOffchain.FieldByName("C") + rC.Set(reflect.MakeSlice(rC.Type(), 2, 2)) + iElm := rC.Index(0) + iElm.FieldByName("X").SetString("foo") + iElm.FieldByName("B").SetInt(10) + iElm.FieldByName("Z").SetInt(20) + iElm = rC.Index(1) + iElm.FieldByName("X").SetString("baz") + iElm.FieldByName("B").SetInt(15) + iElm.FieldByName("Z").SetInt(25) + + iOffchain.FieldByName("D").SetString("bar") + + output, err := nestedRenamer.TransformToOnChain(iOffchain.FieldByName("Y").Interface(), "Y") + + require.NoError(t, err) + + expected := testStruct{ + A: "foo", + B: 10, + C: 20, + } + assert.Equal(t, expected, output) + newInput, err := nestedRenamer.TransformToOffChain(expected, "B") + require.NoError(t, err) + assert.Equal(t, iOffchain.FieldByName("Y").Interface(), newInput) + }) } func assertBasicRenameTransform(t *testing.T, offChainType reflect.Type) {