Skip to content

Commit 9a2ac7e

Browse files
committed
Adds a new Query() method that works like Path() but uses
ohler/ojg to evaluate the jsonPath expression.
1 parent 07a4dbe commit 9a2ac7e

File tree

16 files changed

+378
-106
lines changed

16 files changed

+378
-106
lines changed

array.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ func (a *Array) Path(path string) *Value {
124124
return jsonPath(opChain, a.value, path)
125125
}
126126

127+
// Query is similar to Value.Query.
128+
func (a *Array) Query(path string) *Value {
129+
opChain := a.chain.enter("Query(%q)", path)
130+
defer opChain.leave()
131+
132+
return jsonPathOjg(opChain, a.value, path)
133+
}
134+
127135
// Schema is similar to Value.Schema.
128136
func (a *Array) Schema(schema interface{}) *Array {
129137
opChain := a.chain.enter("Schema()")

array_test.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func TestArray_FailedChain(t *testing.T) {
1212
value.chain.assert(t, failure)
1313

1414
value.Path("$").chain.assert(t, failure)
15+
value.Query("$").chain.assert(t, failure)
1516
value.Schema("")
1617
value.Alias("foo")
1718

@@ -239,28 +240,41 @@ func TestArray_Alias(t *testing.T) {
239240
assert.Equal(t, []string{"foo", "Filter()"}, childValue.chain.context.AliasedPath)
240241
}
241242

243+
var jsonPathCases = []struct {
244+
name string
245+
value []interface{}
246+
}{
247+
{
248+
name: "empty",
249+
value: []interface{}{},
250+
},
251+
{
252+
name: "not empty",
253+
value: []interface{}{"foo", 123.0},
254+
},
255+
}
256+
242257
func TestArray_Path(t *testing.T) {
243-
cases := []struct {
244-
name string
245-
value []interface{}
246-
}{
247-
{
248-
name: "empty",
249-
value: []interface{}{},
250-
},
251-
{
252-
name: "not empty",
253-
value: []interface{}{"foo", 123.0},
254-
},
258+
for _, tc := range jsonPathCases {
259+
t.Run(tc.name, func(t *testing.T) {
260+
reporter := newMockReporter(t)
261+
262+
value := NewArray(reporter, tc.value)
263+
264+
assert.Equal(t, tc.value, value.Path("$").Raw())
265+
value.chain.assert(t, success)
266+
})
255267
}
268+
}
256269

257-
for _, tc := range cases {
270+
func TestArray_Query(t *testing.T) {
271+
for _, tc := range jsonPathCases {
258272
t.Run(tc.name, func(t *testing.T) {
259273
reporter := newMockReporter(t)
260274

261275
value := NewArray(reporter, tc.value)
262276

263-
assert.Equal(t, tc.value, value.Path("$").Raw())
277+
assert.Equal(t, tc.value, value.Query("$").Raw())
264278
value.chain.assert(t, success)
265279
})
266280
}

boolean.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ func (b *Boolean) Path(path string) *Value {
9494
return jsonPath(opChain, b.value, path)
9595
}
9696

97+
// Query is similar to Value.Query
98+
func (b *Boolean) Query(path string) *Value {
99+
opChain := b.chain.enter("Query(%q)", path)
100+
defer opChain.leave()
101+
102+
return jsonPathOjg(opChain, b.value, path)
103+
}
104+
97105
// Schema is similar to Value.Schema.
98106
func (b *Boolean) Schema(schema interface{}) *Boolean {
99107
opChain := b.chain.enter("Schema()")

boolean_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestBoolean_FailedChain(t *testing.T) {
1414
value.chain.assert(t, failure)
1515

1616
value.Path("$").chain.assert(t, failure)
17+
value.Query("$").chain.assert(t, failure)
1718
value.Schema("")
1819
value.Alias("foo")
1920

@@ -133,6 +134,15 @@ func TestBoolean_Path(t *testing.T) {
133134
value.chain.assert(t, success)
134135
}
135136

137+
func TestBoolean_Query(t *testing.T) {
138+
reporter := newMockReporter(t)
139+
140+
value := NewBoolean(reporter, true)
141+
142+
assert.Equal(t, true, value.Query("$").Raw())
143+
value.chain.assert(t, success)
144+
}
145+
136146
func TestBoolean_Schema(t *testing.T) {
137147
reporter := newMockReporter(t)
138148

chain.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
// to current assertion starting from chain root
1717
//
1818
// - AssertionHandler: provides methods to handle successful and failed assertions;
19-
// may be defined by user, but usually we just use DefaulAssertionHandler
19+
// may be defined by user, but usually we just use DefaultAssertionHandler
2020
//
2121
// - AssertionSeverity: severity to be used for failures (fatal or non-fatal)
2222
//

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/gavv/httpexpect/v2
22

3-
go 1.19
3+
go 1.21
44

55
require (
66
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2
@@ -30,6 +30,7 @@ require (
3030
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
3131
github.com/klauspost/compress v1.15.0 // indirect
3232
github.com/mattn/go-colorable v0.1.13 // indirect
33+
github.com/ohler55/ojg v1.22.0 // indirect
3334
github.com/onsi/ginkgo v1.10.1 // indirect
3435
github.com/onsi/gomega v1.7.0 // indirect
3536
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
4343
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
4444
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
4545
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
46+
github.com/ohler55/ojg v1.22.0 h1:McZObj3cD/Zz/ojzk5Pi5VvgQcagxmT1bVKNzhE5ihI=
47+
github.com/ohler55/ojg v1.22.0/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o=
4648
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
4749
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
4850
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

json.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,54 @@ package httpexpect
33
import (
44
"errors"
55
"fmt"
6+
"github.com/ohler55/ojg/jp"
67
"reflect"
78
"regexp"
89

910
"github.com/xeipuuv/gojsonschema"
1011
"github.com/yalp/jsonpath"
1112
)
1213

14+
func jsonPathOjg(opChain *chain, value interface{}, path string) *Value {
15+
if opChain.failed() {
16+
return newValue(opChain, nil)
17+
}
18+
19+
expr, err := jp.ParseString(path)
20+
if err != nil {
21+
opChain.fail(AssertionFailure{
22+
Type: AssertValid,
23+
Actual: &AssertionValue{path},
24+
Errors: []error{
25+
errors.New("expected: valid json path"),
26+
err,
27+
},
28+
})
29+
return newValue(opChain, nil)
30+
}
31+
result := expr.Get(value)
32+
// in order to keep the results somewhat consistent with yalp's results,
33+
// we return a single value where no wildcards or descends are involved.
34+
// TODO: it might be more consistent if it also included filters
35+
if len(result) == 1 && !hasWildcardsOrDescend(expr) {
36+
return newValue(opChain, result[0])
37+
}
38+
if result == nil {
39+
return newValue(opChain, []interface{}{})
40+
}
41+
return newValue(opChain, result)
42+
}
43+
44+
func hasWildcardsOrDescend(expr jp.Expr) bool {
45+
for _, frag := range expr {
46+
switch frag.(type) {
47+
case jp.Wildcard, jp.Descent:
48+
return true
49+
}
50+
}
51+
return false
52+
}
53+
1354
func jsonPath(opChain *chain, value interface{}, path string) *Value {
1455
if opChain.failed() {
1556
return newValue(opChain, nil)

number.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ func (n *Number) Path(path string) *Value {
9595
return jsonPath(opChain, n.value, path)
9696
}
9797

98+
// Query is similar to Value.Query.
99+
func (n *Number) Query(path string) *Value {
100+
opChain := n.chain.enter("Query(%q)", path)
101+
defer opChain.leave()
102+
103+
return jsonPathOjg(opChain, n.value, path)
104+
}
105+
98106
// Schema is similar to Value.Schema.
99107
func (n *Number) Schema(schema interface{}) *Number {
100108
opChain := n.chain.enter("Schema()")

number_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestNumber_FailedChain(t *testing.T) {
1414
value.chain.assert(t, failure)
1515

1616
value.Path("$").chain.assert(t, failure)
17+
value.Query("$").chain.assert(t, failure)
1718
value.Schema("")
1819
value.Alias("foo")
1920

@@ -155,6 +156,15 @@ func TestNumber_Path(t *testing.T) {
155156
value.chain.assert(t, success)
156157
}
157158

159+
func TestNumber_Query(t *testing.T) {
160+
reporter := newMockReporter(t)
161+
162+
value := NewNumber(reporter, 123.0)
163+
164+
assert.Equal(t, 123.0, value.Query("$").Raw())
165+
value.chain.assert(t, success)
166+
}
167+
158168
func TestNumber_Schema(t *testing.T) {
159169
reporter := newMockReporter(t)
160170

0 commit comments

Comments
 (0)