Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Value Codec Access #990

Merged
merged 5 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions pkg/codec/by_item_type_modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,75 @@ func NewByItemTypeModifier(modByItemType map[string]Modifier) (Modifier, error)
}

return &byItemTypeModifier{
modByitemType: modByItemType,
modByItemType: modByItemType,
enableNesting: false,
}, nil
}

// NewNestableByItemTypeModifier returns a Modifier that uses modByItemType to determine which Modifier to use for a
// given itemType. If itemType is structured as a dot-separated string like 'A.B.C', the first part 'A' will be used to
// match in the mod map and the remaining list will be provided to the found Modifier 'B.C'.
func NewNestableByItemTypeModifier(modByItemType map[string]Modifier) (Modifier, error) {
if modByItemType == nil {
modByItemType = map[string]Modifier{}
}

return &byItemTypeModifier{
modByItemType: modByItemType,
enableNesting: true,
}, nil
}

type byItemTypeModifier struct {
modByitemType map[string]Modifier
modByItemType map[string]Modifier
enableNesting bool
}

func (b *byItemTypeModifier) RetypeToOffChain(onChainType reflect.Type, itemType string) (reflect.Type, error) {
mod, ok := b.modByitemType[itemType]
// RetypeToOffChain attempts to apply a modifier using the provided itemType. To allow access to nested fields, this
// function returns an error if a modifier by the specified name is not found. If nesting is enabled, the itemType can
// be of the form `Path.To.Type` and this modifier will attempt to only match on `Path` to find a valid modifier.
func (m *byItemTypeModifier) RetypeToOffChain(onChainType reflect.Type, itemType string) (reflect.Type, error) {
head := itemType
tail := itemType

if m.enableNesting {
head, tail = ItemTyper(itemType).Next()
}

mod, ok := m.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) {
return b.transform(offChainValue, itemType, Modifier.TransformToOnChain)
func (m *byItemTypeModifier) TransformToOnChain(offChainValue any, itemType string) (any, error) {
return m.transform(offChainValue, itemType, Modifier.TransformToOnChain)
}

func (b *byItemTypeModifier) TransformToOffChain(onChainValue any, itemType string) (any, error) {
return b.transform(onChainValue, itemType, Modifier.TransformToOffChain)
func (m *byItemTypeModifier) TransformToOffChain(onChainValue any, itemType string) (any, error) {
return m.transform(onChainValue, itemType, Modifier.TransformToOffChain)
}

func (b *byItemTypeModifier) transform(
val any, itemType string, transform func(Modifier, any, string) (any, error)) (any, error) {
mod, ok := b.modByitemType[itemType]
func (m *byItemTypeModifier) transform(
val any,
itemType string,
transform func(Modifier, any, string) (any, error),
) (any, error) {
head := itemType
tail := itemType

if m.enableNesting {
head, tail = ItemTyper(itemType).Next()
}

mod, ok := m.modByItemType[head]
if !ok {
return nil, fmt.Errorf("%w: cannot find modifier for %s", types.ErrInvalidType, itemType)
}

return transform(mod, val, itemType)
return transform(mod, val, tail)
}

var _ Modifier = &byItemTypeModifier{}
66 changes: 53 additions & 13 deletions pkg/codec/byte_string_modifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,18 @@ type AddressModifier interface {
//
// The fields parameter specifies which fields within a struct should be modified. The AddressModifier
// is injected into the modifier to handle chain-specific logic during the contractReader relayer configuration.
func NewAddressBytesToStringModifier(fields []string, modifier AddressModifier) Modifier {
func NewAddressBytesToStringModifier(
fields []string,
modifier AddressModifier,
) Modifier {
return NewPathTraverseAddressBytesToStringModifier(fields, modifier, false)
}

func NewPathTraverseAddressBytesToStringModifier(
fields []string,
modifier AddressModifier,
enablePathTraverse bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enablePathTraverse is unused

) Modifier {
// bool is a placeholder value
fieldMap := map[string]bool{}
for _, field := range fields {
Expand All @@ -35,9 +46,10 @@ func NewAddressBytesToStringModifier(fields []string, modifier AddressModifier)
m := &bytesToStringModifier{
modifier: modifier,
modifierBase: modifierBase[bool]{
fields: fieldMap,
onToOffChainType: map[reflect.Type]reflect.Type{},
offToOnChainType: map[reflect.Type]reflect.Type{},
enablePathTraverse: enablePathTraverse,
fields: fieldMap,
onToOffChainType: map[reflect.Type]reflect.Type{},
offToOnChainType: map[reflect.Type]reflect.Type{},
},
}

Expand All @@ -60,7 +72,7 @@ type bytesToStringModifier struct {
modifierBase[bool]
}

func (t *bytesToStringModifier) RetypeToOffChain(onChainType reflect.Type, _ string) (tpe reflect.Type, err error) {
func (m *bytesToStringModifier) RetypeToOffChain(onChainType reflect.Type, _ string) (tpe reflect.Type, err error) {
defer func() {
// StructOf can panic if the fields are not valid
if r := recover(); r != nil {
Expand All @@ -70,11 +82,11 @@ func (t *bytesToStringModifier) RetypeToOffChain(onChainType reflect.Type, _ str
}()

// Attempt to retype using the shared functionality in modifierBase
offChainType, err := t.modifierBase.RetypeToOffChain(onChainType, "")
offChainType, err := m.modifierBase.RetypeToOffChain(onChainType, "")
if err != nil {
// Handle additional cases specific to bytesToStringModifier
if onChainType.Kind() == reflect.Array {
addrType := reflect.ArrayOf(t.modifier.Length(), reflect.TypeOf(byte(0)))
addrType := reflect.ArrayOf(m.modifier.Length(), reflect.TypeOf(byte(0)))
// Check for nested byte arrays (e.g., [n][20]byte)
if onChainType.Elem() == addrType.Elem() {
return reflect.ArrayOf(onChainType.Len(), reflect.TypeOf("")), nil
Expand All @@ -86,16 +98,44 @@ func (t *bytesToStringModifier) RetypeToOffChain(onChainType reflect.Type, _ str
}

// TransformToOnChain uses the AddressModifier for string-to-address conversion.
func (t *bytesToStringModifier) TransformToOnChain(offChainValue any, _ string) (any, error) {
return transformWithMaps(offChainValue, t.offToOnChainType, t.fields, noop, stringToAddressHookForOnChain(t.modifier))
func (m *bytesToStringModifier) TransformToOnChain(offChainValue any, itemType string) (any, error) {
offChainValue, itemType, err := m.modifierBase.selectType(offChainValue, m.offChainStructType, itemType)
if err != nil {
return nil, err
}

modified, err := transformWithMaps(offChainValue, m.offToOnChainType, m.fields, noop, stringToAddressHookForOnChain(m.modifier))
if err != nil {
return nil, err
}

if itemType != "" {
return valueForPath(reflect.ValueOf(modified), itemType)
}

return modified, nil
}

// TransformToOffChain uses the AddressModifier for address-to-string conversion.
func (t *bytesToStringModifier) TransformToOffChain(onChainValue any, _ string) (any, error) {
return transformWithMaps(onChainValue, t.onToOffChainType, t.fields,
addressTransformationAction(t.modifier.Length()),
addressToStringHookForOffChain(t.modifier),
func (m *bytesToStringModifier) TransformToOffChain(onChainValue any, itemType string) (any, error) {
onChainValue, itemType, err := m.modifierBase.selectType(onChainValue, m.onChainStructType, itemType)
if err != nil {
return nil, err
}

modified, err := transformWithMaps(onChainValue, m.onToOffChainType, m.fields,
addressTransformationAction(m.modifier.Length()),
addressToStringHookForOffChain(m.modifier),
)
if err != nil {
return nil, err
}

if itemType != "" {
return valueForPath(reflect.ValueOf(modified), itemType)
}

return modified, nil
}

// addressTransformationAction performs conversions over the fields we want to modify.
Expand Down
38 changes: 23 additions & 15 deletions pkg/codec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,17 @@ type ModifierConfig interface {
// The casing of the first character is ignored to allow compatibility
// of go convention for public fields and on-chain names.
type RenameModifierConfig struct {
Fields map[string]string
Fields map[string]string
EnablePathTraverse bool
}

func (r *RenameModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
for k, v := range r.Fields {
delete(r.Fields, k)
r.Fields[upperFirstCharacter(k)] = upperFirstCharacter(v)
}
return NewRenamer(r.Fields), nil

return NewPathTraverseRenamer(r.Fields, r.EnablePathTraverse), nil
}

func (r *RenameModifierConfig) MarshalJSON() ([]byte, error) {
Expand All @@ -130,7 +132,8 @@ func (r *RenameModifierConfig) MarshalJSON() ([]byte, error) {
// For example, if a struct has fields A and B, and you want to rename A to B,
// then you need to either also rename B or drop it.
type DropModifierConfig struct {
Fields []string
Fields []string
EnablePathTraverse bool
}

func (d *DropModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
Expand All @@ -140,7 +143,7 @@ func (d *DropModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modif
fields[upperFirstCharacter(f)] = fmt.Sprintf("dropFieldPrivateName%d", i)
}

return NewRenamer(fields), nil
return NewPathTraverseRenamer(fields, d.EnablePathTraverse), nil
}

func (d *DropModifierConfig) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -171,8 +174,9 @@ func (e *ElementExtractorModifierConfig) MarshalJSON() ([]byte, error) {
// HardCodeModifierConfig is used to hard code values into the map.
// Note that hard-coding values will override other values.
type HardCodeModifierConfig struct {
OnChainValues map[string]any
OffChainValues map[string]any
OnChainValues map[string]any
OffChainValues map[string]any
EnablePathTraverse bool
}

func (h *HardCodeModifierConfig) ToModifier(onChainHooks ...mapstructure.DecodeHookFunc) (Modifier, error) {
Expand All @@ -193,7 +197,7 @@ func (h *HardCodeModifierConfig) ToModifier(onChainHooks ...mapstructure.DecodeH
mapKeyToUpperFirst(h.OnChainValues)
mapKeyToUpperFirst(h.OffChainValues)

return NewHardCoder(h.OnChainValues, h.OffChainValues, onChainHooks...)
return NewPathTraverseHardCoder(h.OnChainValues, h.OffChainValues, h.EnablePathTraverse, onChainHooks...)
}

func (h *HardCodeModifierConfig) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -246,7 +250,8 @@ type PreCodecModifierConfig struct {
// If the path leads to an array, encoding will occur on every entry.
//
// Example: "a.b" -> "uint256 Value"
Fields map[string]string
Fields map[string]string
EnablePathTraverse bool
// Codecs is skipped in JSON serialization, it will be injected later.
// The map should be keyed using the value from "Fields" to a corresponding Codec that can encode/decode for it
// This allows encoding and decoding implementations to be handled outside of the modifier.
Expand All @@ -256,7 +261,7 @@ type PreCodecModifierConfig struct {
}

func (c *PreCodecModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
return NewPreCodec(c.Fields, c.Codecs)
return NewPathTraversePreCodec(c.Fields, c.Codecs, c.EnablePathTraverse)
}

func (c *PreCodecModifierConfig) MarshalJSON() ([]byte, error) {
Expand All @@ -268,14 +273,15 @@ func (c *PreCodecModifierConfig) MarshalJSON() ([]byte, error) {

// EpochToTimeModifierConfig is used to convert epoch seconds as uint64 fields on-chain to time.Time
type EpochToTimeModifierConfig struct {
Fields []string
Fields []string
EnablePathTraverse bool
}

func (e *EpochToTimeModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
for i, f := range e.Fields {
e.Fields[i] = upperFirstCharacter(f)
}
return NewEpochToTimeModifier(e.Fields), nil
return NewPathTraverseEpochToTimeModifier(e.Fields, e.EnablePathTraverse), nil
}

func (e *EpochToTimeModifierConfig) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -303,13 +309,14 @@ func (c *PropertyExtractorConfig) MarshalJSON() ([]byte, error) {
// AddressBytesToStringModifierConfig is used to transform address byte fields into string fields.
// It holds the list of fields that should be modified and the chain-specific logic to do the modifications.
type AddressBytesToStringModifierConfig struct {
Fields []string
Fields []string
EnablePathTraverse bool
// Modifier is skipped in JSON serialization, will be injected later.
Modifier AddressModifier `json:"-"`
}

func (c *AddressBytesToStringModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
return NewAddressBytesToStringModifier(c.Fields, c.Modifier), nil
return NewPathTraverseAddressBytesToStringModifier(c.Fields, c.Modifier, c.EnablePathTraverse), nil
}

func (c *AddressBytesToStringModifierConfig) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -374,7 +381,8 @@ func (c *AddressBytesToStringModifierConfig) MarshalJSON() ([]byte, error) {
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
Fields map[string]string
EnablePathTraverse bool
}

func (r *WrapperModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
Expand All @@ -383,7 +391,7 @@ func (r *WrapperModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Mo
// 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
return NewPathTraverseWrapperModifier(r.Fields, r.EnablePathTraverse), nil
}

func (r *WrapperModifierConfig) MarshalJSON() ([]byte, error) {
Expand Down
Loading
Loading