From 7806655756624541733e9e53ddc3a04167d04e30 Mon Sep 17 00:00:00 2001 From: jonassnellinckx Date: Tue, 19 Dec 2017 17:11:20 +0100 Subject: [PATCH] initial commit --- .gitignore | 2 + query/query.go | 263 ++++++++++++++++++++++++++++++++++++ query/rules/Rule.go | 41 ++++++ query/rules/all.go | 61 +++++++++ query/rules/and.go | 60 ++++++++ query/rules/elem-match.go | 63 +++++++++ query/rules/eq.go | 37 +++++ query/rules/exists.go | 28 ++++ query/rules/gt.go | 47 +++++++ query/rules/gte.go | 33 +++++ query/rules/in.go | 39 ++++++ query/rules/lt.go | 32 +++++ query/rules/lte.go | 32 +++++ query/rules/mod.go | 51 +++++++ query/rules/ne.go | 25 ++++ query/rules/nin.go | 25 ++++ query/rules/nor.go | 29 ++++ query/rules/not.go | 28 ++++ query/rules/or.go | 60 ++++++++ query/rules/regex.go | 37 +++++ query/rules/size.go | 38 ++++++ query/rules/type.go | 53 ++++++++ query/test/all_test.go | 54 ++++++++ query/test/and_test.go | 63 +++++++++ query/test/elematch_test.go | 76 +++++++++++ query/test/eq_test.go | 140 +++++++++++++++++++ query/test/exists_test.go | 66 +++++++++ query/test/gt_test.go | 47 +++++++ query/test/gte_test.go | 47 +++++++ query/test/in_test.go | 51 +++++++ query/test/lt_test.go | 47 +++++++ query/test/lte_test.go | 28 ++++ query/test/main_test.go | 71 ++++++++++ query/test/mod_test.go | 28 ++++ query/test/ne_test.go | 45 ++++++ query/test/or_test.go | 63 +++++++++ query/test/size_test.go | 28 ++++ query/test/type_test.go | 87 ++++++++++++ 38 files changed, 2025 insertions(+) create mode 100644 .gitignore create mode 100644 query/query.go create mode 100644 query/rules/Rule.go create mode 100644 query/rules/all.go create mode 100644 query/rules/and.go create mode 100644 query/rules/elem-match.go create mode 100644 query/rules/eq.go create mode 100644 query/rules/exists.go create mode 100644 query/rules/gt.go create mode 100644 query/rules/gte.go create mode 100644 query/rules/in.go create mode 100644 query/rules/lt.go create mode 100644 query/rules/lte.go create mode 100644 query/rules/mod.go create mode 100644 query/rules/ne.go create mode 100644 query/rules/nin.go create mode 100644 query/rules/nor.go create mode 100644 query/rules/not.go create mode 100644 query/rules/or.go create mode 100644 query/rules/regex.go create mode 100644 query/rules/size.go create mode 100644 query/rules/type.go create mode 100644 query/test/all_test.go create mode 100644 query/test/and_test.go create mode 100644 query/test/elematch_test.go create mode 100644 query/test/eq_test.go create mode 100644 query/test/exists_test.go create mode 100644 query/test/gt_test.go create mode 100644 query/test/gte_test.go create mode 100644 query/test/in_test.go create mode 100644 query/test/lt_test.go create mode 100644 query/test/lte_test.go create mode 100644 query/test/main_test.go create mode 100644 query/test/mod_test.go create mode 100644 query/test/ne_test.go create mode 100644 query/test/or_test.go create mode 100644 query/test/size_test.go create mode 100644 query/test/type_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ddf18e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +chaincode \ No newline at end of file diff --git a/query/query.go b/query/query.go new file mode 100644 index 0000000..caae511 --- /dev/null +++ b/query/query.go @@ -0,0 +1,263 @@ +package query + +import ( + "errors" + "test/couchdbMockQuery/query/rules" + "reflect" + "fmt" + "strings" + "encoding/json" +) + +var Registry []rules.IGenericRule + +func init() { + + Registry = []rules.IGenericRule{ + // === + // Condition Operators == http://docs.couchdb.org/en/2.1.0/api/database/find.html?highlight=find#condition-operators + // === + + /// Operator type - (In)equality + rules.IGenericRule(rules.Lt), + rules.IGenericRule(rules.Lte), + + rules.IGenericRule(rules.Eq), + rules.IGenericRule(rules.Ne), + + rules.IGenericRule(rules.Gte), + rules.IGenericRule(rules.Gt), + + /// Operator type - Object + rules.IGenericRule(rules.Exists), + rules.IGenericRule(rules.Type), + + /// Operator type - Array + rules.IGenericRule(rules.In), + rules.IGenericRule(rules.Nin), + rules.IGenericRule(rules.Size), + + /// Operator type - Miscellaneous + rules.IGenericRule(rules.Mod), + rules.IGenericRule(rules.Regex), + + // === + // Combination Operators == http://docs.couchdb.org/en/2.1.0/api/database/find.html?highlight=find#combination-operators + // === + + rules.IGenericRule(rules.And(test)), + rules.IGenericRule(rules.Or(test)), + rules.IGenericRule(rules.Not(test)), + rules.IGenericRule(rules.Nor(test)), + rules.IGenericRule(rules.All), + rules.IGenericRule(rules.ElemMatch(test)), + } +} + +type StateCouchDBQueryObject struct { + Key string + Value interface{} +} + +type StateCouchDBQueryResult []StateCouchDBQueryObject + +// Query engine which controls and accept the whole query. It makes sure every field is filtered. +func ParseCouchDBQuery(data map[string]interface{}, userQueryMap map[string]interface{}) (StateCouchDBQueryResult, error) { + + userQueryAsBytes, err := json.Marshal(userQueryMap) + + if err != nil { + return nil, err + } + + return ParseCouchDBQueryString(data, string(userQueryAsBytes)) + +} + +func ParseCouchDBQueryString(data map[string]interface{}, userQueryString string) (StateCouchDBQueryResult, error) { + + var userQuery map[string]interface{} + parseErr := json.Unmarshal([]byte(userQueryString), &userQuery) + + if parseErr != nil { + return StateCouchDBQueryResult{}, errors.New("Error parsing query string") + } + + if userQuery["selector"] == nil { + return StateCouchDBQueryResult{}, errors.New("Invalid query, selector required") + } + + var response StateCouchDBQueryResult + + for k, v := range data { + result, err := test(v, userQuery["selector"]) + + if err != nil { + return []StateCouchDBQueryObject{}, err + + } else if result { + response = append(response, StateCouchDBQueryObject{ + k, + v, + }) + + } + } + + // Take slice between skip and limit + + var skip = 0 + var limit = len(response) + + if userQuery["skip"] != nil { + skip, ok := userQuery["skip"].(int) + + if ok { + skip = skip + } else { + return StateCouchDBQueryResult{}, errors.New("Skip must be an integer") + } + } + + if userQuery["limit"] != nil { + limit, ok := userQuery["limit"].(int) + + if ok { + limit = limit + } else { + return StateCouchDBQueryResult{}, errors.New("Limit must be an integer") + } + + } + + response = response[skip:limit] + + return response, nil + +} + +// Matcher function which delegates the work to the different rules based on the key and compares values to +// implicit match values. + +func test(data interface{}, userQuery interface{}) (bool, error) { + + if canDecend(userQuery) && reflect.TypeOf(userQuery).Kind() == reflect.Map { + + userQuery, ok := userQuery.(map[string]interface{}) + + if !ok { + return false, errors.New("Data is not a map") + } + + for k, v := range userQuery { + + if string(k[0]) == "$" { + rule, err := getRule(k) + + if err != nil { + return false, err + } + + matched, err1 := rule.Match(data, v) + + if err1 != nil { + return false, err1 + } + + return matched, nil + + } else { + + dvp, dk := resolveSubdocumentQuery(data, k) + + if dvp != nil && len(dk) == 1 { + return test(dvp[dk[0]], v) + } + + return test(nil, v) + } + } + + } else { + + return rules.Eq.Match(data, userQuery) + } + + return false, nil +} + +// getRule function is used to find rule based on it's name +func getRule(name string) (rules.IGenericRule, error) { + + for _, rule := range Registry { + if rule.GetName() == name { + return rule, nil + } + } + + return nil, fmt.Errorf("Rule %s not found!", name) + +} + +// resolveSubdocumentQuery function is used to match names like "foo.bar" to get the "bar" value in a subdocument +func resolveSubdocumentQuery(data interface{}, key string) (map[string]interface{}, []string) { + + var last []string + var deepObject map[string]interface{} + + stack := strings.Split(key, ".") + + if len(stack) > 0 { + item := stack[len(stack)-1] + + stack = stack[:len(stack)-1] + + last = append([]string{item}, last...) + } + + _, ok := data.(map[string]interface{}) + + if ok { + deepObject, _ = data.(map[string]interface{}) + + for len(stack) > 0 { + x := stack[0] + stack = stack[1:] + + t := interface{}(deepObject[x]) + + newObj, ok := t.(map[string]interface{}) + + if ok { + deepObject = newObj + } else { + stack = stack[:len(stack)-1] + } + + } + } + + last = append(stack, last...) + + if deepObject == nil { + deepObject = data.(map[string]interface{}) + } + + return deepObject, last + +} + +func canDecend(a interface{}) bool { + + rt := reflect.TypeOf(a) + + if (rt.Kind() == reflect.Slice || rt.Kind() == reflect.Slice) && reflect.ValueOf(a).Len() > 0 { + return true + } + + if rt.Kind() == reflect.Map && len(reflect.ValueOf(a).MapKeys()) > 0 { + return true + } + + return false +} diff --git a/query/rules/Rule.go b/query/rules/Rule.go new file mode 100644 index 0000000..aa3e411 --- /dev/null +++ b/query/rules/Rule.go @@ -0,0 +1,41 @@ +package rules + +import ( + "errors" + "reflect" +) + +type Rule struct { + Name string +} + +type IGenericRule interface { + GetName() string + Match(valA interface{}, valB interface{}) (bool, error) +} + +func (r Rule) Match(valA interface{}, valB interface{}) (*bool, error) { + return nil, errors.New(r.Name + " not implemented!") +} + +type Testfunc func(interface{}, interface{}) (bool, error) + +// Utils + +func HasElem(s interface{}, elem interface{}) bool { + arrV := reflect.ValueOf(s) + + if arrV.Kind() == reflect.Slice { + for i := 0; i < arrV.Len(); i++ { + + result, _ := Eq.Match(elem, arrV.Index(i).Interface()) + + if result { + return result + } + + } + } + + return false +} diff --git a/query/rules/all.go b/query/rules/all.go new file mode 100644 index 0000000..e2ea003 --- /dev/null +++ b/query/rules/all.go @@ -0,0 +1,61 @@ +package rules + +import ( + "reflect" + "errors" +) + +type all struct { + Rule +} + +func (r all) Match(valA interface{}, valB interface{}) (bool, error) { + + akind := reflect.TypeOf(valA).Kind() + bkind := reflect.TypeOf(valB).Kind() + + if akind != reflect.Array && akind != reflect.Slice { + return false, errors.New("All parameter must be an array") + } + + if bkind != reflect.Array && bkind != reflect.Slice { + return false, errors.New("All value must be an array") + } + + all := true + + reflectOfB := reflect.ValueOf(valB) + + parsedValB := make([]interface{}, reflectOfB.Len()) + + for i := 0; i < reflectOfB.Len(); i++ { + parsedValB[i] = reflectOfB.Index(i).Interface() + } + + reflectOfA := reflect.ValueOf(valA) + + parsedValA := make([]interface{}, reflectOfA.Len()) + + for i := 0; i < reflectOfA.Len(); i++ { + parsedValA[i] = reflectOfA.Index(i).Interface() + } + + for _, v := range parsedValB { + if !HasElem(parsedValA, v) { + all = false + break + } + } + + return all, nil +} + +func (r all) GetName() string { + return r.Rule.Name +} + +var All = all{ + Rule{ + "$all", + }, +} diff --git a/query/rules/and.go b/query/rules/and.go new file mode 100644 index 0000000..e2a3e8d --- /dev/null +++ b/query/rules/and.go @@ -0,0 +1,60 @@ +package rules + +import ( + "reflect" + "errors" +) + +type and struct { + test Testfunc + Rule +} + +func (r and) Match(valA interface{}, valB interface{}) (bool, error) { + + if valA == nil { + return false, nil + } + + bkind := reflect.TypeOf(valB).Kind() + + if bkind != reflect.Array && bkind != reflect.Slice { + return false, errors.New("And value must be an array") + } + + reflectOfB := reflect.ValueOf(valB) + + parsedValB := make([]interface{}, reflectOfB.Len()) + + for i := 0; i < reflectOfB.Len(); i++ { + parsedValB[i] = reflectOfB.Index(i).Interface() + } + + var last bool = true + + for _, v := range parsedValB { + result, err := r.test(valA, v) + + if err != nil { + return false, err + break + } + + last = last && result + } + + return last, nil +} + +func (r and) GetName() string { + return r.Rule.Name +} + +var And = func(testfunc Testfunc) and { + return and{ + testfunc, + Rule{ + "$and", + }, + } +} diff --git a/query/rules/elem-match.go b/query/rules/elem-match.go new file mode 100644 index 0000000..3313bbd --- /dev/null +++ b/query/rules/elem-match.go @@ -0,0 +1,63 @@ +package rules + +import ( + "reflect" + "errors" +) + +type elemMatch struct { + test Testfunc + Rule +} + +func (r elemMatch) Match(valA interface{}, valB interface{}) (bool, error) { + + if valA == nil { + return false, nil + } + + akind := reflect.TypeOf(valA).Kind() + + if akind != reflect.Array && akind != reflect.Slice { + return false, errors.New("All parameter must be an array") + } + + elemMatch := false + + reflectOfA := reflect.ValueOf(valA) + + parsedValA := make([]interface{}, reflectOfA.Len()) + + for i := 0; i < reflectOfA.Len(); i++ { + parsedValA[i] = reflectOfA.Index(i).Interface() + } + + for _, v := range parsedValA { + result, err := r.test(v, valB) + + if err != nil { + return false, err + } + + if result { + elemMatch = true + break + } + } + + return elemMatch, nil +} + +func (r elemMatch) GetName() string { + return r.Rule.Name +} + +var ElemMatch = func(testfunc Testfunc) elemMatch { + + return elemMatch{ + testfunc, + Rule{ + "$elemMatch", + }, + } +} diff --git a/query/rules/eq.go b/query/rules/eq.go new file mode 100644 index 0000000..5c1951e --- /dev/null +++ b/query/rules/eq.go @@ -0,0 +1,37 @@ +package rules + +import "reflect" + +type eq struct { + Rule +} + +func (r eq) Match(valA interface{}, valB interface{}) (bool, error) { + + akind := reflect.TypeOf(valA).Kind() + + if akind == reflect.Int { + valA = float64(valA.(int)) + } + + if akind == reflect.Slice { + interfaceData := make([]interface{}, reflect.ValueOf(valA).Len()) + for i := 0; i < reflect.ValueOf(valA).Len(); i++ { + interfaceData[i] = reflect.ValueOf(valA).Index(i).Interface() + } + + return reflect.DeepEqual(interfaceData, valB), nil + } + + return reflect.DeepEqual(valA, valB), nil +} + +func (r eq) GetName() string { + return r.Rule.Name +} + +var Eq = eq{ + Rule{ + "$eq", + }, +} diff --git a/query/rules/exists.go b/query/rules/exists.go new file mode 100644 index 0000000..862fee4 --- /dev/null +++ b/query/rules/exists.go @@ -0,0 +1,28 @@ +package rules + +import "errors" + +type exists struct { + Rule +} + +func (r exists) Match(valA interface{}, valB interface{}) (bool, error) { + + valB, ok := valB.(bool) + + if !ok { + return false, errors.New("Exists value must be a boolean") + } + + return (valA != nil) == valB, nil +} + +func (r exists) GetName() string { + return r.Rule.Name +} + +var Exists = exists{ + Rule{ + "$exists", + }, +} diff --git a/query/rules/gt.go b/query/rules/gt.go new file mode 100644 index 0000000..daa3f32 --- /dev/null +++ b/query/rules/gt.go @@ -0,0 +1,47 @@ +package rules + +import ( + "reflect" +) + +type gt struct { + Rule +} + +func (r gt) Match(valA interface{}, valB interface{}) (bool, error) { + + typeOfA := reflect.TypeOf(valA) + typeOfB := reflect.TypeOf(valB) + + if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.String { + return valA.(string) > valB.(string), nil + } else if typeOfA.Kind() == reflect.Int && typeOfB.Kind() == reflect.Float64 { + return float64(valA.(int)) > valB.(float64), nil + } else if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.Int { + return float64(getSum(valA.(string))) > valB.(float64), nil + } + + return false, nil +} + +func getSum(s string) int32 { + var sum int32 = 0 + + var integers []int32 = []rune(s) + + for _, v := range integers { + sum += v + } + + return sum +} + +func (r gt) GetName() string { + return r.Rule.Name +} + +var Gt = gt{ + Rule{ + "$gt", + }, +} diff --git a/query/rules/gte.go b/query/rules/gte.go new file mode 100644 index 0000000..8e22d32 --- /dev/null +++ b/query/rules/gte.go @@ -0,0 +1,33 @@ +package rules + +import "reflect" + +type gte struct { + Rule +} + +func (r gte) Match(valA interface{}, valB interface{}) (bool, error) { + + typeOfA := reflect.TypeOf(valA) + typeOfB := reflect.TypeOf(valB) + + if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.String { + return valA.(string) >= valB.(string), nil + } else if typeOfA.Kind() == reflect.Int && typeOfB.Kind() == reflect.Float64 { + return float64(valA.(int)) >= valB.(float64), nil + } else if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.Int { + return float64(getSum(valA.(string))) >= valB.(float64), nil + } + + return false, nil +} + +func (r gte) GetName() string { + return r.Rule.Name +} + +var Gte = gte{ + Rule{ + "$gte", + }, +} diff --git a/query/rules/in.go b/query/rules/in.go new file mode 100644 index 0000000..d917d8a --- /dev/null +++ b/query/rules/in.go @@ -0,0 +1,39 @@ +package rules + +import ( + "reflect" + "errors" +) + +type in struct { + Rule +} + +func (r in) Match(valA interface{}, valB interface{}) (bool, error) { + + bkind := reflect.TypeOf(valB).Kind() + + if bkind != reflect.Array && bkind != reflect.Slice { + return false, errors.New("In value must be an array") + } + + reflectOfB := reflect.ValueOf(valB) + + parsedValB := make([]interface{}, reflectOfB.Len()) + + for i := 0; i < reflectOfB.Len(); i++ { + parsedValB[i] = reflectOfB.Index(i).Interface() + } + + return HasElem(parsedValB, valA), nil +} + +func (r in) GetName() string { + return r.Rule.Name +} + +var In = in{ + Rule{ + "$in", + }, +} diff --git a/query/rules/lt.go b/query/rules/lt.go new file mode 100644 index 0000000..630ed15 --- /dev/null +++ b/query/rules/lt.go @@ -0,0 +1,32 @@ +package rules + +import "reflect" + +type lt struct { + Rule +} + +func (r lt) Match(valA interface{}, valB interface{}) (bool, error) { + typeOfA := reflect.TypeOf(valA) + typeOfB := reflect.TypeOf(valB) + + if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.String { + return valA.(string) < valB.(string), nil + } else if typeOfA.Kind() == reflect.Int && typeOfB.Kind() == reflect.Float64 { + return float64(valA.(int)) < valB.(float64), nil + } else if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.Int { + return float64(getSum(valA.(string))) < valB.(float64), nil + } + + return false, nil +} + +func (r lt) GetName() string { + return r.Rule.Name +} + +var Lt = lt{ + Rule{ + "$lt", + }, +} diff --git a/query/rules/lte.go b/query/rules/lte.go new file mode 100644 index 0000000..de0e02b --- /dev/null +++ b/query/rules/lte.go @@ -0,0 +1,32 @@ +package rules + +import "reflect" + +type lte struct { + Rule +} + +func (r lte) Match(valA interface{}, valB interface{}) (bool, error) { + typeOfA := reflect.TypeOf(valA) + typeOfB := reflect.TypeOf(valB) + + if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.String { + return valA.(string) <= valB.(string), nil + } else if typeOfA.Kind() == reflect.Int && typeOfB.Kind() == reflect.Float64 { + return float64(valA.(int)) <= valB.(float64), nil + } else if typeOfA.Kind() == reflect.String && typeOfB.Kind() == reflect.Float64 { + return float64(getSum(valA.(string))) <= valB.(float64), nil + } + + return false, nil +} + +func (r lte) GetName() string { + return r.Rule.Name +} + +var Lte = lte{ + Rule{ + "$lte", + }, +} diff --git a/query/rules/mod.go b/query/rules/mod.go new file mode 100644 index 0000000..c074fba --- /dev/null +++ b/query/rules/mod.go @@ -0,0 +1,51 @@ +package rules + +import ( + "reflect" + "errors" +) + +type mod struct { + Rule +} + +func (r mod) Match(valA interface{}, valB interface{}) (bool, error) { + + parsedValA, ok := valA.(int) + + if !ok { + return false, errors.New("Document value must be an integer to use $mod") + } + + kind := reflect.TypeOf(valB).Kind() + ref := reflect.ValueOf(valB) + + if (kind == reflect.Array || kind == reflect.Slice && ref.Len() != 2) || (kind != reflect.Slice && kind != reflect.Array) { + return false, errors.New("Mod value must be an array containing [Divisor, Remainder]") + } + + parsedValB := valB.([]interface{}) + + if _, ok := parsedValB[0].(float64); !ok { + return false, errors.New("Mod [Divisor, Remainder] must both be integers") + } + + if _, ok := parsedValB[1].(float64); !ok { + return false, errors.New("Mod [Divisor, Remainder] must both be integers") + } + + divisor := int(parsedValB[0].(float64)) + remainder := int(parsedValB[1].(float64)) + + return parsedValA%divisor == remainder, nil +} + +func (r mod) GetName() string { + return r.Rule.Name +} + +var Mod = mod{ + Rule{ + "$mod", + }, +} diff --git a/query/rules/ne.go b/query/rules/ne.go new file mode 100644 index 0000000..c38c928 --- /dev/null +++ b/query/rules/ne.go @@ -0,0 +1,25 @@ +package rules + +type ne struct { + Rule +} + +func (r ne) Match(valA interface{}, valB interface{}) (bool, error) { + result, err := Eq.Match(valA, valB) + + if err != nil { + return false, err + } + + return !result, nil +} + +func (r ne) GetName() string { + return r.Rule.Name +} + +var Ne = ne{ + Rule{ + "$ne", + }, +} diff --git a/query/rules/nin.go b/query/rules/nin.go new file mode 100644 index 0000000..a3adb4e --- /dev/null +++ b/query/rules/nin.go @@ -0,0 +1,25 @@ +package rules + +type nin struct { + Rule +} + +func (r nin) Match(valA interface{}, valB interface{}) (bool, error) { + + result, err := In.Match(valA, valB) + + if err != nil { + return false, err + } + return !result, nil +} + +func (r nin) GetName() string { + return r.Rule.Name +} + +var Nin = nin{ + Rule{ + "$nin", + }, +} diff --git a/query/rules/nor.go b/query/rules/nor.go new file mode 100644 index 0000000..028fd24 --- /dev/null +++ b/query/rules/nor.go @@ -0,0 +1,29 @@ +package rules + +type nor struct { + test Testfunc + Rule +} + +func (r nor) Match(valA interface{}, valB interface{}) (bool, error) { + + result, err := Or(r.test).Match(valA, valB) + + if err != nil { + return false, err + } + return !result, nil +} + +func (r nor) GetName() string { + return r.Rule.Name +} + +var Nor = func(testfunc Testfunc) nor { + return nor{ + testfunc, + Rule{ + "$nor", + }, + } +} diff --git a/query/rules/not.go b/query/rules/not.go new file mode 100644 index 0000000..0b20e39 --- /dev/null +++ b/query/rules/not.go @@ -0,0 +1,28 @@ +package rules + +type not struct { + test Testfunc + Rule +} + +func (r not) Match(valA interface{}, valB interface{}) (bool, error) { + + result, err := r.test(valA, valB) + + if err != nil { + return false, err + } + return !result, nil +} +func (r not) GetName() string { + return r.Rule.Name +} + +var Not = func(testfunc Testfunc) not { + return not{ + testfunc, + Rule{ + "$not", + }, + } +} diff --git a/query/rules/or.go b/query/rules/or.go new file mode 100644 index 0000000..d4b79ea --- /dev/null +++ b/query/rules/or.go @@ -0,0 +1,60 @@ +package rules + +import ( + "reflect" + "errors" +) + +type or struct { + test Testfunc + Rule +} + +func (r or) Match(valA interface{}, valB interface{}) (bool, error) { + + if valA == nil { + return false, nil + } + + bkind := reflect.TypeOf(valB).Kind() + + if bkind != reflect.Array && bkind != reflect.Slice { + return false, errors.New("And value must be an array") + } + + reflectOfB := reflect.ValueOf(valB) + + parsedValB := make([]interface{}, reflectOfB.Len()) + + for i := 0; i < reflectOfB.Len(); i++ { + parsedValB[i] = reflectOfB.Index(i).Interface() + } + + var last bool = false + + for _, v := range parsedValB { + result, err := r.test(valA, v) + + if err != nil { + return false, err + break + } + + last = last || result + } + + return last, nil +} + +func (r or) GetName() string { + return r.Rule.Name +} + +var Or = func(testfunc Testfunc) or { + return or{ + testfunc, + Rule{ + "$or", + }, + } +} diff --git a/query/rules/regex.go b/query/rules/regex.go new file mode 100644 index 0000000..c27e618 --- /dev/null +++ b/query/rules/regex.go @@ -0,0 +1,37 @@ +package rules + +import ( + "errors" + "regexp" +) + +type regex struct { + Rule +} + +func (r regex) Match(valA interface{}, valB interface{}) (bool, error) { + parsedValA, ok := valA.(string) + + if !ok { + return false, errors.New("Regex can only be matched is parameter field is string") + } + + regex, ok := valB.(string) + + if !ok { + return false, errors.New("Regex value must be string") + } + + return regexp.MatchString(regex, parsedValA) + +} + +func (r regex) GetName() string { + return r.Rule.Name +} + +var Regex = regex{ + Rule{ + "$regex", + }, +} diff --git a/query/rules/size.go b/query/rules/size.go new file mode 100644 index 0000000..3d33c0b --- /dev/null +++ b/query/rules/size.go @@ -0,0 +1,38 @@ +package rules + +import ( + "reflect" + "errors" +) + +type size struct { + Rule +} + +func (r size) Match(valA interface{}, valB interface{}) (bool, error) { + + parsedValB, ok := valB.(float64) + + if !ok { + return false, errors.New("Size value must be an integer") + } + + kind := reflect.TypeOf(valA).Kind() + size := 0 + + if kind == reflect.Array || kind == reflect.Slice { + size = reflect.ValueOf(valA).Len() + } + + return size == int(parsedValB), nil +} + +func (r size) GetName() string { + return r.Rule.Name +} + +var Size = size{ + Rule{ + "$size", + }, +} diff --git a/query/rules/type.go b/query/rules/type.go new file mode 100644 index 0000000..a6fcc84 --- /dev/null +++ b/query/rules/type.go @@ -0,0 +1,53 @@ +package rules + +import ( + "reflect" +) + +type _type struct { + Rule +} + +// Valid values "null", "boolean", "number", "string", "array", "object" + +func (r _type) Match(valA interface{}, valB interface{}) (bool, error) { + + typeOfValA := "null" + kind := reflect.TypeOf(valA).Kind() + + if valA != nil { + switch kind { + case reflect.Array: + case reflect.Slice: + typeOfValA = "array" + break + case reflect.Map: + typeOfValA = "object" + break + case reflect.String: + typeOfValA = "string" + break + case reflect.Bool: + typeOfValA = "boolean" + break + case reflect.Float32: + case reflect.Float64: + case reflect.Int: + typeOfValA = "number" + break + + } + } + + return typeOfValA == valB, nil +} + +func (r _type) GetName() string { + return r.Rule.Name +} + +var Type = _type{ + Rule{ + "$type", + }, +} diff --git a/query/test/all_test.go b/query/test/all_test.go new file mode 100644 index 0000000..d9af580 --- /dev/null +++ b/query/test/all_test.go @@ -0,0 +1,54 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestAll(t *testing.T) { + + t.Run("should contain all elements", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "previousOwners": map[string]interface{}{ + "$all": []string{ + "alice", + "donald", + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) + + t.Run("should contain all elements", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "previousOwners": map[string]interface{}{ + "$all": []string{ + "alice", + "donald", + "tom", + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("Query should have returned 0 results") + } + }) +} diff --git a/query/test/and_test.go b/query/test/and_test.go new file mode 100644 index 0000000..ca0fee5 --- /dev/null +++ b/query/test/and_test.go @@ -0,0 +1,63 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestAnd(t *testing.T) { + + t.Run("Element should be returned when owner bob & size 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "$and": []interface{}{ + map[string]interface{}{ + "owner": "bob", + }, + map[string]interface{}{ + "size": 3, + }, + }, + + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) + + t.Run("Element should be returned when not equal owner bob & size 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "$and": []interface{}{ + map[string]interface{}{ + "owner": map[string]interface{}{ + "$ne": "bob", + }, + }, + map[string]interface{}{ + "size": map[string]interface{}{ + "$ne": 3, + }, + }, + }, + + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("Query should have returned 2 results") + } + }) +} diff --git a/query/test/elematch_test.go b/query/test/elematch_test.go new file mode 100644 index 0000000..2f77529 --- /dev/null +++ b/query/test/elematch_test.go @@ -0,0 +1,76 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestElematch(t *testing.T) { + + t.Run("Array should contains element with organizationId", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "verification": map[string]interface{}{ + "$elemMatch": map[string]interface{}{ + "organizationId": "marbles ID inc.", + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) + + t.Run("Array should contains element with score greater than 6", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "verification": map[string]interface{}{ + "$elemMatch": map[string]interface{}{ + "score": map[string]interface{}{ + "$gt": 6, + }, + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) + + t.Run("Array should contains element with score greater than 9", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "verification": map[string]interface{}{ + "$elemMatch": map[string]interface{}{ + "score": map[string]interface{}{ + "$gt": 9, + }, + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("Query should have returned 0 results") + } + }) +} diff --git a/query/test/eq_test.go b/query/test/eq_test.go new file mode 100644 index 0000000..a4435d3 --- /dev/null +++ b/query/test/eq_test.go @@ -0,0 +1,140 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestEq(t *testing.T) { + t.Run("Size should equal 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$eq": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 result") + } + }) + + t.Run("ObjectType should equal marble", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "objectType": map[string]interface{}{ + "$eq": "MARBLE", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) +} + +func TestImplicitEq(t *testing.T) { + t.Run("Size should equal 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": 3, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 result") + } + }) + + t.Run("previousOwners should equal array with alice & donald", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "previousOwners": []string{ + "alice", + "donald", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 result") + } + }) + + t.Run("previousOwners should not equal boolean false", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "previousOwners": false, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("Query should have returned 0 result") + } + }) +} + +func TestEQSubdocument(t *testing.T) { + t.Run("Family name should equal colored implicit", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "family.name": "colored", + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 result") + } + }) + t.Run("Family name should be colored", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "family.name": map[string]interface{}{ + "$eq": "colored", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 result") + } + }) + +} diff --git a/query/test/exists_test.go b/query/test/exists_test.go new file mode 100644 index 0000000..fda42bf --- /dev/null +++ b/query/test/exists_test.go @@ -0,0 +1,66 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestExists(t *testing.T) { + + t.Run("Owner property should exist", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "owner": map[string]interface{}{ + "$exists": true, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) + + t.Run("ownership property should not exist", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "ownership": map[string]interface{}{ + "$exists": false, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) + + t.Run("verification property should exist on some", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "verification": map[string]interface{}{ + "$exists": true, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Errorf("Query should have returned 2 results, returned %d", len(res)) + } + }) +} diff --git a/query/test/gt_test.go b/query/test/gt_test.go new file mode 100644 index 0000000..65969a8 --- /dev/null +++ b/query/test/gt_test.go @@ -0,0 +1,47 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestGt(t *testing.T) { + + t.Run("Size should be greater than 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$gt": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("TestGtNotEq should have returned 1 result") + } + }) + + t.Run("Size should be greater than 0", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$gt": 0, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("TestGtMultiple should have returned 3 result") + } + }) +} diff --git a/query/test/gte_test.go b/query/test/gte_test.go new file mode 100644 index 0000000..4001823 --- /dev/null +++ b/query/test/gte_test.go @@ -0,0 +1,47 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestGte(t *testing.T) { + + t.Run("Size should be greater than or equal 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$gte": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("should have returned 2 result") + } + }) + + t.Run("Size should be greater than or equal 1", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$gte": 1, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("should have returned 3 result") + } + }) +} diff --git a/query/test/in_test.go b/query/test/in_test.go new file mode 100644 index 0000000..4081186 --- /dev/null +++ b/query/test/in_test.go @@ -0,0 +1,51 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestIn(t *testing.T) { + + t.Run("size should equal one element in array", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$in": []int{ + 1, 3, + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("Query should have returned 2 results") + } + }) + + t.Run("should contain all elements", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "owner": map[string]interface{}{ + "$in": []string{ + "alice", "bob", "arnold", + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) +} diff --git a/query/test/lt_test.go b/query/test/lt_test.go new file mode 100644 index 0000000..13ca91b --- /dev/null +++ b/query/test/lt_test.go @@ -0,0 +1,47 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestLt(t *testing.T) { + + t.Run("Size should be lower than 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$lt": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("should have returned 1 result") + } + }) + + t.Run("Size should be greater than 0", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$lt": 0, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("should have returned 0 result") + } + }) +} diff --git a/query/test/lte_test.go b/query/test/lte_test.go new file mode 100644 index 0000000..fa4f2d3 --- /dev/null +++ b/query/test/lte_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestLte(t *testing.T) { + + t.Run("Size should be lower than 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$lte": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("should have returned 2 result") + } + }) +} diff --git a/query/test/main_test.go b/query/test/main_test.go new file mode 100644 index 0000000..fdaf00a --- /dev/null +++ b/query/test/main_test.go @@ -0,0 +1,71 @@ +package test + +var TestData = map[string]interface{}{ + "MARBLE1": map[string]interface{}{ + "objectType": "MARBLE", + "owner": "bob", + "size": 3, + "previousOwners": []string{ + "alice", + "donald", + }, + "family": map[string]interface{}{ + "name": "colored", + "origin": "spain", + }, + }, + "MARBLE2": map[string]interface{}{ + "objectType": "MARBLE", + "owner": "alice", + "size": 1, + "previousOwners": []string{ + "donald", + }, + "family": map[string]interface{}{ + "name": "white", + "origin": "france", + }, + "verification": []interface{}{ + map[string]interface{}{ + "organizationId": "marbles inspectors inc.", + "checkedAt": "2017-12-18T12:11:34.171Z", + "score": 5, + }, + }, + }, + "MARBLE3": map[string]interface{}{ + "objectType": "MARBLE", + "owner": "arnold", + "size": 5, + "previousOwners": []string{ + "alice", + }, + "family": map[string]interface{}{ + "name": "striped", + "origin": "america", + }, + "verification": []interface{}{ + map[string]interface{}{ + "organizationId": "marbles ID inc.", + "checkedAt": "2017-12-18T12:11:34.171Z", + "score": 3, + }, + map[string]interface{}{ + "organizationId": "marbles ID inc.", + "checkedAt": "2016-12-18T12:11:34.171Z", + "score": 7, + }, + }, + }, +} + +// TODO test +/* +res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "objectType": map[string]interface{}{ + "ne": "MARBLE", + }, + }, + }) + */ diff --git a/query/test/mod_test.go b/query/test/mod_test.go new file mode 100644 index 0000000..2d5eadd --- /dev/null +++ b/query/test/mod_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestMod(t *testing.T) { + + t.Run("Size mod 3 should remain 1", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$mod": []int{3, 1}, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) +} diff --git a/query/test/ne_test.go b/query/test/ne_test.go new file mode 100644 index 0000000..ee53f1a --- /dev/null +++ b/query/test/ne_test.go @@ -0,0 +1,45 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestNe(t *testing.T) { + + t.Run("Size should equal 3", func(t *testing.T) { + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$ne": 3, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("TestNe should have returned 2 result") + } + }) + + t.Run("ObjectType should equal marble", func(t *testing.T) { + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "objectType": map[string]interface{}{ + "$ne": "MARBLE", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("TestNeMultiple should have returned 0 results") + } + }) +} \ No newline at end of file diff --git a/query/test/or_test.go b/query/test/or_test.go new file mode 100644 index 0000000..dfaaa33 --- /dev/null +++ b/query/test/or_test.go @@ -0,0 +1,63 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestOr(t *testing.T) { + + t.Run("Element should be returned when owner bob or size 5", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "$or": []interface{}{ + map[string]interface{}{ + "owner": "bob", + }, + map[string]interface{}{ + "size": 5, + }, + }, + + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 2 { + t.Error("Query should have returned 2 results") + } + }) + + t.Run("Element should be returned when not equal owner bob & size 3", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "$or": []interface{}{ + map[string]interface{}{ + "owner": map[string]interface{}{ + "$ne": "alicia", + }, + }, + map[string]interface{}{ + "size": map[string]interface{}{ + "$ne": 20, + }, + }, + }, + + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) +} diff --git a/query/test/size_test.go b/query/test/size_test.go new file mode 100644 index 0000000..adc3b44 --- /dev/null +++ b/query/test/size_test.go @@ -0,0 +1,28 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestSize(t *testing.T) { + + t.Run("PreviousOwners size should equal 2", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "previousOwners": map[string]interface{}{ + "$size": 2, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 1 { + t.Error("Query should have returned 1 results") + } + }) +} diff --git a/query/test/type_test.go b/query/test/type_test.go new file mode 100644 index 0000000..3838c44 --- /dev/null +++ b/query/test/type_test.go @@ -0,0 +1,87 @@ +package test + +import ( + "test/couchdbMockQuery/query" + "testing" +) + +func TestType(t *testing.T) { + + t.Run("Size type should equal int", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "size": map[string]interface{}{ + "$type": "number", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) + + t.Run("Owner type should equal string", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "owner": map[string]interface{}{ + "$type": "string", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) + + t.Run("Owner type should not equal int", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "owner": map[string]interface{}{ + "$type": "int", + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 0 { + t.Error("Query should have returned 0 results") + } + }) + + t.Run("Test recursive negation", func(t *testing.T) { + + res, err := query.ParseCouchDBQuery(TestData, map[string]interface{}{ + "selector": map[string]interface{}{ + "owner": map[string]interface{}{ + "$ne": map[string]interface{}{ + "$type": "int", + }, + }, + }, + }) + + if err != nil { + t.Error(err) + } + + if len(res) != 3 { + t.Error("Query should have returned 3 results") + } + }) +}