Skip to content

Commit ce6673e

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 ce6673e

16 files changed

+368
-106
lines changed

array.go

+8
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

+28-14
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

+8
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

+10
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

+1-1
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

+2-1
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

+2
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

+41
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

+8
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

+10
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

object.go

+8
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ func (o *Object) Path(path string) *Value {
133133
return jsonPath(opChain, o.value, path)
134134
}
135135

136+
// Query is similar to Value.Query.
137+
func (o *Object) Query(query string) *Value {
138+
opChain := o.chain.enter("Query(%q)", query)
139+
defer opChain.leave()
140+
141+
return jsonPathOjg(opChain, o.value, query)
142+
}
143+
136144
// Schema is similar to Value.Schema.
137145
func (o *Object) Schema(schema interface{}) *Object {
138146
opChain := o.chain.enter("Schema()")

object_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func TestObject_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

@@ -291,6 +292,23 @@ func TestObject_Path(t *testing.T) {
291292
value.chain.assert(t, success)
292293
}
293294

295+
func TestObject_Query(t *testing.T) {
296+
reporter := newMockReporter(t)
297+
298+
m := map[string]interface{}{
299+
"foo": 123.0,
300+
"bar": []interface{}{"456", 789.0},
301+
"baz": map[string]interface{}{
302+
"a": "b",
303+
},
304+
}
305+
306+
value := NewObject(reporter, m)
307+
308+
assert.Equal(t, m, value.Query("$").Raw())
309+
value.chain.assert(t, success)
310+
}
311+
294312
func TestObject_Schema(t *testing.T) {
295313
reporter := newMockReporter(t)
296314

string.go

+8
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ func (s *String) Path(path string) *Value {
9999
return jsonPath(opChain, s.value, path)
100100
}
101101

102+
// Query is similar to Value.Query.
103+
func (s *String) Query(path string) *Value {
104+
opChain := s.chain.enter("Query(%q)", path)
105+
defer opChain.leave()
106+
107+
return jsonPathOjg(opChain, s.value, path)
108+
}
109+
102110
// Schema is similar to Value.Schema.
103111
func (s *String) Schema(schema interface{}) *String {
104112
opChain := s.chain.enter("Schema()")

string_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestString_FailedChain(t *testing.T) {
1515
value.chain.assert(t, failure)
1616

1717
value.Path("$").chain.assert(t, failure)
18+
value.Query("$").chain.assert(t, failure)
1819
value.Schema("")
1920
value.Alias("foo")
2021

@@ -163,6 +164,15 @@ func TestString_Path(t *testing.T) {
163164
value.chain.assert(t, success)
164165
}
165166

167+
func TestString_Query(t *testing.T) {
168+
reporter := newMockReporter(t)
169+
170+
value := NewString(reporter, "foo")
171+
172+
assert.Equal(t, "foo", value.Query("$").Raw())
173+
value.chain.assert(t, success)
174+
}
175+
166176
func TestString_Schema(t *testing.T) {
167177
reporter := newMockReporter(t)
168178

value.go

+7
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ func (v *Value) Path(path string) *Value {
172172
return jsonPath(opChain, v.value, path)
173173
}
174174

175+
func (v *Value) Query(path string) *Value {
176+
opChain := v.chain.enter("Query(%q)", path)
177+
defer opChain.leave()
178+
179+
return jsonPathOjg(opChain, v.value, path)
180+
}
181+
175182
// Schema succeeds if value matches given JSON Schema.
176183
//
177184
// JSON Schema specifies a JSON-based format to define the structure of

0 commit comments

Comments
 (0)