From 0a37b91e80f06ef142c9d2a0771219459c3e9f86 Mon Sep 17 00:00:00 2001 From: Narcis Date: Fri, 13 Mar 2026 16:18:18 +0100 Subject: [PATCH 1/4] perf improvements + extra bench and tests Signed-off-by: Narcis --- dotnotation.go | 291 ++++++++++++-- dotnotation_benchmark_test.go | 264 +++++++++++- dotnotation_test.go | 734 ++++++++++++++++++++++++++-------- 3 files changed, 1074 insertions(+), 215 deletions(-) diff --git a/dotnotation.go b/dotnotation.go index 25e3553..4ae9895 100644 --- a/dotnotation.go +++ b/dotnotation.go @@ -4,6 +4,7 @@ import ( "errors" "strconv" "strings" + "sync" ) type fieldType uint8 @@ -18,7 +19,6 @@ type step struct { ft fieldType key string index int - o func(interface{}) interface{} } // Applier holds the compiled instructions to apply an operation to the compiled dotnotation path @@ -28,7 +28,7 @@ type Applier struct { // Apply applies an operation to the compiled dotnotation path of the given data func (a *Applier) Apply(v interface{}) { - a.e.Evaluate(v) + a.e.Apply(v) } // Extractor holds the compiled instructions to extract the compiled dotnotation path values @@ -38,7 +38,7 @@ type Extractor struct { // Extract extracts the values in the compiled dotnotation path of the given data func (e *Extractor) Extract(v interface{}) []interface{} { - return e.e.Evaluate(v) + return e.e.Extract(v) } // CompileExtractor compiles the dotnotation expression and returns an Extractor @@ -61,6 +61,7 @@ func compile(expr string, op func(interface{}) interface{}) (*dotNotation, error parts := strings.Split(expr, ".") steps := make([]step, 0, len(parts)) wc := 0 + for _, p := range parts { idx, err := strconv.Atoi(p) isIndex := err == nil && idx >= 0 @@ -78,88 +79,272 @@ func compile(expr string, op func(interface{}) interface{}) (*dotNotation, error } } - steps[len(steps)-1].o = op + // if there's a wildcard on last step, we remove it from counter so it goes through Linear paths anyway + // since it does not need to allocate a slice while traversing the main path + if steps[len(steps)-1].ft == wildcardType { + wc-- + } + + var applyStep step + if op != nil { + applyStep = steps[len(steps)-1] + steps = steps[:len(steps)-1] + } return &dotNotation{ - steps: steps, - wc: wc, + extractSteps: steps, + applyStep: applyStep, + op: op, + wc: wc, }, nil } type dotNotation struct { - steps []step - wc int + extractSteps []step + applyStep step + op func(interface{}) interface{} + wc int +} + +var extractPool = sync.Pool{ + New: func() any { + s := make([]interface{}, 0, 8) + return &s + }, +} + +var applyPool = sync.Pool{ + New: func() any { + p := [2][]interface{}{make([]interface{}, 0, 8), make([]interface{}, 0, 8)} + return &p + }, } -func (e *dotNotation) Evaluate(data interface{}) []interface{} { - current := []interface{}{data} - next := make([]interface{}, 0, 4*e.wc+1) +func (e *dotNotation) Extract(data interface{}) []interface{} { + if e.wc == 0 { + return e.extractLinear(data) + } + + current := make([]interface{}, 1, 4*e.wc+1) + current[0] = data - for _, step := range e.steps { - next = next[:0] + nextPtr := extractPool.Get().(*[]interface{}) + next := *nextPtr + for _, step := range e.extractSteps { switch step.ft { case stringType: next = stringTraverse(current, step, next) case numericType: next = numericTraverse(current, step, next) case wildcardType: - next = wildcardTraverse(current, step, next) + next = wildcardTraverse(current, next) } if len(next) == 0 { - return next + *nextPtr = next + extractPool.Put(nextPtr) + return next[:0:0] } - current, next = next, current + current, next = next, current[:0] } + + *nextPtr = next + extractPool.Put(nextPtr) return current } -func wildcardTraverse(current []interface{}, step step, next []interface{}) []interface{} { +func (e *dotNotation) extractLinear(data interface{}) []interface{} { + current := data + + for _, step := range e.extractSteps { + switch step.ft { + case stringType: + m, ok := current.(map[string]interface{}) + if !ok { + return nil + } + v, exists := m[step.key] + if !exists { + return nil + } + current = v + case numericType: + if m, ok := current.(map[string]interface{}); ok { + v, exists := m[step.key] + if !exists { + return nil + } + current = v + } else if arr, ok := current.([]interface{}); ok { + if step.index >= len(arr) { + return nil + } + current = arr[step.index] + } + // wildcard can only happen on last step + case wildcardType: + if m, ok := current.(map[string]interface{}); ok { + res := make([]interface{}, 0, len(m)) + for _, v := range m { + res = append(res, v) + } + return res + } else if arr, ok := current.([]interface{}); ok { + res := make([]interface{}, 0, len(arr)) + return append(res, arr...) + } + } + } + return []interface{}{current} +} + +func (e *dotNotation) Apply(data interface{}) { + if e.wc == 0 { + e.applyLinear(data) + return + } + + pair := applyPool.Get().(*[2][]interface{}) + current := append(pair[0], data) + next := pair[1] + + for _, step := range e.extractSteps { + switch step.ft { + case stringType: + next = stringTraverse(current, step, next) + case numericType: + next = numericTraverse(current, step, next) + case wildcardType: + next = wildcardTraverse(current, next) + } + + if len(next) == 0 { + pair[0] = current[:0] + pair[1] = next + applyPool.Put(pair) + return + } + + current, next = next, current[:0] + } + + switch e.applyStep.ft { + case stringType: + stringApply(current, e.applyStep, e.op) + case numericType: + numericApply(current, e.applyStep, e.op) + case wildcardType: + wildcardApply(current, e.op) + } + + pair[0] = current[:0] + pair[1] = next + applyPool.Put(pair) +} + +func (e *dotNotation) applyLinear(data interface{}) { + current := data + + for _, step := range e.extractSteps { + switch step.ft { + case stringType: + m, ok := current.(map[string]interface{}) + if !ok { + return + } + v, exists := m[step.key] + if !exists { + return + } + current = v + case numericType: + if m, ok := current.(map[string]interface{}); ok { + v, exists := m[step.key] + if !exists { + return + } + current = v + } else if arr, ok := current.([]interface{}); ok { + if step.index >= len(arr) { + return + } + current = arr[step.index] + } + } + } + + switch e.applyStep.ft { + case stringType: + if m, ok := current.(map[string]interface{}); ok { + if v, exists := m[e.applyStep.key]; exists { + m[e.applyStep.key] = e.op(v) + } + } + case numericType: + if m, ok := current.(map[string]interface{}); ok { + if v, exists := m[e.applyStep.key]; exists { + m[e.applyStep.key] = e.op(v) + } + } else if arr, ok := current.([]interface{}); ok { + if e.applyStep.index < len(arr) { + arr[e.applyStep.index] = e.op(arr[e.applyStep.index]) + } + } + case wildcardType: + switch v := current.(type) { + case []interface{}: + for i, vv := range v { + v[i] = e.op(vv) + } + case map[string]interface{}: + for k, vv := range v { + v[k] = e.op(vv) + } + } + } +} + +func wildcardTraverse(current []interface{}, next []interface{}) []interface{} { for _, n := range current { switch v := n.(type) { case []interface{}: - if step.o != nil { - for i := range v { - v[i] = step.o(v[i]) - } - continue - } next = append(next, v...) case map[string]interface{}: - if step.o != nil { - for i := range v { - v[i] = step.o(v[i]) - } - continue - } - for i := range v { - next = append(next, v[i]) + for _, vv := range v { + next = append(next, vv) } } } return next } +func wildcardApply(current []interface{}, op func(interface{}) interface{}) { + for _, n := range current { + switch v := n.(type) { + case []interface{}: + for i, vv := range v { + v[i] = op(vv) + } + case map[string]interface{}: + for i, vv := range v { + v[i] = op(vv) + } + } + } +} + func numericTraverse(current []interface{}, step step, next []interface{}) []interface{} { for _, n := range current { if m, ok := n.(map[string]interface{}); ok { if v, exists := m[step.key]; exists { - if step.o != nil { - m[step.key] = step.o(v) - continue - } next = append(next, v) } continue } if arr, ok := n.([]interface{}); ok { if step.index < len(arr) { - if step.o != nil { - arr[step.index] = step.o(arr[step.index]) - continue - } next = append(next, arr[step.index]) } } @@ -167,17 +352,37 @@ func numericTraverse(current []interface{}, step step, next []interface{}) []int return next } +func numericApply(current []interface{}, step step, op func(interface{}) interface{}) { + for _, n := range current { + if m, ok := n.(map[string]interface{}); ok { + if v, exists := m[step.key]; exists { + m[step.key] = op(v) + } + } else if arr, ok := n.([]interface{}); ok { + if step.index < len(arr) { + arr[step.index] = op(arr[step.index]) + } + } + } +} + func stringTraverse(current []interface{}, step step, next []interface{}) []interface{} { for _, n := range current { if m, ok := n.(map[string]interface{}); ok { if v, exists := m[step.key]; exists { - if step.o != nil { - m[step.key] = step.o(v) - continue - } next = append(next, v) } } } return next } + +func stringApply(current []interface{}, step step, op func(interface{}) interface{}) { + for _, n := range current { + if m, ok := n.(map[string]interface{}); ok { + if v, exists := m[step.key]; exists { + m[step.key] = op(v) + } + } + } +} diff --git a/dotnotation_benchmark_test.go b/dotnotation_benchmark_test.go index 336566e..a4bf249 100644 --- a/dotnotation_benchmark_test.go +++ b/dotnotation_benchmark_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func BenchmarkDotNotationStrategies(b *testing.B) { // skipcq: GO-R1005 +func BenchmarkDotNotationStrategiesApplier(b *testing.B) { // skipcq: GO-R1005 op := func(data interface{}) interface{} { if v, ok := data.(string); ok { return strings.ReplaceAll(v, "tupu", "supu") @@ -187,7 +187,7 @@ func BenchmarkDotNotationStrategies(b *testing.B) { // skipcq: GO-R1005 } }) b.Run("long maps+slice structure with wildcards", func(b *testing.B) { - m, err := CompileApplier("a.*.0.0.d.c.a.b.c.a.*.c", op) + m, err := CompileApplier("a.*.0.0.d.c.a.b.c.a.*.*", op) if err != nil { b.Fatal(err) } @@ -209,9 +209,27 @@ func BenchmarkDotNotationStrategies(b *testing.B) { // skipcq: GO-R1005 "a": []interface{}{ map[string]interface{}{ "c": "tupu", + "d": "tupu", + "e": "tupu", + "f": "tupu", + }, + []interface{}{ + "tupu", + "supu", + "sapu", + "tapu", }, map[string]interface{}{ "c": "tupu", + "d": "tupu", + "e": "tupu", + "f": "tupu", + }, + []interface{}{ + "tupu", + "supu", + "sapu", + "tapu", }, }, }, @@ -234,3 +252,245 @@ func BenchmarkDotNotationStrategies(b *testing.B) { // skipcq: GO-R1005 } }) } + +func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 + b.Run("simple maps extractor", func(b *testing.B) { + m, err := CompileExtractor("a.b.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("long map structure extractor", func(b *testing.B) { + m, err := CompileExtractor("a.b.c.a.b.d.a.b.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + }, + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + }, + }, + }, + }, + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("simple maps with wildcard extractor", func(b *testing.B) { + m, err := CompileExtractor("a.*.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("long map structure with wildcard extractor", func(b *testing.B) { + m, err := CompileExtractor("a.*.c.a.d.c.a.b.c.a.*.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + "d": "tupu", + "e": "tupu", + "f": "tupu", + }, + []interface{}{ + "tupu", + "supu", + "sapu", + "tapu", + }, + map[string]interface{}{ + "c": "tupu", + "d": "tupu", + "e": "tupu", + "f": "tupu", + }, + []interface{}{ + "tupu", + "supu", + "sapu", + "tapu", + }, + }, + }, + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + }, + }, + }, + }, + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("simple maps+slice extractor", func(b *testing.B) { + m, err := CompileExtractor("a.1.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("simple maps+slice with wildcard extractor", func(b *testing.B) { + m, err := CompileExtractor("a.*.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) + b.Run("long maps+slice structure with wildcards extractor", func(b *testing.B) { + m, err := CompileExtractor("a.*.0.0.d.c.a.b.c.a.*.*") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + []interface{}{ + map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + }, + }, + "d": map[string]interface{}{ + "c": "tupu", + }, + }, + }, + }, + }, + }, + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + } + m.Extract(data) + } + }) +} diff --git a/dotnotation_test.go b/dotnotation_test.go index e0c5ff9..8fd54fe 100644 --- a/dotnotation_test.go +++ b/dotnotation_test.go @@ -6,222 +6,616 @@ import ( "testing" ) -func TestWildcardStruct(t *testing.T) { - expected := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "supu", +func TestApply(t *testing.T) { + t.Run("TestWildcardStruct", func(t *testing.T) { + expected := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "supu", + }, + "d": map[string]interface{}{ + "c": "supu", + }, }, - "d": map[string]interface{}{ - "c": "supu", + } + m, err := CompileApplier("a.*.c", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m, err := CompileApplier("a.*.c", func(data interface{}) interface{} { - if v, ok := data.(string); ok { - return strings.ReplaceAll(v, "tupu", "supu") } - return data + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } }) - if err != nil { - t.Fatal(err) - } - data := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "tupu", + t.Run("TestNumericMap", func(t *testing.T) { + m, err := CompileApplier("a.1.1", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "1": "tupu", + }, + map[string]interface{}{ + "1": "tupu", + }, }, - "d": map[string]interface{}{ - "c": "tupu", + } + m.Apply(data) + expected := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "1": "tupu", + }, + map[string]interface{}{ + "1": "supu", + }, }, - }, - } - m.Apply(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) - } -} + } + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) -func TestWildcardSlice(t *testing.T) { - expected := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "supu", + t.Run("TestIndexSlice", func(t *testing.T) { + expected := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "supu", + }, }, - map[string]interface{}{ - "c": "supu", + } + m, err := CompileApplier("a.1.c", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m, err := CompileApplier("a.*.c", func(data interface{}) interface{} { - if v, ok := data.(string); ok { - return strings.ReplaceAll(v, "tupu", "supu") } - return data + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } }) - if err != nil { - t.Fatal(err) - } - data := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "tupu", + t.Run("TestIndexMap", func(t *testing.T) { + expected := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "1": map[string]interface{}{ + "c": "tupu", + }, }, - map[string]interface{}{ - "c": "tupu", + } + m, err := CompileApplier("patata", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "1": map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m.Apply(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) - } -} + } + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) -func TestWildcardSliceExtract(t *testing.T) { - expected := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "tupu1", + t.Run("TestMapStruct", func(t *testing.T) { + expected := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "supu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, }, - map[string]interface{}{ - "c": "tupu2", + } + m, err := CompileApplier("a.b.c", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m, err := CompileExtractor("a.*.c") - if err != nil { - t.Fatal(err) - } + } + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) - data := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "tupu1", + t.Run("TestNumeric", func(t *testing.T) { + expected := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + 1, + "supu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - map[string]interface{}{ - "c": "tupu2", + } + m, err := CompileApplier("a.0.1", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + 1, + "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - res := m.Extract(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) - } - if !reflect.DeepEqual(res, []interface{}{"tupu1", "tupu2"}) { - t.Errorf("%v is not %v", data, expected) - } -} + } + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) -func TestIndexSlice(t *testing.T) { - expected := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "tupu", + t.Run("TestWildcardNumeric", func(t *testing.T) { + expected := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + 1, + "supu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - map[string]interface{}{ - "c": "supu", + } + m, err := CompileApplier("a.*.1", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + 1, + "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m, err := CompileApplier("a.1.c", func(data interface{}) interface{} { - if v, ok := data.(string); ok { - return strings.ReplaceAll(v, "tupu", "supu") } - return data + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } }) - if err != nil { - t.Fatal(err) - } - data := map[string]interface{}{ - "a": []interface{}{ - map[string]interface{}{ - "c": "tupu", + t.Run("TestWildcardSlice", func(t *testing.T) { + expected := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "supu", + }, + map[string]interface{}{ + "c": "supu", + }, }, - map[string]interface{}{ - "c": "tupu", + } + m, err := CompileApplier("a.*.c", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, }, - }, - } - m.Apply(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) - } -} + } + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) -func TestIndexMap(t *testing.T) { - expected := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "tupu", + t.Run("TestMapWildcard", func(t *testing.T) { + expected := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + "d": "tupu", + }, + "d": map[string]interface{}{ + "c": "supu", + "d": "supu", + }, }, - "1": map[string]interface{}{ - "c": "tupu", + } + m, err := CompileApplier("a.d.*", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + "d": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + "d": "tupu", + }, }, - }, - } - m, err := CompileApplier("patata", func(data interface{}) interface{} { - if v, ok := data.(string); ok { - return strings.ReplaceAll(v, "tupu", "supu") } - return data + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } }) - if err != nil { - t.Fatal(err) - } - data := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "tupu", + t.Run("TestMapWildcardWildcard", func(t *testing.T) { + expected := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "supu", + "d": "supu", + }, + "d": map[string]interface{}{ + "c": "supu", + "d": "supu", + }, }, - "1": map[string]interface{}{ - "c": "tupu", + } + m, err := CompileApplier("a.*.*", func(data interface{}) interface{} { + if v, ok := data.(string); ok { + return strings.ReplaceAll(v, "tupu", "supu") + } + return data + }) + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + "d": "tupu", + }, + "d": map[string]interface{}{ + "c": "tupu", + "d": "tupu", + }, }, - }, - } - m.Apply(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) - } + } + m.Apply(data) + if !reflect.DeepEqual(data, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) } -func TestMapStruct(t *testing.T) { - expected := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "supu", +func TestExtract(t *testing.T) { + t.Run("TestWildcardStruct", func(t *testing.T) { + m, err := CompileExtractor("a.*.c") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "supu", + }, }, - "d": map[string]interface{}{ - "c": "tupu", + } + res := m.Extract(data) + expected := []interface{}{"tupu", "supu"} + if !equalNoOrder(res, expected) { + t.Errorf("%v is not %v", res, expected) + } + }) + + t.Run("TestNumericMap", func(t *testing.T) { + m, err := CompileExtractor("a.1.1") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "1": "tupu", + }, + map[string]interface{}{ + "1": "supu", + }, }, - }, - } - m, err := CompileApplier("a.b.c", func(data interface{}) interface{} { - if v, ok := data.(string); ok { - return strings.ReplaceAll(v, "tupu", "supu") } - return data + res := m.Extract(data) + expected := []interface{}{"supu"} + if !reflect.DeepEqual(res, expected) { + t.Errorf("%v is not %v", data, expected) + } }) - if err != nil { - t.Fatal(err) - } - data := map[string]interface{}{ - "a": map[string]interface{}{ - "b": map[string]interface{}{ - "c": "tupu", + t.Run("TestWildcardNumericMap", func(t *testing.T) { + m, err := CompileExtractor("a.*.1") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "0": "tupu", + }, + map[string]interface{}{ + "1": "supu", + }, }, - "d": map[string]interface{}{ - "c": "tupu", + } + res := m.Extract(data) + expected := []interface{}{"supu"} + if !reflect.DeepEqual(res, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) + + t.Run("TestNoMatch", func(t *testing.T) { + m, err := CompileExtractor("patata") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "1": map[string]interface{}{ + "c": "tupu", + }, }, - }, + } + res := m.Extract(data) + expected := []interface{}{} + if len(res) != 0 { + t.Errorf("%v is not %v", res, expected) + } + }) + + t.Run("TestMapStruct", func(t *testing.T) { + m, err := CompileExtractor("a.b.c") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "tupu", + }, + "d": map[string]interface{}{ + "c": "supu", + }, + }, + } + res := m.Extract(data) + expected := []interface{}{"tupu"} + if !reflect.DeepEqual(res, expected) { + t.Errorf("%v is not %v", res, expected) + } + }) + + t.Run("TestNumeric", func(t *testing.T) { + m, err := CompileExtractor("a.0.1") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + []interface{}{ + 1, + "tupu", + }, + map[string]interface{}{ + "c": "supu", + }, + }, + } + v := m.Extract(data) + expected := []interface{}{"tupu"} + if !reflect.DeepEqual(v, []interface{}{"tupu"}) { + t.Errorf("%v is not %v", v, expected) + } + }) + + t.Run("TestWildcardSlice", func(t *testing.T) { + m, err := CompileExtractor("a.*.c") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu1", + }, + map[string]interface{}{ + "c": "tupu2", + }, + }, + } + res := m.Extract(data) + expected := []interface{}{"tupu1", "tupu2"} + if !reflect.DeepEqual(res, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) + + t.Run("TestWildcardLastStepMap", func(t *testing.T) { + m, err := CompileExtractor("a.1.*") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu1", + }, + map[string]interface{}{ + "c": "tupu", + "b": "supu", + }, + }, + } + res := m.Extract(data) + expected := []interface{}{"tupu", "supu"} + if !equalNoOrder(res, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) + + t.Run("TestWildcardLastStepSlice", func(t *testing.T) { + m, err := CompileExtractor("a.1.*") + if err != nil { + t.Fatal(err) + } + + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu1", + }, + []interface{}{ + "tupu", + "supu", + 2, + }, + }, + } + res := m.Extract(data) + expected := []interface{}{"tupu", "supu", 2} + if !reflect.DeepEqual(res, expected) { + t.Errorf("%v is not %v", data, expected) + } + }) +} + +func equalNoOrder(a, b []interface{}) bool { + if len(a) != len(b) { + return false } - m.Apply(data) - if !reflect.DeepEqual(data, expected) { - t.Errorf("%v is not %v", data, expected) + counts := make(map[interface{}]int, len(a)) + for _, v := range a { + counts[v]++ + } + for _, v := range b { + counts[v]-- + if counts[v] < 0 { + return false + } } + return true } From 2a0019d0a753388e73a8363b04262d99ba3e540a Mon Sep 17 00:00:00 2001 From: Narcis Date: Fri, 13 Mar 2026 17:26:13 +0100 Subject: [PATCH 2/4] split applier and extractor into their own files Signed-off-by: Narcis --- applier.go | 169 ++++++++++++++++++++++++++++++ dotnotation.go | 272 ------------------------------------------------- extractor.go | 104 +++++++++++++++++++ 3 files changed, 273 insertions(+), 272 deletions(-) create mode 100644 applier.go create mode 100644 extractor.go diff --git a/applier.go b/applier.go new file mode 100644 index 0000000..b00f0b8 --- /dev/null +++ b/applier.go @@ -0,0 +1,169 @@ +package dotnotation + +import ( + "sync" +) + +// CompileApplier compiles the dotnotation expression and returns an Applier +func CompileApplier(expr string, op func(interface{}) interface{}) (*Applier, error) { + e, err := compile(expr, op) + return &Applier{e: e}, err +} + +var applyPool = sync.Pool{ + New: func() any { + p := [2][]interface{}{make([]interface{}, 0, 8), make([]interface{}, 0, 8)} + return &p + }, +} + +// Applier holds the compiled instructions to apply an operation to the compiled dotnotation path +type Applier struct { + e *dotNotation +} + +// Apply applies an operation to the compiled dotnotation path of the given data +func (e *Applier) Apply(data interface{}) { + if e.e.wc == 0 { + e.applyLinear(data) + return + } + + pair := applyPool.Get().(*[2][]interface{}) + current := append(pair[0], data) + next := pair[1] + + for _, step := range e.e.extractSteps { + switch step.ft { + case stringType: + next = stringTraverse(current, step, next) + case numericType: + next = numericTraverse(current, step, next) + case wildcardType: + next = wildcardTraverse(current, next) + } + + if len(next) == 0 { + pair[0] = current[:0] + pair[1] = next + applyPool.Put(pair) + return + } + + current, next = next, current[:0] + } + + switch e.e.applyStep.ft { + case stringType: + stringApply(current, e.e.applyStep, e.e.op) + case numericType: + numericApply(current, e.e.applyStep, e.e.op) + case wildcardType: + wildcardApply(current, e.e.op) + } + + pair[0] = current[:0] + pair[1] = next + applyPool.Put(pair) +} + +func (e *Applier) applyLinear(data interface{}) { + current := data + + for _, step := range e.e.extractSteps { + switch step.ft { + case stringType: + m, ok := current.(map[string]interface{}) + if !ok { + return + } + v, exists := m[step.key] + if !exists { + return + } + current = v + case numericType: + if m, ok := current.(map[string]interface{}); ok { + v, exists := m[step.key] + if !exists { + return + } + current = v + } else if arr, ok := current.([]interface{}); ok { + if step.index >= len(arr) { + return + } + current = arr[step.index] + } + } + } + + switch e.e.applyStep.ft { + case stringType: + if m, ok := current.(map[string]interface{}); ok { + if v, exists := m[e.e.applyStep.key]; exists { + m[e.e.applyStep.key] = e.e.op(v) + } + } + case numericType: + if m, ok := current.(map[string]interface{}); ok { + if v, exists := m[e.e.applyStep.key]; exists { + m[e.e.applyStep.key] = e.e.op(v) + } + } else if arr, ok := current.([]interface{}); ok { + if e.e.applyStep.index < len(arr) { + arr[e.e.applyStep.index] = e.e.op(arr[e.e.applyStep.index]) + } + } + case wildcardType: + switch v := current.(type) { + case []interface{}: + for i, vv := range v { + v[i] = e.e.op(vv) + } + case map[string]interface{}: + for k, vv := range v { + v[k] = e.e.op(vv) + } + } + } +} + +func wildcardApply(current []interface{}, op func(interface{}) interface{}) { + for _, n := range current { + switch v := n.(type) { + case []interface{}: + for i, vv := range v { + v[i] = op(vv) + } + case map[string]interface{}: + for i, vv := range v { + v[i] = op(vv) + } + } + } +} + +func numericApply(current []interface{}, step step, op func(interface{}) interface{}) { + for _, n := range current { + if m, ok := n.(map[string]interface{}); ok { + if v, exists := m[step.key]; exists { + m[step.key] = op(v) + } + } else if arr, ok := n.([]interface{}); ok { + if step.index < len(arr) { + arr[step.index] = op(arr[step.index]) + } + } + } +} + +func stringApply(current []interface{}, step step, op func(interface{}) interface{}) { + for _, n := range current { + if m, ok := n.(map[string]interface{}); ok { + if v, exists := m[step.key]; exists { + m[step.key] = op(v) + } + } + } +} diff --git a/dotnotation.go b/dotnotation.go index 4ae9895..25ece48 100644 --- a/dotnotation.go +++ b/dotnotation.go @@ -4,7 +4,6 @@ import ( "errors" "strconv" "strings" - "sync" ) type fieldType uint8 @@ -21,38 +20,6 @@ type step struct { index int } -// Applier holds the compiled instructions to apply an operation to the compiled dotnotation path -type Applier struct { - e *dotNotation -} - -// Apply applies an operation to the compiled dotnotation path of the given data -func (a *Applier) Apply(v interface{}) { - a.e.Apply(v) -} - -// Extractor holds the compiled instructions to extract the compiled dotnotation path values -type Extractor struct { - e *dotNotation -} - -// Extract extracts the values in the compiled dotnotation path of the given data -func (e *Extractor) Extract(v interface{}) []interface{} { - return e.e.Extract(v) -} - -// CompileExtractor compiles the dotnotation expression and returns an Extractor -func CompileExtractor(expr string) (*Extractor, error) { - e, err := compile(expr, nil) - return &Extractor{e: e}, err -} - -// CompileApplier compiles the dotnotation expression and returns an Applier -func CompileApplier(expr string, op func(interface{}) interface{}) (*Applier, error) { - e, err := compile(expr, op) - return &Applier{e: e}, err -} - func compile(expr string, op func(interface{}) interface{}) (*dotNotation, error) { if expr == "" { return nil, errors.New("cannot compile empty expr") @@ -106,206 +73,6 @@ type dotNotation struct { wc int } -var extractPool = sync.Pool{ - New: func() any { - s := make([]interface{}, 0, 8) - return &s - }, -} - -var applyPool = sync.Pool{ - New: func() any { - p := [2][]interface{}{make([]interface{}, 0, 8), make([]interface{}, 0, 8)} - return &p - }, -} - -func (e *dotNotation) Extract(data interface{}) []interface{} { - if e.wc == 0 { - return e.extractLinear(data) - } - - current := make([]interface{}, 1, 4*e.wc+1) - current[0] = data - - nextPtr := extractPool.Get().(*[]interface{}) - next := *nextPtr - - for _, step := range e.extractSteps { - switch step.ft { - case stringType: - next = stringTraverse(current, step, next) - case numericType: - next = numericTraverse(current, step, next) - case wildcardType: - next = wildcardTraverse(current, next) - } - - if len(next) == 0 { - *nextPtr = next - extractPool.Put(nextPtr) - return next[:0:0] - } - - current, next = next, current[:0] - } - - *nextPtr = next - extractPool.Put(nextPtr) - return current -} - -func (e *dotNotation) extractLinear(data interface{}) []interface{} { - current := data - - for _, step := range e.extractSteps { - switch step.ft { - case stringType: - m, ok := current.(map[string]interface{}) - if !ok { - return nil - } - v, exists := m[step.key] - if !exists { - return nil - } - current = v - case numericType: - if m, ok := current.(map[string]interface{}); ok { - v, exists := m[step.key] - if !exists { - return nil - } - current = v - } else if arr, ok := current.([]interface{}); ok { - if step.index >= len(arr) { - return nil - } - current = arr[step.index] - } - // wildcard can only happen on last step - case wildcardType: - if m, ok := current.(map[string]interface{}); ok { - res := make([]interface{}, 0, len(m)) - for _, v := range m { - res = append(res, v) - } - return res - } else if arr, ok := current.([]interface{}); ok { - res := make([]interface{}, 0, len(arr)) - return append(res, arr...) - } - } - } - return []interface{}{current} -} - -func (e *dotNotation) Apply(data interface{}) { - if e.wc == 0 { - e.applyLinear(data) - return - } - - pair := applyPool.Get().(*[2][]interface{}) - current := append(pair[0], data) - next := pair[1] - - for _, step := range e.extractSteps { - switch step.ft { - case stringType: - next = stringTraverse(current, step, next) - case numericType: - next = numericTraverse(current, step, next) - case wildcardType: - next = wildcardTraverse(current, next) - } - - if len(next) == 0 { - pair[0] = current[:0] - pair[1] = next - applyPool.Put(pair) - return - } - - current, next = next, current[:0] - } - - switch e.applyStep.ft { - case stringType: - stringApply(current, e.applyStep, e.op) - case numericType: - numericApply(current, e.applyStep, e.op) - case wildcardType: - wildcardApply(current, e.op) - } - - pair[0] = current[:0] - pair[1] = next - applyPool.Put(pair) -} - -func (e *dotNotation) applyLinear(data interface{}) { - current := data - - for _, step := range e.extractSteps { - switch step.ft { - case stringType: - m, ok := current.(map[string]interface{}) - if !ok { - return - } - v, exists := m[step.key] - if !exists { - return - } - current = v - case numericType: - if m, ok := current.(map[string]interface{}); ok { - v, exists := m[step.key] - if !exists { - return - } - current = v - } else if arr, ok := current.([]interface{}); ok { - if step.index >= len(arr) { - return - } - current = arr[step.index] - } - } - } - - switch e.applyStep.ft { - case stringType: - if m, ok := current.(map[string]interface{}); ok { - if v, exists := m[e.applyStep.key]; exists { - m[e.applyStep.key] = e.op(v) - } - } - case numericType: - if m, ok := current.(map[string]interface{}); ok { - if v, exists := m[e.applyStep.key]; exists { - m[e.applyStep.key] = e.op(v) - } - } else if arr, ok := current.([]interface{}); ok { - if e.applyStep.index < len(arr) { - arr[e.applyStep.index] = e.op(arr[e.applyStep.index]) - } - } - case wildcardType: - switch v := current.(type) { - case []interface{}: - for i, vv := range v { - v[i] = e.op(vv) - } - case map[string]interface{}: - for k, vv := range v { - v[k] = e.op(vv) - } - } - } -} - func wildcardTraverse(current []interface{}, next []interface{}) []interface{} { for _, n := range current { switch v := n.(type) { @@ -320,21 +87,6 @@ func wildcardTraverse(current []interface{}, next []interface{}) []interface{} { return next } -func wildcardApply(current []interface{}, op func(interface{}) interface{}) { - for _, n := range current { - switch v := n.(type) { - case []interface{}: - for i, vv := range v { - v[i] = op(vv) - } - case map[string]interface{}: - for i, vv := range v { - v[i] = op(vv) - } - } - } -} - func numericTraverse(current []interface{}, step step, next []interface{}) []interface{} { for _, n := range current { if m, ok := n.(map[string]interface{}); ok { @@ -352,20 +104,6 @@ func numericTraverse(current []interface{}, step step, next []interface{}) []int return next } -func numericApply(current []interface{}, step step, op func(interface{}) interface{}) { - for _, n := range current { - if m, ok := n.(map[string]interface{}); ok { - if v, exists := m[step.key]; exists { - m[step.key] = op(v) - } - } else if arr, ok := n.([]interface{}); ok { - if step.index < len(arr) { - arr[step.index] = op(arr[step.index]) - } - } - } -} - func stringTraverse(current []interface{}, step step, next []interface{}) []interface{} { for _, n := range current { if m, ok := n.(map[string]interface{}); ok { @@ -376,13 +114,3 @@ func stringTraverse(current []interface{}, step step, next []interface{}) []inte } return next } - -func stringApply(current []interface{}, step step, op func(interface{}) interface{}) { - for _, n := range current { - if m, ok := n.(map[string]interface{}); ok { - if v, exists := m[step.key]; exists { - m[step.key] = op(v) - } - } - } -} diff --git a/extractor.go b/extractor.go new file mode 100644 index 0000000..3f80e54 --- /dev/null +++ b/extractor.go @@ -0,0 +1,104 @@ +package dotnotation + +import ( + "sync" +) + +// CompileExtractor compiles the dotnotation expression and returns an Extractor +func CompileExtractor(expr string) (*Extractor, error) { + e, err := compile(expr, nil) + return &Extractor{e: e}, err +} + +// Extractor holds the compiled instructions to extract the compiled dotnotation path values +type Extractor struct { + e *dotNotation +} + +var extractPool = sync.Pool{ + New: func() any { + s := make([]interface{}, 0, 8) + return &s + }, +} + +// Extract extracts the values in the compiled dotnotation path of the given data +func (e *Extractor) Extract(data interface{}) []interface{} { + if e.e.wc == 0 { + return e.extractLinear(data) + } + + current := make([]interface{}, 1, 4*e.e.wc+1) + current[0] = data + + nextPtr := extractPool.Get().(*[]interface{}) + next := *nextPtr + + for _, step := range e.e.extractSteps { + switch step.ft { + case stringType: + next = stringTraverse(current, step, next) + case numericType: + next = numericTraverse(current, step, next) + case wildcardType: + next = wildcardTraverse(current, next) + } + + if len(next) == 0 { + *nextPtr = next + extractPool.Put(nextPtr) + return next[:0:0] + } + + current, next = next, current[:0] + } + + *nextPtr = next + extractPool.Put(nextPtr) + return current +} + +func (e *Extractor) extractLinear(data interface{}) []interface{} { + current := data + + for _, step := range e.e.extractSteps { + switch step.ft { + case stringType: + m, ok := current.(map[string]interface{}) + if !ok { + return nil + } + v, exists := m[step.key] + if !exists { + return nil + } + current = v + case numericType: + if m, ok := current.(map[string]interface{}); ok { + v, exists := m[step.key] + if !exists { + return nil + } + current = v + } else if arr, ok := current.([]interface{}); ok { + if step.index >= len(arr) { + return nil + } + current = arr[step.index] + } + // wildcard can only happen on last step + case wildcardType: + if m, ok := current.(map[string]interface{}); ok { + res := make([]interface{}, 0, len(m)) + for _, v := range m { + res = append(res, v) + } + return res + } else if arr, ok := current.([]interface{}); ok { + res := make([]interface{}, 0, len(arr)) + return append(res, arr...) + } + } + } + return []interface{}{current} +} From 981afd438a1eb7c15b4eb804116db7ad0f4a99f5 Mon Sep 17 00:00:00 2001 From: Narcis Date: Mon, 16 Mar 2026 14:53:01 +0100 Subject: [PATCH 3/4] fix/skip deepsource issues Signed-off-by: Narcis --- applier.go | 4 ++-- dotnotation_test.go | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/applier.go b/applier.go index b00f0b8..c6b0b3b 100644 --- a/applier.go +++ b/applier.go @@ -30,7 +30,7 @@ func (e *Applier) Apply(data interface{}) { } pair := applyPool.Get().(*[2][]interface{}) - current := append(pair[0], data) + current := append(pair[0], data) // skipcq: CRT-D0001 next := pair[1] for _, step := range e.e.extractSteps { @@ -67,7 +67,7 @@ func (e *Applier) Apply(data interface{}) { applyPool.Put(pair) } -func (e *Applier) applyLinear(data interface{}) { +func (e *Applier) applyLinear(data interface{}) { // skipcq: GO-R1005 current := data for _, step := range e.e.extractSteps { diff --git a/dotnotation_test.go b/dotnotation_test.go index 8fd54fe..e33fb32 100644 --- a/dotnotation_test.go +++ b/dotnotation_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestApply(t *testing.T) { +func TestApply(t *testing.T) { // skipcq: GO-R1005 t.Run("TestWildcardStruct", func(t *testing.T) { expected := map[string]interface{}{ "a": map[string]interface{}{ @@ -390,7 +390,7 @@ func TestApply(t *testing.T) { }) } -func TestExtract(t *testing.T) { +func TestExtract(t *testing.T) { // skipcq: GO-R1005 t.Run("TestWildcardStruct", func(t *testing.T) { m, err := CompileExtractor("a.*.c") if err != nil { @@ -477,9 +477,8 @@ func TestExtract(t *testing.T) { }, } res := m.Extract(data) - expected := []interface{}{} if len(res) != 0 { - t.Errorf("%v is not %v", res, expected) + t.Errorf("%v is not []", res) } }) From 32768dcdafa047807763fbd9c3bbbc0e0d90e2bb Mon Sep 17 00:00:00 2001 From: Narcis Date: Thu, 19 Mar 2026 17:44:16 +0100 Subject: [PATCH 4/4] improvements after review Signed-off-by: Narcis --- applier.go | 12 +++++++++--- dotnotation_benchmark_test.go | 30 +++++++++++++++++++++++------- extractor.go | 7 +++++-- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/applier.go b/applier.go index c6b0b3b..93b0a5a 100644 --- a/applier.go +++ b/applier.go @@ -89,7 +89,9 @@ func (e *Applier) applyLinear(data interface{}) { // skipcq: GO-R1005 return } current = v - } else if arr, ok := current.([]interface{}); ok { + continue + } + if arr, ok := current.([]interface{}); ok { if step.index >= len(arr) { return } @@ -110,7 +112,9 @@ func (e *Applier) applyLinear(data interface{}) { // skipcq: GO-R1005 if v, exists := m[e.e.applyStep.key]; exists { m[e.e.applyStep.key] = e.e.op(v) } - } else if arr, ok := current.([]interface{}); ok { + return + } + if arr, ok := current.([]interface{}); ok { if e.e.applyStep.index < len(arr) { arr[e.e.applyStep.index] = e.e.op(arr[e.e.applyStep.index]) } @@ -150,7 +154,9 @@ func numericApply(current []interface{}, step step, op func(interface{}) interfa if v, exists := m[step.key]; exists { m[step.key] = op(v) } - } else if arr, ok := n.([]interface{}); ok { + continue + } + if arr, ok := n.([]interface{}); ok { if step.index < len(arr) { arr[step.index] = op(arr[step.index]) } diff --git a/dotnotation_benchmark_test.go b/dotnotation_benchmark_test.go index a4bf249..d1f4a73 100644 --- a/dotnotation_benchmark_test.go +++ b/dotnotation_benchmark_test.go @@ -253,6 +253,8 @@ func BenchmarkDotNotationStrategiesApplier(b *testing.B) { // skipcq: GO-R1005 }) } +var result []interface{} + func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Run("simple maps extractor", func(b *testing.B) { m, err := CompileExtractor("a.b.c") @@ -260,6 +262,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": map[string]interface{}{ @@ -271,8 +274,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("long map structure extractor", func(b *testing.B) { m, err := CompileExtractor("a.b.c.a.b.d.a.b.c") @@ -280,6 +284,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": map[string]interface{}{ @@ -318,8 +323,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("simple maps with wildcard extractor", func(b *testing.B) { m, err := CompileExtractor("a.*.c") @@ -327,6 +333,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": map[string]interface{}{ @@ -338,8 +345,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("long map structure with wildcard extractor", func(b *testing.B) { m, err := CompileExtractor("a.*.c.a.d.c.a.b.c.a.*.c") @@ -347,6 +355,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": map[string]interface{}{ @@ -403,8 +412,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("simple maps+slice extractor", func(b *testing.B) { m, err := CompileExtractor("a.1.c") @@ -412,6 +422,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": []interface{}{ @@ -423,8 +434,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("simple maps+slice with wildcard extractor", func(b *testing.B) { m, err := CompileExtractor("a.*.c") @@ -432,6 +444,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": []interface{}{ @@ -443,8 +456,9 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) b.Run("long maps+slice structure with wildcards extractor", func(b *testing.B) { m, err := CompileExtractor("a.*.0.0.d.c.a.b.c.a.*.*") @@ -452,6 +466,7 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 b.Fatal(err) } b.ResetTimer() + var tmp []interface{} for i := 0; i < b.N; i++ { data := map[string]interface{}{ "a": []interface{}{ @@ -490,7 +505,8 @@ func BenchmarkDotNotationStrategiesExtractor(b *testing.B) { // skipcq: GO-R1005 }, }, } - m.Extract(data) + tmp = m.Extract(data) } + result = tmp }) } diff --git a/extractor.go b/extractor.go index 3f80e54..62c2abd 100644 --- a/extractor.go +++ b/extractor.go @@ -80,7 +80,9 @@ func (e *Extractor) extractLinear(data interface{}) []interface{} { return nil } current = v - } else if arr, ok := current.([]interface{}); ok { + continue + } + if arr, ok := current.([]interface{}); ok { if step.index >= len(arr) { return nil } @@ -94,7 +96,8 @@ func (e *Extractor) extractLinear(data interface{}) []interface{} { res = append(res, v) } return res - } else if arr, ok := current.([]interface{}); ok { + } + if arr, ok := current.([]interface{}); ok { res := make([]interface{}, 0, len(arr)) return append(res, arr...) }