diff --git a/applier.go b/applier.go new file mode 100644 index 0000000..93b0a5a --- /dev/null +++ b/applier.go @@ -0,0 +1,175 @@ +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) // skipcq: CRT-D0001 + 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{}) { // skipcq: GO-R1005 + 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 + continue + } + 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) + } + 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]) + } + } + 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) + } + continue + } + 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 25e3553..25ece48 100644 --- a/dotnotation.go +++ b/dotnotation.go @@ -18,39 +18,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 -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.Evaluate(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.Evaluate(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) { @@ -61,6 +28,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,64 +46,41 @@ 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 } -func (e *dotNotation) Evaluate(data interface{}) []interface{} { - current := []interface{}{data} - next := make([]interface{}, 0, 4*e.wc+1) - - for _, step := range e.steps { - next = next[:0] - - switch step.ft { - case stringType: - next = stringTraverse(current, step, next) - case numericType: - next = numericTraverse(current, step, next) - case wildcardType: - next = wildcardTraverse(current, step, next) - } - - if len(next) == 0 { - return next - } - - current, next = next, current - } - return current -} - -func wildcardTraverse(current []interface{}, step step, next []interface{}) []interface{} { +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) } } } @@ -146,20 +91,12 @@ func numericTraverse(current []interface{}, step step, next []interface{}) []int 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]) } } @@ -171,10 +108,6 @@ func stringTraverse(current []interface{}, step step, next []interface{}) []inte 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) } } diff --git a/dotnotation_benchmark_test.go b/dotnotation_benchmark_test.go index 336566e..d1f4a73 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,261 @@ func BenchmarkDotNotationStrategies(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") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + 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", + }, + }, + } + 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") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + 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", + }, + }, + } + tmp = m.Extract(data) + } + result = tmp + }) + b.Run("simple maps with wildcard extractor", func(b *testing.B) { + m, err := CompileExtractor("a.*.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + 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", + }, + }, + } + 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") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + 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", + }, + }, + } + tmp = m.Extract(data) + } + result = tmp + }) + b.Run("simple maps+slice extractor", func(b *testing.B) { + m, err := CompileExtractor("a.1.c") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + } + tmp = m.Extract(data) + } + result = tmp + }) + 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() + var tmp []interface{} + for i := 0; i < b.N; i++ { + data := map[string]interface{}{ + "a": []interface{}{ + map[string]interface{}{ + "c": "tupu", + }, + map[string]interface{}{ + "c": "tupu", + }, + }, + } + 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.*.*") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + var tmp []interface{} + 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", + }, + }, + } + tmp = m.Extract(data) + } + result = tmp + }) +} diff --git a/dotnotation_test.go b/dotnotation_test.go index e0c5ff9..e33fb32 100644 --- a/dotnotation_test.go +++ b/dotnotation_test.go @@ -6,222 +6,615 @@ 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) { // skipcq: GO-R1005 + 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) { // skipcq: GO-R1005 + 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) + if len(res) != 0 { + t.Errorf("%v is not []", res) + } + }) + + 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 } diff --git a/extractor.go b/extractor.go new file mode 100644 index 0000000..62c2abd --- /dev/null +++ b/extractor.go @@ -0,0 +1,107 @@ +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 + continue + } + 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 + } + if arr, ok := current.([]interface{}); ok { + res := make([]interface{}, 0, len(arr)) + return append(res, arr...) + } + } + } + return []interface{}{current} +}