...

Source file src/github.com/go-kivik/kivik/v4/x/mango/ast_test.go

Documentation: github.com/go-kivik/kivik/v4/x/mango

     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: &notNode{
   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  }
   480  

View as plain text