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

Add codec wrapper modifier #905

Merged
merged 5 commits into from
Nov 5, 2024
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
78 changes: 78 additions & 0 deletions pkg/codec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
62 changes: 62 additions & 0 deletions pkg/codec/wrapper.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading