diff --git a/internal/server/expression.go b/internal/server/expression.go new file mode 100644 index 00000000..624a236f --- /dev/null +++ b/internal/server/expression.go @@ -0,0 +1,186 @@ +package server + +import ( + "strings" + + "github.com/tidwall/geojson" +) + +type BinaryOp byte + +const ( + NOOP BinaryOp = iota + AND + OR + tokenAND = "and" + tokenOR = "or" + tokenNOT = "not" + tokenLParen = "(" + tokenRParen = ")" +) + +// areaExpression is (maybe negated) either an spatial object or operator + children (other expressions). +type areaExpression struct { + negate bool + obj geojson.Object + op BinaryOp + children children +} + +type children []*areaExpression + +// String representation, helpful in logging. +func (e *areaExpression) String() (res string) { + if e.obj != nil { + res = e.obj.String() + } else { + var chStrings []string + for _, c := range e.children { + chStrings = append(chStrings, c.String()) + } + switch e.op { + case NOOP: + res = "empty operator" + case AND: + res = "(" + strings.Join(chStrings, " "+tokenAND+" ") + ")" + case OR: + res = "(" + strings.Join(chStrings, " "+tokenOR+" ") + ")" + default: + res = "unknown operator" + } + } + if e.negate { + res = tokenNOT + " " + res + } + return +} + +// Return boolean value modulo negate field of the expression. +func (e *areaExpression) maybeNegate(val bool) bool { + if e.negate { + return !val + } + return val +} + +// Methods for testing an areaExpression against the spatial object. +func (e *areaExpression) testObject( + o geojson.Object, + objObjTest func(o1, o2 geojson.Object) bool, + exprObjTest func(ae *areaExpression, ob geojson.Object) bool, +) bool { + if e.obj != nil { + return objObjTest(e.obj, o) + } + switch e.op { + case AND: + for _, c := range e.children { + if !exprObjTest(c, o) { + return false + } + } + return true + case OR: + for _, c := range e.children { + if exprObjTest(c, o) { + return true + } + } + return false + } + return false +} + +func (e *areaExpression) rawIntersects(o geojson.Object) bool { + return e.testObject(o, geojson.Object.Intersects, (*areaExpression).Intersects) +} + +func (e *areaExpression) rawContains(o geojson.Object) bool { + return e.testObject(o, geojson.Object.Contains, (*areaExpression).Contains) +} + +func (e *areaExpression) rawWithin(o geojson.Object) bool { + return e.testObject(o, geojson.Object.Within, (*areaExpression).Within) +} + +func (e *areaExpression) Intersects(o geojson.Object) bool { + return e.maybeNegate(e.rawIntersects(o)) +} + +func (e *areaExpression) Contains(o geojson.Object) bool { + return e.maybeNegate(e.rawContains(o)) +} + +func (e *areaExpression) Within(o geojson.Object) bool { + return e.maybeNegate(e.rawWithin(o)) +} + +// Methods for testing an areaExpression against another areaExpression. +func (e *areaExpression) testExpression( + other *areaExpression, + exprObjTest func(ae *areaExpression, ob geojson.Object) bool, + rawExprExprTest func(ae1, ae2 *areaExpression) bool, + exprExprTest func(ae1, ae2 *areaExpression) bool, +) bool { + if other.negate { + oppositeExp := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children} + nonNegateOther := &areaExpression{obj: other.obj, op: other.op, children: other.children} + return exprExprTest(oppositeExp, nonNegateOther) + } + if other.obj != nil { + return exprObjTest(e, other.obj) + } + switch other.op { + case AND: + for _, c := range other.children { + if !rawExprExprTest(e, c) { + return false + } + } + return true + case OR: + for _, c := range other.children { + if rawExprExprTest(e, c) { + return true + } + } + return false + } + return false +} + +func (e *areaExpression) rawIntersectsExpr(other *areaExpression) bool { + return e.testExpression( + other, + (*areaExpression).rawIntersects, + (*areaExpression).rawIntersectsExpr, + (*areaExpression).IntersectsExpr) +} + +func (e *areaExpression) rawWithinExpr(other *areaExpression) bool { + return e.testExpression( + other, + (*areaExpression).rawWithin, + (*areaExpression).rawWithinExpr, + (*areaExpression).WithinExpr) +} + +func (e *areaExpression) rawContainsExpr(other *areaExpression) bool { + return e.testExpression( + other, + (*areaExpression).rawContains, + (*areaExpression).rawContainsExpr, + (*areaExpression).ContainsExpr) +} + +func (e *areaExpression) IntersectsExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawIntersectsExpr(other)) +} + +func (e *areaExpression) WithinExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawWithinExpr(other)) +} + +func (e *areaExpression) ContainsExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawContainsExpr(other)) +} diff --git a/internal/server/test.go b/internal/server/test.go index 267f3c27..e276367c 100644 --- a/internal/server/test.go +++ b/internal/server/test.go @@ -231,8 +231,9 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { var ok bool var test string - var obj1, obj2, clipped geojson.Object - if vs, obj1, err = s.parseArea(vs, false); err != nil { + var clipped geojson.Object + var area1, area2 *areaExpression + if vs, area1, err = s.parseAreaExpression(vs, false); err != nil { return } if vs, test, ok = tokenval(vs); !ok || test == "" { @@ -259,7 +260,11 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { doClip = true } } - if vs, obj2, err = s.parseArea(vs, doClip); err != nil { + if vs, area2, err = s.parseAreaExpression(vs, doClip); err != nil { + return + } + if doClip && (area1.obj == nil || area2.obj == nil) { + err = errInvalidArgument("clip") return } if len(vs) != 0 { @@ -268,14 +273,14 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { var result int if lTest == "within" { - if obj1.Within(obj2) { + if area1.WithinExpr(area2) { result = 1 } } else if lTest == "intersects" { - if obj1.Intersects(obj2) { + if area1.IntersectsExpr(area2) { result = 1 if doClip { - clipped = clip.Clip(obj1, obj2) + clipped = clip.Clip(area1.obj, area2.obj) } } } diff --git a/internal/server/token.go b/internal/server/token.go index 71a81044..8d1afa1d 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -688,3 +688,140 @@ func (c *Server) parseSearchScanBaseTokens( tout = t return } + +type parentStack []*areaExpression + +func (ps *parentStack) isEmpty() bool { + return len(*ps) == 0 +} + +func (ps *parentStack) push(e *areaExpression) { + *ps = append(*ps, e) +} + +func (ps *parentStack) pop() (e *areaExpression, empty bool) { + n := len(*ps) + if n == 0 { + return nil, true + } + x := (*ps)[n-1] + *ps = (*ps)[:n-1] + return x, false +} + +func (s *Server) parseAreaExpression(vsin []string, doClip bool) (vsout []string, ae *areaExpression, err error) { + ps := &parentStack{} + vsout = vsin[:] + var negate, needObj bool +loop: + for { + nvs, wtok, ok := tokenval(vsout) + if !ok || len(wtok) == 0 { + break + } + switch strings.ToLower(wtok) { + case tokenLParen: + newExpr := &areaExpression{negate: negate, op: NOOP} + negate = false + needObj = false + if ae != nil { + ae.children = append(ae.children, newExpr) + } + ae = newExpr + ps.push(ae) + vsout = nvs + case tokenRParen: + if needObj { + err = errInvalidArgument(tokenRParen) + return + } + if parent, empty := ps.pop(); empty { + err = errInvalidArgument(tokenRParen) + return + } else { + ae = parent + } + vsout = nvs + case tokenNOT: + negate = !negate + needObj = true + vsout = nvs + case tokenAND: + if needObj { + err = errInvalidArgument(tokenAND) + return + } + needObj = true + if ae == nil { + err = errInvalidArgument(tokenAND) + return + } else if ae.obj == nil { + switch ae.op { + case OR: + numChildren := len(ae.children) + if numChildren < 2 { + err = errInvalidNumberOfArguments + return + } else { + ae.children = append( + ae.children[:numChildren-1], + &areaExpression{ + op: AND, + children: []*areaExpression{ae.children[numChildren-1]}}) + } + case NOOP: + ae.op = AND + } + } else { + ae = &areaExpression{op: AND, children: []*areaExpression{ae}} + } + vsout = nvs + case tokenOR: + if needObj { + err = errInvalidArgument(tokenOR) + return + } + needObj = true + if ae == nil { + err = errInvalidArgument(tokenOR) + return + } else if ae.obj == nil { + switch ae.op { + case AND: + if len(ae.children) < 2 { + err = errInvalidNumberOfArguments + return + } else { + ae = &areaExpression{op: OR, children: []*areaExpression{ae}} + } + case NOOP: + ae.op = OR + } + } else { + ae = &areaExpression{op: OR, children: []*areaExpression{ae}} + } + vsout = nvs + case "point", "circle", "object", "bounds", "hash", "quadkey", "tile", "get": + if parsedVs, parsedObj, areaErr := s.parseArea(vsout, doClip); areaErr != nil { + err = areaErr + return + } else { + newExpr := &areaExpression{negate: negate, obj: parsedObj, op: NOOP} + negate = false + needObj = false + if ae == nil { + ae = newExpr + } else { + ae.children = append(ae.children, newExpr) + } + vsout = parsedVs + } + default: + break loop + } + } + if !ps.isEmpty() || needObj || ae == nil || (ae.obj == nil && len(ae.children) == 0) { + err = errInvalidNumberOfArguments + } + return +} diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index d6d65231..51c55da2 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -8,6 +8,8 @@ func subTestTestCmd(t *testing.T, mc *mockServer) { runStep(t, mc, "WITHIN", testcmd_WITHIN_test) runStep(t, mc, "INTERSECTS", testcmd_INTERSECTS_test) runStep(t, mc, "INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test) + runStep(t, mc, "ExpressionErrors", testcmd_expressionErrors_test) + runStep(t, mc, "Expressions", testcmd_expression_test) } func testcmd_WITHIN_test(mc *mockServer) error { @@ -115,3 +117,97 @@ func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error { {"TEST", "OBJECT", poly101, "INTERSECTS", "CLIP", "BOUNDS", 37.73315644825698, -122.44054287672043, 37.73349585185455, -122.44008690118788}, {"0"}, }) } + +func testcmd_expressionErrors_test(mc *mockServer) error { + return mc.DoBatch([][]interface{}{ + {"SET", "mykey", "foo", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "bar", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "baz", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "(", "GET", "mykey", "bar"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", ")"}, { + "ERR invalid argument ')'"}, + + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "OR", "GET", "mykey", "bar"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "AND", "GET", "mykey", "bar"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "OR", "GET", "mykey", "baz"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "OR", "GET", "mykey", "baz"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + }) +} + +func testcmd_expression_test(mc *mockServer) error { + poly := `{ + "type": "Polygon", + "coordinates": [ + [ + [-122.44126439094543,37.732906137107], + [-122.43980526924135,37.732906137107], + [-122.43980526924135,37.73421283683962], + [-122.44126439094543,37.73421283683962], + [-122.44126439094543,37.732906137107] + ] + ] + }` + poly8 := `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]],[[-122.44060993194579,37.73345766902749],[-122.44044363498686,37.73345766902749],[-122.44044363498686,37.73355524732416],[-122.44060993194579,37.73355524732416],[-122.44060993194579,37.73345766902749]],[[-122.44060724973677,37.7336888869566],[-122.4402102828026,37.7336888869566],[-122.4402102828026,37.7339752567853],[-122.44060724973677,37.7339752567853],[-122.44060724973677,37.7336888869566]]]}` + poly9 := `{"type": "Polygon","coordinates": [[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}` + + return mc.DoBatch([][]interface{}{ + {"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, + + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "OBJECT", poly}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "NOT", "OBJECT", poly}, {"0"}, + + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly}, {"1"}, + + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "NOT", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", + "OR", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly, + "OR", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR", + "(", "OBJECT", poly8, "AND", "OBJECT", poly, ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", + "(", "GET", "mykey", "line3", "OR", "OBJECT", poly8, ")", "AND", "OBJECT", poly}, {"1"}, + + {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "line3"}, {"0"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3"}, {"1"}, + }) +}