github.com/go-kivik/kivik/v4@v4.3.2/x/mango/ast_test.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 package mango 14 15 import ( 16 "regexp" 17 "testing" 18 19 "github.com/google/go-cmp/cmp" 20 "gitlab.com/flimzy/testy" 21 ) 22 23 var cmpOpts = []cmp.Option{ 24 cmp.AllowUnexported(notNode{}, combinationNode{}, conditionNode{}), 25 } 26 27 func TestParse(t *testing.T) { 28 type test struct { 29 input string 30 want Node 31 wantErr string 32 } 33 34 tests := testy.NewTable() 35 tests.Add("empty", test{ 36 input: "{}", 37 want: &combinationNode{ 38 op: OpAnd, 39 sel: nil, 40 }, 41 }) 42 tests.Add("implicit equality", test{ 43 input: `{"foo": "bar"}`, 44 want: &fieldNode{ 45 field: "foo", 46 cond: &conditionNode{ 47 op: OpEqual, 48 cond: "bar", 49 }, 50 }, 51 }) 52 tests.Add("explicit equality", test{ 53 input: `{"foo": {"$eq": "bar"}}`, 54 want: &fieldNode{ 55 field: "foo", 56 cond: &conditionNode{ 57 op: OpEqual, 58 cond: "bar", 59 }, 60 }, 61 }) 62 tests.Add("explicit equality with too many object keys", test{ 63 input: `{"foo": {"$eq": "bar", "$ne": "baz"}}`, 64 wantErr: "too many keys in object", 65 }) 66 tests.Add("implicit equality with empty object", test{ 67 input: `{"foo": {}}`, 68 want: &fieldNode{ 69 field: "foo", 70 cond: &conditionNode{ 71 op: OpEqual, 72 cond: map[string]interface{}{}, 73 }, 74 }, 75 }) 76 tests.Add("explicit invalid comparison operator", test{ 77 input: `{"foo": {"$invalid": "bar"}}`, 78 wantErr: "invalid operator $invalid", 79 }) 80 tests.Add("explicit equality against object", test{ 81 input: `{"foo": {"$eq": {"bar": "baz"}}}`, 82 want: &fieldNode{ 83 field: "foo", 84 cond: &conditionNode{ 85 op: OpEqual, 86 cond: map[string]interface{}{"bar": "baz"}, 87 }, 88 }, 89 }) 90 tests.Add("less than", test{ 91 input: `{"foo": {"$lt": 42}}`, 92 want: &fieldNode{ 93 field: "foo", 94 cond: &conditionNode{ 95 op: OpLessThan, 96 cond: float64(42), 97 }, 98 }, 99 }) 100 tests.Add("less than or equal", test{ 101 input: `{"foo": {"$lte": 42}}`, 102 want: &fieldNode{ 103 field: "foo", 104 cond: &conditionNode{ 105 op: OpLessThanOrEqual, 106 cond: float64(42), 107 }, 108 }, 109 }) 110 tests.Add("not equal", test{ 111 input: `{"foo": {"$ne": 42}}`, 112 want: &fieldNode{ 113 field: "foo", 114 cond: &conditionNode{ 115 op: OpNotEqual, 116 cond: float64(42), 117 }, 118 }, 119 }) 120 tests.Add("greater than", test{ 121 input: `{"foo": {"$gt": 42}}`, 122 want: &fieldNode{ 123 field: "foo", 124 cond: &conditionNode{ 125 op: OpGreaterThan, 126 cond: float64(42), 127 }, 128 }, 129 }) 130 tests.Add("greater than or equal", test{ 131 input: `{"foo": {"$gte": 42}}`, 132 want: &fieldNode{ 133 field: "foo", 134 cond: &conditionNode{ 135 op: OpGreaterThanOrEqual, 136 cond: float64(42), 137 }, 138 }, 139 }) 140 tests.Add("exists", test{ 141 input: `{"foo": {"$exists": true}}`, 142 want: &fieldNode{ 143 field: "foo", 144 cond: &conditionNode{ 145 op: OpExists, 146 cond: true, 147 }, 148 }, 149 }) 150 tests.Add("exists with non-boolean", test{ 151 input: `{"foo": {"$exists": 42}}`, 152 wantErr: "$exists: json: cannot unmarshal number into Go value of type bool", 153 }) 154 tests.Add("type", test{ 155 input: `{"foo": {"$type": "string"}}`, 156 want: &fieldNode{ 157 field: "foo", 158 cond: &conditionNode{ 159 op: OpType, 160 cond: "string", 161 }, 162 }, 163 }) 164 tests.Add("type with non-string", test{ 165 input: `{"foo": {"$type": 42}}`, 166 wantErr: "$type: json: cannot unmarshal number into Go value of type string", 167 }) 168 tests.Add("in", test{ 169 input: `{"foo": {"$in": [1, 2, 3]}}`, 170 want: &fieldNode{ 171 field: "foo", 172 cond: &conditionNode{ 173 op: OpIn, 174 cond: []interface{}{float64(1), float64(2), float64(3)}, 175 }, 176 }, 177 }) 178 tests.Add("in with non-array", test{ 179 input: `{"foo": {"$in": 42}}`, 180 wantErr: "$in: json: cannot unmarshal number into Go value of type []interface {}", 181 }) 182 tests.Add("not in", test{ 183 input: `{"foo": {"$nin": [1, 2, 3]}}`, 184 want: &fieldNode{ 185 field: "foo", 186 cond: &conditionNode{ 187 op: OpNotIn, 188 cond: []interface{}{float64(1), float64(2), float64(3)}, 189 }, 190 }, 191 }) 192 tests.Add("not in with non-array", test{ 193 input: `{"foo": {"$nin": 42}}`, 194 wantErr: "$nin: json: cannot unmarshal number into Go value of type []interface {}", 195 }) 196 tests.Add("size", test{ 197 input: `{"foo": {"$size": 42}}`, 198 want: &fieldNode{ 199 field: "foo", 200 cond: &conditionNode{ 201 op: OpSize, 202 cond: float64(42), 203 }, 204 }, 205 }) 206 tests.Add("size with non-integer", test{ 207 input: `{"foo": {"$size": 42.5}}`, 208 wantErr: "$size: json: cannot unmarshal number 42.5 into Go value of type uint", 209 }) 210 tests.Add("mod", test{ 211 input: `{"foo": {"$mod": [2, 1]}}`, 212 want: &fieldNode{ 213 field: "foo", 214 cond: &conditionNode{ 215 op: OpMod, 216 cond: [2]int64{2, 1}, 217 }, 218 }, 219 }) 220 tests.Add("mod with non-array", test{ 221 input: `{"foo": {"$mod": 42}}`, 222 wantErr: "$mod: json: cannot unmarshal number into Go value of type [2]int64", 223 }) 224 tests.Add("mod with zero divisor", test{ 225 input: `{"foo": {"$mod": [0, 1]}}`, 226 wantErr: "$mod: divisor must be non-zero", 227 }) 228 tests.Add("regex", test{ 229 input: `{"foo": {"$regex": "^bar$"}}`, 230 want: &fieldNode{ 231 field: "foo", 232 cond: &conditionNode{ 233 op: OpRegex, 234 cond: regexp.MustCompile("^bar$"), 235 }, 236 }, 237 }) 238 tests.Add("regexp non-string", test{ 239 input: `{"foo": {"$regex": 42}}`, 240 wantErr: "$regex: json: cannot unmarshal number into Go value of type string", 241 }) 242 tests.Add("regexp invalid", test{ 243 input: `{"foo": {"$regex": "["}}`, 244 wantErr: "$regex: error parsing regexp: missing closing ]: `[`", 245 }) 246 tests.Add("implicit $and", test{ 247 input: `{"foo":"bar","baz":"qux"}`, 248 want: &combinationNode{ 249 op: OpAnd, 250 sel: []Node{ 251 &fieldNode{ 252 field: "baz", 253 cond: &conditionNode{ 254 op: OpEqual, 255 cond: "qux", 256 }, 257 }, 258 &fieldNode{ 259 field: "foo", 260 cond: &conditionNode{ 261 op: OpEqual, 262 cond: "bar", 263 }, 264 }, 265 }, 266 }, 267 }) 268 tests.Add("explicit $and", test{ 269 input: `{"$and":[{"foo":"bar"},{"baz":"qux"}]}`, 270 want: &combinationNode{ 271 op: OpAnd, 272 sel: []Node{ 273 &fieldNode{ 274 field: "foo", 275 cond: &conditionNode{ 276 op: OpEqual, 277 cond: "bar", 278 }, 279 }, 280 &fieldNode{ 281 field: "baz", 282 cond: &conditionNode{ 283 op: OpEqual, 284 cond: "qux", 285 }, 286 }, 287 }, 288 }, 289 }) 290 tests.Add("nested implicit and explicit $and", test{ 291 input: `{"$and":[{"foo":"bar"},{"baz":"qux"}, {"quux":"corge","grault":"garply"}]}`, 292 want: &combinationNode{ 293 op: OpAnd, 294 sel: []Node{ 295 &fieldNode{ 296 field: "foo", 297 cond: &conditionNode{ 298 op: OpEqual, 299 cond: "bar", 300 }, 301 }, 302 &fieldNode{ 303 field: "baz", 304 cond: &conditionNode{ 305 op: OpEqual, 306 cond: "qux", 307 }, 308 }, 309 &combinationNode{ 310 op: OpAnd, 311 sel: []Node{ 312 &fieldNode{ 313 field: "grault", 314 cond: &conditionNode{ 315 op: OpEqual, 316 cond: "garply", 317 }, 318 }, 319 &fieldNode{ 320 field: "quux", 321 cond: &conditionNode{ 322 op: OpEqual, 323 cond: "corge", 324 }, 325 }, 326 }, 327 }, 328 }, 329 }, 330 }) 331 tests.Add("$or", test{ 332 input: `{"$or":[{"foo":"bar"},{"baz":"qux"}]}`, 333 want: &combinationNode{ 334 op: OpOr, 335 sel: []Node{ 336 &fieldNode{ 337 field: "foo", 338 cond: &conditionNode{ 339 op: OpEqual, 340 cond: "bar", 341 }, 342 }, 343 &fieldNode{ 344 field: "baz", 345 cond: &conditionNode{ 346 op: OpEqual, 347 cond: "qux", 348 }, 349 }, 350 }, 351 }, 352 }) 353 tests.Add("invalid operator", test{ 354 input: `{"$invalid": "bar"}`, 355 wantErr: "unknown operator $invalid", 356 }) 357 tests.Add("$not", test{ 358 input: `{"$not": {"foo":"bar"}}`, 359 want: ¬Node{ 360 sel: &fieldNode{ 361 field: "foo", 362 cond: &conditionNode{ 363 op: OpEqual, 364 cond: "bar", 365 }, 366 }, 367 }, 368 }) 369 tests.Add("$not with invalid selector", test{ 370 input: `{"$not": []}`, 371 wantErr: "$not: json: cannot unmarshal array into Go value of type map[string]json.RawMessage", 372 }) 373 tests.Add("$and with invalid selector array", test{ 374 input: `{"$and": {}}`, 375 wantErr: "$and: json: cannot unmarshal object into Go value of type []json.RawMessage", 376 }) 377 tests.Add("$and with invalid selector", test{ 378 input: `{"$and": [42]}`, 379 wantErr: "$and: json: cannot unmarshal number into Go value of type map[string]json.RawMessage", 380 }) 381 tests.Add("$nor", test{ 382 input: `{"$nor":[{"foo":"bar"},{"baz":"qux"}]}`, 383 want: &combinationNode{ 384 op: OpNor, 385 sel: []Node{ 386 &fieldNode{ 387 field: "foo", 388 cond: &conditionNode{ 389 op: OpEqual, 390 cond: "bar", 391 }, 392 }, 393 &fieldNode{ 394 field: "baz", 395 cond: &conditionNode{ 396 op: OpEqual, 397 cond: "qux", 398 }, 399 }, 400 }, 401 }, 402 }) 403 tests.Add("$all", test{ 404 input: `{"foo": {"$all": ["bar", "baz"]}}`, 405 want: &fieldNode{ 406 field: "foo", 407 cond: &conditionNode{ 408 op: OpAll, 409 cond: []interface{}{"bar", "baz"}, 410 }, 411 }, 412 }) 413 tests.Add("$all with non-array", test{ 414 input: `{"foo": {"$all": "bar"}}`, 415 wantErr: "$all: json: cannot unmarshal string into Go value of type []interface {}", 416 }) 417 tests.Add("$elemMatch", test{ 418 input: `{"genre": {"$elemMatch": {"$eq": "Horror"}}}`, 419 want: &fieldNode{ 420 field: "genre", 421 cond: &elementNode{ 422 op: OpElemMatch, 423 cond: &conditionNode{ 424 op: OpEqual, 425 cond: "Horror", 426 }, 427 }, 428 }, 429 }) 430 tests.Add("$allMatch", test{ 431 input: `{"genre": {"$allMatch": {"$eq": "Horror"}}}`, 432 want: &fieldNode{ 433 field: "genre", 434 cond: &elementNode{ 435 op: OpAllMatch, 436 cond: &conditionNode{ 437 op: OpEqual, 438 cond: "Horror", 439 }, 440 }, 441 }, 442 }) 443 tests.Add("$keyMapMatch", test{ 444 input: `{"cameras": {"$keyMapMatch": {"$eq": "secondary"}}}`, 445 want: &fieldNode{ 446 field: "cameras", 447 cond: &elementNode{ 448 op: OpKeyMapMatch, 449 cond: &conditionNode{ 450 op: OpEqual, 451 cond: "secondary", 452 }, 453 }, 454 }, 455 }) 456 tests.Add("element selector with invalid selector", test{ 457 input: `{"cameras": {"$keyMapMatch": 42}}`, 458 wantErr: "$keyMapMatch: json: cannot unmarshal number into Go value of type map[string]json.RawMessage", 459 }) 460 461 /* 462 TODO: 463 - $mod with non-integer values returns 404 (WTF) https://docs.couchdb.org/en/stable/api/database/find.html#condition-operators 464 465 */ 466 467 tests.Run(t, func(t *testing.T, tt test) { 468 got, err := Parse([]byte(tt.input)) 469 if !testy.ErrorMatches(tt.wantErr, err) { 470 t.Fatalf("Unexpected error: %s", err) 471 } 472 if err != nil { 473 return 474 } 475 if d := cmp.Diff(tt.want.String(), got.String(), cmpOpts...); d != "" { 476 t.Errorf("Unexpected result (-want +got):\n%s", d) 477 } 478 }) 479 }