Skip to content

Commit

Permalink
Adds a new Query() method that works like Path() but uses
Browse files Browse the repository at this point in the history
ohler/ojg to evaluate the jsonPath expression.
  • Loading branch information
stubents committed May 21, 2024
1 parent 07a4dbe commit ce6673e
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 106 deletions.
8 changes: 8 additions & 0 deletions array.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ func (a *Array) Path(path string) *Value {
return jsonPath(opChain, a.value, path)
}

// Query is similar to Value.Query.
func (a *Array) Query(path string) *Value {
opChain := a.chain.enter("Query(%q)", path)
defer opChain.leave()

return jsonPathOjg(opChain, a.value, path)
}

// Schema is similar to Value.Schema.
func (a *Array) Schema(schema interface{}) *Array {
opChain := a.chain.enter("Schema()")
Expand Down
42 changes: 28 additions & 14 deletions array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func TestArray_FailedChain(t *testing.T) {
value.chain.assert(t, failure)

value.Path("$").chain.assert(t, failure)
value.Query("$").chain.assert(t, failure)
value.Schema("")
value.Alias("foo")

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

var jsonPathCases = []struct {
name string
value []interface{}
}{
{
name: "empty",
value: []interface{}{},
},
{
name: "not empty",
value: []interface{}{"foo", 123.0},
},
}

func TestArray_Path(t *testing.T) {
cases := []struct {
name string
value []interface{}
}{
{
name: "empty",
value: []interface{}{},
},
{
name: "not empty",
value: []interface{}{"foo", 123.0},
},
for _, tc := range jsonPathCases {
t.Run(tc.name, func(t *testing.T) {
reporter := newMockReporter(t)

value := NewArray(reporter, tc.value)

assert.Equal(t, tc.value, value.Path("$").Raw())
value.chain.assert(t, success)
})
}
}

for _, tc := range cases {
func TestArray_Query(t *testing.T) {
for _, tc := range jsonPathCases {
t.Run(tc.name, func(t *testing.T) {
reporter := newMockReporter(t)

value := NewArray(reporter, tc.value)

assert.Equal(t, tc.value, value.Path("$").Raw())
assert.Equal(t, tc.value, value.Query("$").Raw())
value.chain.assert(t, success)
})
}
Expand Down
8 changes: 8 additions & 0 deletions boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ func (b *Boolean) Path(path string) *Value {
return jsonPath(opChain, b.value, path)
}

// Query is similar to Value.Query
func (b *Boolean) Query(path string) *Value {
opChain := b.chain.enter("Query(%q)", path)
defer opChain.leave()

return jsonPathOjg(opChain, b.value, path)
}

// Schema is similar to Value.Schema.
func (b *Boolean) Schema(schema interface{}) *Boolean {
opChain := b.chain.enter("Schema()")
Expand Down
10 changes: 10 additions & 0 deletions boolean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestBoolean_FailedChain(t *testing.T) {
value.chain.assert(t, failure)

value.Path("$").chain.assert(t, failure)
value.Query("$").chain.assert(t, failure)
value.Schema("")
value.Alias("foo")

Expand Down Expand Up @@ -133,6 +134,15 @@ func TestBoolean_Path(t *testing.T) {
value.chain.assert(t, success)
}

func TestBoolean_Query(t *testing.T) {
reporter := newMockReporter(t)

value := NewBoolean(reporter, true)

assert.Equal(t, true, value.Query("$").Raw())
value.chain.assert(t, success)
}

func TestBoolean_Schema(t *testing.T) {
reporter := newMockReporter(t)

Expand Down
2 changes: 1 addition & 1 deletion chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// to current assertion starting from chain root
//
// - AssertionHandler: provides methods to handle successful and failed assertions;
// may be defined by user, but usually we just use DefaulAssertionHandler
// may be defined by user, but usually we just use DefaultAssertionHandler
//
// - AssertionSeverity: severity to be used for failures (fatal or non-fatal)
//
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/gavv/httpexpect/v2

go 1.19
go 1.21

require (
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2
Expand Down Expand Up @@ -30,6 +30,7 @@ require (
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/ohler55/ojg v1.22.0 // indirect
github.com/onsi/ginkgo v1.10.1 // indirect
github.com/onsi/gomega v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/ohler55/ojg v1.22.0 h1:McZObj3cD/Zz/ojzk5Pi5VvgQcagxmT1bVKNzhE5ihI=
github.com/ohler55/ojg v1.22.0/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand Down
41 changes: 41 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,54 @@ package httpexpect
import (
"errors"
"fmt"
"github.com/ohler55/ojg/jp"
"reflect"
"regexp"

"github.com/xeipuuv/gojsonschema"
"github.com/yalp/jsonpath"
)

func jsonPathOjg(opChain *chain, value interface{}, path string) *Value {
if opChain.failed() {
return newValue(opChain, nil)
}

expr, err := jp.ParseString(path)
if err != nil {
opChain.fail(AssertionFailure{
Type: AssertValid,
Actual: &AssertionValue{path},
Errors: []error{
errors.New("expected: valid json path"),
err,
},
})
return newValue(opChain, nil)
}
result := expr.Get(value)
// in order to keep the results somewhat consistent with yalp's results,
// we return a single value where no wildcards or descends are involved.
// TODO: it might be more consistent if it also included filters
if len(result) == 1 && !hasWildcardsOrDescend(expr) {
return newValue(opChain, result[0])
}
if result == nil {
return newValue(opChain, []interface{}{})
}
return newValue(opChain, result)
}

func hasWildcardsOrDescend(expr jp.Expr) bool {
for _, frag := range expr {
switch frag.(type) {
case jp.Wildcard, jp.Descent:
return true
}
}
return false
}

func jsonPath(opChain *chain, value interface{}, path string) *Value {
if opChain.failed() {
return newValue(opChain, nil)
Expand Down
8 changes: 8 additions & 0 deletions number.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ func (n *Number) Path(path string) *Value {
return jsonPath(opChain, n.value, path)
}

// Query is similar to Value.Query.
func (n *Number) Query(path string) *Value {
opChain := n.chain.enter("Query(%q)", path)
defer opChain.leave()

return jsonPathOjg(opChain, n.value, path)
}

// Schema is similar to Value.Schema.
func (n *Number) Schema(schema interface{}) *Number {
opChain := n.chain.enter("Schema()")
Expand Down
10 changes: 10 additions & 0 deletions number_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestNumber_FailedChain(t *testing.T) {
value.chain.assert(t, failure)

value.Path("$").chain.assert(t, failure)
value.Query("$").chain.assert(t, failure)
value.Schema("")
value.Alias("foo")

Expand Down Expand Up @@ -155,6 +156,15 @@ func TestNumber_Path(t *testing.T) {
value.chain.assert(t, success)
}

func TestNumber_Query(t *testing.T) {
reporter := newMockReporter(t)

value := NewNumber(reporter, 123.0)

assert.Equal(t, 123.0, value.Query("$").Raw())
value.chain.assert(t, success)
}

func TestNumber_Schema(t *testing.T) {
reporter := newMockReporter(t)

Expand Down
8 changes: 8 additions & 0 deletions object.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ func (o *Object) Path(path string) *Value {
return jsonPath(opChain, o.value, path)
}

// Query is similar to Value.Query.
func (o *Object) Query(query string) *Value {
opChain := o.chain.enter("Query(%q)", query)
defer opChain.leave()

return jsonPathOjg(opChain, o.value, query)
}

// Schema is similar to Value.Schema.
func (o *Object) Schema(schema interface{}) *Object {
opChain := o.chain.enter("Schema()")
Expand Down
18 changes: 18 additions & 0 deletions object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func TestObject_FailedChain(t *testing.T) {
value.chain.assert(t, failure)

value.Path("$").chain.assert(t, failure)
value.Query("$").chain.assert(t, failure)
value.Schema("")
value.Alias("foo")

Expand Down Expand Up @@ -291,6 +292,23 @@ func TestObject_Path(t *testing.T) {
value.chain.assert(t, success)
}

func TestObject_Query(t *testing.T) {
reporter := newMockReporter(t)

m := map[string]interface{}{
"foo": 123.0,
"bar": []interface{}{"456", 789.0},
"baz": map[string]interface{}{
"a": "b",
},
}

value := NewObject(reporter, m)

assert.Equal(t, m, value.Query("$").Raw())
value.chain.assert(t, success)
}

func TestObject_Schema(t *testing.T) {
reporter := newMockReporter(t)

Expand Down
8 changes: 8 additions & 0 deletions string.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ func (s *String) Path(path string) *Value {
return jsonPath(opChain, s.value, path)
}

// Query is similar to Value.Query.
func (s *String) Query(path string) *Value {
opChain := s.chain.enter("Query(%q)", path)
defer opChain.leave()

return jsonPathOjg(opChain, s.value, path)
}

// Schema is similar to Value.Schema.
func (s *String) Schema(schema interface{}) *String {
opChain := s.chain.enter("Schema()")
Expand Down
10 changes: 10 additions & 0 deletions string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestString_FailedChain(t *testing.T) {
value.chain.assert(t, failure)

value.Path("$").chain.assert(t, failure)
value.Query("$").chain.assert(t, failure)
value.Schema("")
value.Alias("foo")

Expand Down Expand Up @@ -163,6 +164,15 @@ func TestString_Path(t *testing.T) {
value.chain.assert(t, success)
}

func TestString_Query(t *testing.T) {
reporter := newMockReporter(t)

value := NewString(reporter, "foo")

assert.Equal(t, "foo", value.Query("$").Raw())
value.chain.assert(t, success)
}

func TestString_Schema(t *testing.T) {
reporter := newMockReporter(t)

Expand Down
7 changes: 7 additions & 0 deletions value.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ func (v *Value) Path(path string) *Value {
return jsonPath(opChain, v.value, path)
}

func (v *Value) Query(path string) *Value {
opChain := v.chain.enter("Query(%q)", path)
defer opChain.leave()

return jsonPathOjg(opChain, v.value, path)
}

// Schema succeeds if value matches given JSON Schema.
//
// JSON Schema specifies a JSON-based format to define the structure of
Expand Down
Loading

0 comments on commit ce6673e

Please sign in to comment.