Repository navigation
Expand file tree
/
Copy pathpath_bytes_test.go
More file actions
207 lines (186 loc) · 7.1 KB
/
Copy pathpath_bytes_test.go
File metadata and controls
207 lines (186 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package gnata_test
import (
"context"
"encoding/json"
"reflect"
"testing"
"github.com/recolabs/gnata"
)
const pathBytesTestData = `{
"Account": {
"Name": "Firefly",
"Order": [
{"OrderID": "order103", "Product": [
{"SKU": "0406654608", "UnitPrice": 68.45},
{"SKU": "040657863", "UnitPrice": 107.99}
]},
{"OrderID": "order104", "Product": [
{"SKU": "0406654608", "UnitPrice": 68.45},
{"SKU": "0406654603", "UnitPrice": 21.67}
]},
{"OrderID": "order105", "Product": [], "Note": null},
{"OrderID": "order106"}
]
}
}`
// evalBytesMatchesEvalCase runs one EvalBytes-vs-Eval differential check,
// shared by the pure-path and aggregate-fast test suites below (they differ
// only in which fast-path classifier they assert and the fixture used).
func evalBytesMatchesEvalCase(
t *testing.T, expr string, rawData json.RawMessage, decoded any, wantClassifier func(*gnata.Expression) bool,
) {
t.Helper()
compiled, err := gnata.Compile(expr)
if err != nil {
t.Fatalf("Compile(%q): %v", expr, err)
}
if !wantClassifier(compiled) {
t.Fatalf("Compile(%q): fast-path classification mismatch", expr)
}
wantResult, wantErr := compiled.Eval(context.Background(), decoded)
gotResult, gotErr := compiled.EvalBytes(context.Background(), rawData)
if (wantErr == nil) != (gotErr == nil) {
t.Fatalf("EvalBytes err = %v, Eval err = %v", gotErr, wantErr)
}
want := gnata.NormalizeValue(wantResult)
got := gnata.NormalizeValue(gotResult)
if !reflect.DeepEqual(got, want) {
t.Fatalf("EvalBytes(%q) = %#v, want %#v (from Eval)", expr, got, want)
}
}
func TestEvalBytes_ArrayAutoMap_MatchesEval(t *testing.T) {
testCases := []struct {
desc string
expr string
}{
{desc: "two array boundaries to scalar leaf", expr: "Account.Order.Product.SKU"},
{desc: "two array boundaries to numeric leaf", expr: "Account.Order.Product.UnitPrice"},
{desc: "single array boundary", expr: "Account.Order.OrderID"},
{desc: "field missing on some elements", expr: "Account.Order.Note"},
{desc: "pure path with no arrays", expr: "Account.Name"},
}
rawData := json.RawMessage(pathBytesTestData)
var decoded any
if err := json.Unmarshal(rawData, &decoded); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
evalBytesMatchesEvalCase(t, tC.expr, rawData, decoded, (*gnata.Expression).IsFastPath)
})
}
}
func TestEvalBytes_ArrayAutoMap_NoFullDecode(t *testing.T) {
expr, err := gnata.Compile("Account.Order.Product.SKU")
if err != nil {
t.Fatalf("Compile: %v", err)
}
rawData := json.RawMessage(pathBytesTestData)
allocs := testing.AllocsPerRun(20, func() {
if _, err := expr.EvalBytes(context.Background(), rawData); err != nil {
t.Fatal(err)
}
})
// The full-decode fallback (json.Decoder into *OrderedMap for every
// object, plus the AST walk) allocates well over 40 times per call for
// this fixture. The array-aware walker should stay far below that.
const maxAllocs = 30
if allocs > maxAllocs {
t.Fatalf("EvalBytes allocated %.1f times per call, want <= %d (full decode likely still happening)", allocs, maxAllocs)
}
}
func TestEvalBytes_AggregateFast_MatchesEval(t *testing.T) {
testCases := []struct {
desc string
expr string
}{
{desc: "sum across two array boundaries", expr: "$sum(Account.Order.Product.UnitPrice)"},
{desc: "count across two array boundaries", expr: "$count(Account.Order.Product.UnitPrice)"},
{desc: "max across two array boundaries", expr: "$max(Account.Order.Product.UnitPrice)"},
{desc: "min across two array boundaries", expr: "$min(Account.Order.Product.UnitPrice)"},
{desc: "average across two array boundaries", expr: "$average(Account.Order.Product.UnitPrice)"},
}
rawData := json.RawMessage(pathBytesTestData)
var decoded any
if err := json.Unmarshal(rawData, &decoded); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
evalBytesMatchesEvalCase(t, tC.expr, rawData, decoded, (*gnata.Expression).IsFuncFastPath)
})
}
}
// TestEvalBytes_ArrayAutoMap_LiteralKeyWithSpecialChars guards against the
// walker treating a field name as a gjson path expression instead of a
// literal key. gjson.Result.Get interprets '.', '*', '?', '#', '|', '!',
// brackets, and backslash as path syntax — a field literally named "a.b"
// sitting alongside a nested {"a":{"b":...}} would resolve to the wrong
// value (the nested one) if the walker ever called r.Get(step) with the raw
// field name instead of doing a literal-key lookup.
func TestEvalBytes_ArrayAutoMap_LiteralKeyWithSpecialChars(t *testing.T) {
rawData := json.RawMessage(`{"Items":[{"a.b": 99, "a": {"b": 1}}]}`)
var decoded any
if err := json.Unmarshal(rawData, &decoded); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
expr := `Items."a.b"`
e, err := gnata.Compile(expr)
if err != nil {
t.Fatalf("Compile(%q): %v", expr, err)
}
if !e.IsFastPath() {
t.Fatalf("Compile(%q): expected IsFastPath() == true so this exercises the walker", expr)
}
want, wantErr := e.Eval(context.Background(), decoded)
got, gotErr := e.EvalBytes(context.Background(), rawData)
if (wantErr == nil) != (gotErr == nil) {
t.Fatalf("EvalBytes err = %v, Eval err = %v", gotErr, wantErr)
}
// gnata.DeepEqual (JSONata equality), not reflect.DeepEqual: EvalBytes
// represents numbers as json.Number, Eval on already-decoded plain Go
// values sees float64 — both mean the same JSONata number.
if !gnata.DeepEqual(got, want) {
t.Fatalf("EvalBytes(%q) = %#v, want %#v (from Eval) — walker likely misread the literal key as a gjson path", expr, got, want)
}
}
func TestEvalBytes_ArrayAutoMap_FallsBackOnScalarStep(t *testing.T) {
// "Name" is a scalar, not an object or array — a further step past it
// must fall back to full evaluation and still report "undefined", not
// an incorrect fast-path value.
expr, err := gnata.Compile("Account.Name.Missing")
if err != nil {
t.Fatalf("Compile: %v", err)
}
rawData := json.RawMessage(pathBytesTestData)
got, err := expr.EvalBytes(context.Background(), rawData)
if err != nil {
t.Fatalf("EvalBytes: %v", err)
}
if got != nil {
t.Fatalf("EvalBytes(%q) = %#v, want nil (undefined)", "Account.Name.Missing", got)
}
}
func TestEvalBytes_GroupOrIndex_NotFastPath(t *testing.T) {
testCases := []struct {
desc string
expr string
}{
{desc: "group after a plain path", expr: `Account.Order{OrderID: $count(Product)}`},
{desc: "group after an index binding", expr: `Account.Order#$i{OrderID: $i}`},
{desc: "group after a bound [] step", expr: `Account.Order#$i[]{OrderID: $i}`},
{desc: "index binding on a name step", expr: `Account.Order.OrderID#$i`},
{desc: "index binding after a dropped []", expr: `Account.Order.Product.SKU#$i[]`},
}
rawData := json.RawMessage(pathBytesTestData)
var decoded any
if err := json.Unmarshal(rawData, &decoded); err != nil {
t.Fatalf("unmarshal fixture: %v", err)
}
notFastPath := func(e *gnata.Expression) bool { return !e.IsFastPath() }
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
evalBytesMatchesEvalCase(t, tC.expr, rawData, decoded, notFastPath)
})
}
}