Skip to content

Commit d68a079

Browse files
authored
Add features to wrapper and property extractor modifiers (#1098)
* Change hardcoder to support TransformToOffChain for primitive variables * Fix err handling in transformWithMapsHelper * Improve extractElement to work on uninitialised slices * Add a test for hardcoder TransformToOffChain for primitives variables * Handle uninitialised slices in property extractor and add tests * Improve transformWithMapsHelper err message * minor improvements * Improve err handling in initSliceForFieldPath * Use derefPtr helper
1 parent d0dcced commit d68a079

7 files changed

+150
-13
lines changed

pkg/codec/hard_coder.go

+9
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ func (m *onChainHardCoder) TransformToOffChain(onChainValue any, itemType string
137137
copy(allHooks, m.hooks)
138138
allHooks[len(m.hooks)] = hardCodeManyHook
139139

140+
// if there is only one field with unset name, then a primitive variable is being hardcoded
141+
if len(m.fields) == 1 {
142+
for k, v := range m.fields {
143+
if k == "" {
144+
return v, nil
145+
}
146+
}
147+
}
148+
140149
modified, err := transformWithMaps(onChainValue, m.onToOffChainType, m.fields, hardCode, allHooks...)
141150
if err != nil {
142151
return nil, err

pkg/codec/hard_coder_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,20 @@ func TestHardCoder(t *testing.T) {
470470
assert.Equal(t, int32(123), reflect.ValueOf(offChain).FieldByName("B").Interface())
471471
})
472472

473+
t.Run("TransformToOffChain works on primitive variables", func(t *testing.T) {
474+
hardCoder, err = codec.NewHardCoder(map[string]any{}, map[string]any{"": "test"})
475+
require.NoError(t, err)
476+
477+
var a string
478+
_, err = hardCoder.RetypeToOffChain(reflect.TypeOf(a), "")
479+
require.NoError(t, err)
480+
481+
offChain, err := hardCoder.TransformToOffChain(a, "")
482+
require.NoError(t, err)
483+
484+
assert.Equal(t, "test", offChain)
485+
})
486+
473487
t.Run("TransformToOnChain and TransformToOffChain works for itemType path", func(t *testing.T) {
474488
nestedHardCoder, err := codec.NewPathTraverseHardCoder(map[string]any{
475489
"A": "Top",

pkg/codec/modifier_base.go

+4
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ func transformWithMapsHelper[T any](
270270
}
271271

272272
tmp, err := transformWithMapsHelper(elm, toType.Elem(), fields, fn, hooks)
273+
if err != nil {
274+
return reflect.Value{}, fmt.Errorf("%w: failed to transform elem: %q of item: %q, to %q, with", err, elm.Type(), rItem.Type(), toType.Elem())
275+
}
276+
273277
result := reflect.New(toType.Elem())
274278
reflect.Indirect(result).Set(tmp)
275279

pkg/codec/property_extractor.go

+79-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package codec
22

33
import (
4+
"errors"
45
"fmt"
56
"reflect"
67
"strings"
@@ -376,7 +377,18 @@ func extractElement(src any, field string) (reflect.Value, error) {
376377
return reflect.Value{}, err
377378
}
378379

379-
if len(extractMaps) != 1 {
380+
// if extract maps is empty, check if the underlying field is an uninitialised slice, if so initialise it and return extracted elem.
381+
if len(extractMaps) == 0 {
382+
typ, err := initSliceForFieldPath(reflect.TypeOf(src), field)
383+
if errors.Is(err, &NoSliceUnderFieldPathError{}) {
384+
return reflect.Value{}, fmt.Errorf("%w: cannot find %q in type: %q for extraction", types.ErrInvalidType, field, reflect.TypeOf(src).String())
385+
} else if err != nil {
386+
return reflect.Value{}, fmt.Errorf("%w: cannot find %q in type: %q for extraction, tried to check if path leads to an uninitialised slice, but failed with %w", types.ErrInvalidType, field, reflect.TypeOf(src).String(), err)
387+
}
388+
return typ, nil
389+
}
390+
391+
if len(extractMaps) > 1 {
380392
var sliceValue reflect.Value
381393
var sliceInitialized bool
382394
for _, fields := range extractMaps {
@@ -407,7 +419,7 @@ func extractElement(src any, field string) (reflect.Value, error) {
407419

408420
item, ok := em[name]
409421
if !ok {
410-
return reflect.Value{}, fmt.Errorf("%w: cannot find %s", types.ErrInvalidType, field)
422+
return reflect.Value{}, fmt.Errorf("%w: cannot find %q", types.ErrInvalidType, field)
411423
}
412424

413425
return reflect.ValueOf(item), nil
@@ -452,3 +464,68 @@ func pathAndName(field string) ([]string, string) {
452464

453465
return path, name
454466
}
467+
468+
type NoSliceUnderFieldPathError struct {
469+
Err error
470+
}
471+
472+
func (e *NoSliceUnderFieldPathError) Error() string {
473+
return fmt.Sprintf("field path did not resolve to a slice")
474+
}
475+
476+
func initSliceForFieldPath(rootType reflect.Type, fieldPath string) (reflect.Value, error) {
477+
parts := strings.Split(fieldPath, ".")
478+
var prevIsSlice bool
479+
480+
if rootType == nil {
481+
return reflect.Value{}, fmt.Errorf("root type is nil")
482+
}
483+
484+
typ := derefTypePtr(rootType)
485+
486+
for i, p := range parts {
487+
if typ.Kind() != reflect.Struct {
488+
return reflect.Value{}, fmt.Errorf("expected a struct when processing field %q, got %s", p, typ.Kind())
489+
}
490+
491+
fieldByName, ok := typ.FieldByName(p)
492+
if !ok {
493+
return reflect.Value{}, fmt.Errorf("field %q not found in type %s", p, typ.Name())
494+
}
495+
496+
fieldType := derefTypePtr(fieldByName.Type)
497+
498+
// at end of path return a slice or a slice of slice if parent was a slice
499+
if i == len(parts)-1 {
500+
if prevIsSlice {
501+
fieldType = reflect.SliceOf(fieldType)
502+
}
503+
504+
if fieldType.Kind() != reflect.Slice {
505+
return reflect.Value{}, &NoSliceUnderFieldPathError{}
506+
}
507+
508+
return reflect.MakeSlice(fieldType, 0, 0), nil
509+
}
510+
511+
if fieldType.Kind() == reflect.Slice {
512+
if prevIsSlice {
513+
return reflect.Value{}, fmt.Errorf("multiple nested slices are not allowed: found a slice at field %q, but parent in path is already a slice", p)
514+
}
515+
prevIsSlice = true
516+
517+
newTyp := fieldType.Elem()
518+
newTyp = derefTypePtr(newTyp)
519+
520+
if newTyp.Kind() == reflect.Slice {
521+
return reflect.Value{}, fmt.Errorf("multiple nested slices are not allowed: field %q in path contains a nested slice", p)
522+
}
523+
524+
typ = newTyp
525+
} else {
526+
typ = derefTypePtr(fieldByName.Type)
527+
}
528+
}
529+
530+
return reflect.Value{}, &NoSliceUnderFieldPathError{}
531+
}

pkg/codec/property_extractor_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestPropertyExtractor(t *testing.T) {
2020

2121
type testStruct2 struct {
2222
D [16]uint8
23+
F int
2324
}
2425

2526
type testStruct3 struct {
@@ -166,6 +167,43 @@ func TestPropertyExtractor(t *testing.T) {
166167
require.Equal(t, [][16]uint8{{1}, {2}}, offChainValue)
167168
})
168169

170+
t.Run("TransformToOffChain works on filed that is an uninitialised slice", func(t *testing.T) {
171+
_, err := nestedExtractorFieldUnderSlice.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "")
172+
require.NoError(t, err)
173+
174+
onChainValue := nestedTestStruct{
175+
A: "test",
176+
B: testStruct{
177+
A: true,
178+
B: 42,
179+
},
180+
C: testStruct3{},
181+
}
182+
183+
// can't be transformed back to on-chain, its completely lossy
184+
offChainValue, err := nestedExtractorFieldUnderSlice.TransformToOffChain(onChainValue, "")
185+
require.NoError(t, err)
186+
require.Equal(t, [][16]uint8{}, offChainValue)
187+
188+
uninitialisedSliceExtractor := codec.NewPropertyExtractor("C.E")
189+
_, err = uninitialisedSliceExtractor.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "")
190+
require.NoError(t, err)
191+
192+
// can't be transformed back to on-chain, its completely lossy
193+
offChainValue, err = uninitialisedSliceExtractor.TransformToOffChain(onChainValue, "")
194+
require.NoError(t, err)
195+
require.Equal(t, []testStruct2{}, offChainValue)
196+
197+
uninitialisedSliceExtractor = codec.NewPropertyExtractor("C.E.F")
198+
_, err = uninitialisedSliceExtractor.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "")
199+
require.NoError(t, err)
200+
201+
// can't be transformed back to on-chain, its completely lossy
202+
offChainValue, err = uninitialisedSliceExtractor.TransformToOffChain(onChainValue, "")
203+
require.NoError(t, err)
204+
require.Equal(t, []int{}, offChainValue)
205+
})
206+
169207
t.Run("TransformToOnChain and TransformToOffChain works on pointers", func(t *testing.T) {
170208
_, err := nestedExtractor.RetypeToOffChain(reflect.PointerTo(onChainType), "")
171209
require.NoError(t, err)

pkg/codec/utils.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ func SetValueAtPath(vInto, vField reflect.Value, itemType string) error {
474474
}
475475

476476
func applyValue(vInto, vField reflect.Value) error {
477-
if typeWithoutPtr(vInto.Type()) != typeWithoutPtr(vField.Type()) {
477+
if derefTypePtr(vInto.Type()) != derefTypePtr(vField.Type()) {
478478
return fmt.Errorf("value type mismatch for field")
479479
}
480480

@@ -522,11 +522,9 @@ func applyValue(vInto, vField reflect.Value) error {
522522
return nil
523523
}
524524

525-
func typeWithoutPtr(val reflect.Type) reflect.Type {
526-
switch val.Kind() {
527-
case reflect.Ptr:
528-
return typeWithoutPtr(val.Elem())
529-
default:
530-
return val
525+
func derefTypePtr(typ reflect.Type) reflect.Type {
526+
for typ.Kind() == reflect.Ptr {
527+
typ = typ.Elem()
531528
}
529+
return typ
532530
}

pkg/codec/wrapper.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ func (m *wrapperModifier) TransformToOnChain(offChainValue any, itemType string)
7272
}
7373

7474
// check if the offChainValue is a wrapper around the whole value
75-
typ := reflect.TypeOf(offChainValue)
76-
if typ.Kind() == reflect.Ptr {
77-
typ = typ.Elem()
78-
}
75+
typ := derefTypePtr(reflect.TypeOf(offChainValue))
7976

8077
if typ.Kind() == reflect.Struct && typ.NumField() == 1 {
8178
if m.isWholeValueWrapper() {

0 commit comments

Comments
 (0)