-
Notifications
You must be signed in to change notification settings - Fork 47
/
json.go
348 lines (322 loc) · 12.2 KB
/
json.go
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
// package match contains matchers for HTTP and JSON data.
//
// Matchers are composable functions which check for the data specified, returning a golang error if a matcher fails.
// They are typically used with the 'must' package in the following way:
//
// res := user.Do(t, "GET", []string{"_matrix", "client", "v3", "rooms", roomID, "state", "m.room.server_acl"})
// must.MatchResponse(t, res, match.HTTPResponse{
// StatusCode: 200,
// JSON: []match.JSON{
// match.JSONKeyEqual("allow", []string{"*"}),
// match.JSONKeyEqual("deny", []string{"hs2"}),
// match.JSONKeyEqual("allow_ip_literals", true),
// },
// })
//
// Matchers have no concept of tests, and do not automatically fail tests if the match fails. This can be useful
// when you want to repeatedly perform a check until it succeeds (e.g from /sync). If you want matches to fail a test,
// you can use the 'must' package.
package match
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/tidwall/gjson"
)
// JSON will perform some matches on the given JSON body, returning an error on a mis-match.
// It can be assumed that the bytes are valid JSON.
type JSON func(body gjson.Result) error
// JSONKeyEqual returns a matcher which will check that `wantKey` is present and its value matches `wantValue`.
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
// `wantValue` is matched via JSONDeepEqual and the JSON takes the forms according to https://godoc.org/github.com/tidwall/gjson#Result.Value
func JSONKeyEqual(wantKey string, wantValue interface{}) JSON {
return func(body gjson.Result) error {
res := body
if wantKey != "" {
res = body.Get(wantKey)
}
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
gotValue := res.Value()
if !jsonDeepEqual([]byte(res.Raw), wantValue) {
return fmt.Errorf(
"key '%s' got '%v' (type %T) want '%v' (type %T)",
wantKey, gotValue, gotValue, wantValue, wantValue,
)
}
return nil
}
}
// JSONKeyPresent returns a matcher which will check that `wantKey` is present in the JSON object.
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
func JSONKeyPresent(wantKey string) JSON {
return func(body gjson.Result) error {
res := body.Get(wantKey)
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
return nil
}
}
// JSONKeyMissing returns a matcher which will check that `forbiddenKey` is not present in the JSON object.
// `forbiddenKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
func JSONKeyMissing(forbiddenKey string) JSON {
return func(body gjson.Result) error {
res := body.Get(forbiddenKey)
if res.Exists() {
return fmt.Errorf("key '%s' present", forbiddenKey)
}
return nil
}
}
// JSONKeyTypeEqual returns a matcher which will check that `wantKey` is present and its value is of the type `wantType`.
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
func JSONKeyTypeEqual(wantKey string, wantType gjson.Type) JSON {
return func(body gjson.Result) error {
res := body.Get(wantKey)
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
if res.Type != wantType {
return fmt.Errorf("key '%s' is of the wrong type, got %s want %s", wantKey, res.Type, wantType)
}
return nil
}
}
// JSONKeyArrayOfSize returns a matcher which will check that `wantKey` is present and
// its value is an array with the given size.
// `wantKey` can be nested, see https://godoc.org/github.com/tidwall/gjson#Get for details.
func JSONKeyArrayOfSize(wantKey string, wantSize int) JSON {
return func(body gjson.Result) error {
res := body.Get(wantKey)
if !res.Exists() {
return fmt.Errorf("key '%s' missing", wantKey)
}
if !res.IsArray() {
return fmt.Errorf("key '%s' is not an array", wantKey)
}
entries := res.Array()
if len(entries) != wantSize {
return fmt.Errorf("key '%s' is an array of the wrong size, got %v want %v", wantKey, len(entries), wantSize)
}
return nil
}
}
type checkOffOpts struct {
allowUnwantedItems bool
mapper func(gjson.Result) interface{}
forEach func(interface{}, gjson.Result) error
}
// CheckOffAllowUnwanted allows unwanted items, that is items not in `wantItems`,
// to not fail the check.
func CheckOffAllowUnwanted() func(*checkOffOpts) {
return func(coo *checkOffOpts) {
coo.allowUnwantedItems = true
}
}
// CheckOffMapper maps each item /before/ continuing the check off process. This
// is useful to convert a gjson.Result to something more domain specific such as
// an event ID. For example, if `r` is a Matrix event, this allows `wantItems` to
// be a slice of event IDs:
//
// CheckOffMapper(func(r gjson.Result) interface{} {
// return r.Get("event_id").Str
// })
//
// The `mapper` function should map the item to an interface which will be
// comparable via JSONDeepEqual with items in `wantItems`.
func CheckOffMapper(mapper func(gjson.Result) interface{}) func(*checkOffOpts) {
return func(coo *checkOffOpts) {
coo.mapper = mapper
}
}
// CheckOffForEach does not change the check off logic, but instead passes each item
// to the provided function. If the function returns an error, the check fails.
// It is called with 2 args: the item being checked and the element itself
// (or value if it's an object).
func CheckOffForEach(forEach func(interface{}, gjson.Result) error) func(*checkOffOpts) {
return func(coo *checkOffOpts) {
coo.forEach = forEach
}
}
// EXPERIMENTAL
// JSONCheckOff returns a matcher which will loop over `wantKey` and ensure that the items
// (which can be array elements or object keys) are present exactly once in `wantItems`.
// This matcher can be used to check off items in an array/object.
//
// This function supports functional options which change the behaviour of the check off
// logic, see match.CheckOff... functions for more information.
//
// Usage: (ensures `events` has these events in any order, with the right event type)
//
// JSONCheckOff("events", []interface{}{"$foo:bar", "$baz:quuz"}, CheckOffMapper(func(r gjson.Result) interface{} {
// return r.Get("event_id").Str
// }), CheckOffForEach(func(eventID interface{}, eventBody gjson.Result) error {
// if eventBody.Get("type").Str != "m.room.message" {
// return fmt.Errorf("expected event to be 'm.room.message'")
// }
// }))
func JSONCheckOff(wantKey string, wantItems []interface{}, opts ...func(*checkOffOpts)) JSON {
var coo checkOffOpts
for _, opt := range opts {
opt(&coo)
}
return func(body gjson.Result) error {
res := body.Get(wantKey)
if !res.Exists() {
return fmt.Errorf("JSONCheckOff: missing key '%s'", wantKey)
}
if !res.IsArray() && !res.IsObject() {
return fmt.Errorf("JSONCheckOff: key '%s' is not an array or object", wantKey)
}
var err error
res.ForEach(func(key, val gjson.Result) bool {
itemRes := key
if res.IsArray() {
itemRes = val
}
var item interface{} = itemRes
if coo.mapper != nil {
// convert it to something we can check off
item = coo.mapper(itemRes)
if item == nil {
err = fmt.Errorf("JSONCheckOff(%s): mapper function mapped %v to nil", wantKey, itemRes.Raw)
return false
}
}
// check off the item
want := -1
for i, w := range wantItems {
wBytes, _ := json.Marshal(w)
if jsonDeepEqual(wBytes, item) {
want = i
break
}
}
if !coo.allowUnwantedItems && want == -1 {
err = fmt.Errorf("JSONCheckOff(%s): unexpected item %v (mapped value %v)", wantKey, itemRes.Raw, item)
return false
}
if want != -1 {
// delete the wanted item
wantItems = append(wantItems[:want], wantItems[want+1:]...)
}
// do further checks
if coo.forEach != nil {
err = coo.forEach(item, val)
if err != nil {
err = fmt.Errorf("JSONCheckOff(%s): forEach function returned an error for item %v: %w", wantKey, val, err)
return false
}
}
return true
})
// at this point we should have gone through all of wantItems.
// If we haven't then we expected to see some items but didn't.
if err == nil && len(wantItems) > 0 {
err = fmt.Errorf("JSONCheckOff(%s): did not see items: %v", wantKey, wantItems)
}
return err
}
}
// DEPRECATED: Prefer JSONCheckOff as this uses functional options which makes params easier to understand.
//
// JSONCheckOff returns a matcher which will loop over `wantKey` and ensure that the items
// (which can be array elements or object keys)
// are present exactly once in any order in `wantItems`. If there are unexpected items or items
// appear more than once then the match fails. This matcher can be used to check off items in
// an array/object. The `mapper` function should map the item to an interface which will be
// comparable via JSONDeepEqual with items in `wantItems`. The optional `fn` callback
// allows more checks to be performed other than checking off the item from the list. It is
// called with 2 args: the result of the `mapper` function and the element itself (or value if
// it's an object).
//
// Usage: (ensures `events` has these events in any order, with the right event type)
//
// JSONCheckOff("events", []interface{}{"$foo:bar", "$baz:quuz"}, func(r gjson.Result) interface{} {
// return r.Get("event_id").Str
// }, func(eventID interface{}, eventBody gjson.Result) error {
// if eventBody.Get("type").Str != "m.room.message" {
// return fmt.Errorf("expected event to be 'm.room.message'")
// }
// })
func JSONCheckOffDeprecated(wantKey string, wantItems []interface{}, mapper func(gjson.Result) interface{}, fn func(interface{}, gjson.Result) error) JSON {
return JSONCheckOff(wantKey, wantItems, CheckOffMapper(mapper), CheckOffForEach(fn))
}
// JSONArrayEach returns a matcher which will check that `wantKey` is an array then loops over each
// item calling `fn`. If `fn` returns an error, iterating stops and an error is returned.
func JSONArrayEach(wantKey string, fn func(gjson.Result) error) JSON {
return func(body gjson.Result) error {
if wantKey != "" {
body = body.Get(wantKey)
}
if !body.Exists() {
return fmt.Errorf("JSONArrayEach: missing key '%s'", wantKey)
}
if !body.IsArray() {
return fmt.Errorf("JSONArrayEach: key '%s' is not an array", wantKey)
}
var err error
body.ForEach(func(_, val gjson.Result) bool {
err = fn(val)
return err == nil
})
return err
}
}
// JSONMapEach returns a matcher which will check that `wantKey` is a map then loops over each
// item calling `fn`. If `fn` returns an error, iterating stops and an error is returned.
func JSONMapEach(wantKey string, fn func(k, v gjson.Result) error) JSON {
return func(body gjson.Result) error {
res := body.Get(wantKey)
if !res.Exists() {
return fmt.Errorf("JSONMapEach: missing key '%s'", wantKey)
}
if !res.IsObject() {
return fmt.Errorf("JSONMapEach: key '%s' is not an object", wantKey)
}
var err error
res.ForEach(func(key, val gjson.Result) bool {
err = fn(key, val)
return err == nil
})
return err
}
}
// EXPERIMENTAL
// AnyOf takes 1 or more `checkers`, and builds a new checker which accepts a given
// json body iff it's accepted by at least one of the original `checkers`.
func AnyOf(checkers ...JSON) JSON {
return func(body gjson.Result) error {
if len(checkers) == 0 {
return fmt.Errorf("must provide at least one checker to AnyOf")
}
errors := make([]error, len(checkers))
for i, check := range checkers {
errors[i] = check(body)
if errors[i] == nil {
return nil
}
}
builder := strings.Builder{}
builder.WriteString("all checks failed:")
for _, err := range errors {
builder.WriteString("\n ")
builder.WriteString(err.Error())
}
return fmt.Errorf(builder.String())
}
}
// jsonDeepEqual compares raw json with a json-serializable value, seeing if they're equal.
// It forces `gotJson` through a JSON parser to ensure keys/whitespace are identical to the marshalled form of `wantValue`.
func jsonDeepEqual(gotJson []byte, wantValue interface{}) bool {
// marshal what the test gave us
wantBytes, _ := json.Marshal(wantValue)
// re-marshal what the network gave us to acount for key ordering
var gotVal interface{}
_ = json.Unmarshal(gotJson, &gotVal)
gotBytes, _ := json.Marshal(gotVal)
return bytes.Equal(gotBytes, wantBytes)
}