tile38/controller/token.go
Josh Baker 6c52f3f3f1 added command filter for geofences
It's now possible to mask the fence notifications based on the
command. For example, if we only want "set" and "del" commands.

NEARBY fleet FENCE COMMANDS set,del POINT 33 -115 10000

Suggested by @amorskoy, closes #99
2016-12-15 10:00:08 -07:00

476 lines
11 KiB
Go

package controller
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"github.com/tidwall/resp"
)
const defaultSearchOutput = outputObjects
var errInvalidNumberOfArguments = errors.New("invalid number of arguments")
var errKeyNotFound = errors.New("key not found")
var errIDNotFound = errors.New("id not found")
var errIDAlreadyExists = errors.New("id already exists")
var errPathNotFound = errors.New("path not found")
func errInvalidArgument(arg string) error {
return fmt.Errorf("invalid argument '%s'", arg)
}
func errDuplicateArgument(arg string) error {
return fmt.Errorf("duplicate argument '%s'", arg)
}
func token(line string) (newLine, token string) {
for i := 0; i < len(line); i++ {
if line[i] == ' ' {
return line[i+1:], line[:i]
}
}
return "", line
}
func tokenval(vs []resp.Value) (nvs []resp.Value, token string, ok bool) {
if len(vs) > 0 {
token = vs[0].String()
nvs = vs[1:]
ok = true
}
return
}
func tokenvalbytes(vs []resp.Value) (nvs []resp.Value, token []byte, ok bool) {
if len(vs) > 0 {
token = vs[0].Bytes()
nvs = vs[1:]
ok = true
}
return
}
func tokenlc(line string) (newLine, token string) {
for i := 0; i < len(line); i++ {
ch := line[i]
if ch == ' ' {
return line[i+1:], line[:i]
}
if ch >= 'A' && ch <= 'Z' {
lc := make([]byte, 0, 16)
if i > 0 {
lc = append(lc, []byte(line[:i])...)
}
lc = append(lc, ch+32)
i++
for ; i < len(line); i++ {
ch = line[i]
if ch == ' ' {
return line[i+1:], string(lc)
}
if ch >= 'A' && ch <= 'Z' {
lc = append(lc, ch+32)
} else {
lc = append(lc, ch)
}
}
return "", string(lc)
}
}
return "", line
}
func lcb(s1 []byte, s2 string) bool {
if len(s1) != len(s2) {
return false
}
for i := 0; i < len(s1); i++ {
ch := s1[i]
if ch >= 'A' && ch <= 'Z' {
if ch+32 != s2[i] {
return false
}
} else if ch != s2[i] {
return false
}
}
return true
}
func lc(s1, s2 string) bool {
if len(s1) != len(s2) {
return false
}
for i := 0; i < len(s1); i++ {
ch := s1[i]
if ch >= 'A' && ch <= 'Z' {
if ch+32 != s2[i] {
return false
}
} else if ch != s2[i] {
return false
}
}
return true
}
type whereT struct {
field string
minx bool
min float64
maxx bool
max float64
}
func (where whereT) match(value float64) bool {
if !where.minx {
if value < where.min {
return false
}
} else {
if value <= where.min {
return false
}
}
if !where.maxx {
if value > where.max {
return false
}
} else {
if value >= where.max {
return false
}
}
return true
}
func zMinMaxFromWheres(wheres []whereT) (minZ, maxZ float64) {
for _, w := range wheres {
if w.field == "z" {
minZ = w.min
maxZ = w.max
return
}
}
minZ = math.Inf(-1)
maxZ = math.Inf(+1)
return
}
type searchScanBaseTokens struct {
key string
cursor uint64
output outputT
precision uint64
lineout string
fence bool
detect map[string]bool
accept map[string]bool
glob string
wheres []whereT
nofields bool
limit uint64
sparse uint8
desc bool
}
func parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vsout []resp.Value, t searchScanBaseTokens, err error) {
var ok bool
if vs, t.key, ok = tokenval(vs); !ok || t.key == "" {
err = errInvalidNumberOfArguments
return
}
var slimit string
var ssparse string
var scursor string
var asc bool
for {
nvs, wtok, ok := tokenval(vs)
if ok && len(wtok) > 0 {
if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "cursor" {
vs = nvs
if scursor != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
if vs, scursor, ok = tokenval(vs); !ok || scursor == "" {
err = errInvalidNumberOfArguments
return
}
continue
} else if (wtok[0] == 'W' || wtok[0] == 'w') && strings.ToLower(wtok) == "where" {
vs = nvs
var field, smin, smax string
if vs, field, ok = tokenval(vs); !ok || field == "" {
err = errInvalidNumberOfArguments
return
}
if vs, smin, ok = tokenval(vs); !ok || smin == "" {
err = errInvalidNumberOfArguments
return
}
if vs, smax, ok = tokenval(vs); !ok || smax == "" {
err = errInvalidNumberOfArguments
return
}
var minx, maxx bool
var min, max float64
if strings.ToLower(smin) == "-inf" {
min = math.Inf(-1)
} else {
if strings.HasPrefix(smin, "(") {
minx = true
smin = smin[1:]
}
min, err = strconv.ParseFloat(smin, 64)
if err != nil {
err = errInvalidArgument(smin)
return
}
}
if strings.ToLower(smax) == "+inf" {
max = math.Inf(+1)
} else {
if strings.HasPrefix(smax, "(") {
maxx = true
smax = smax[1:]
}
max, err = strconv.ParseFloat(smax, 64)
if err != nil {
err = errInvalidArgument(smax)
return
}
}
t.wheres = append(t.wheres, whereT{field, minx, min, maxx, max})
continue
} else if (wtok[0] == 'N' || wtok[0] == 'n') && strings.ToLower(wtok) == "nofields" {
vs = nvs
if t.nofields {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.nofields = true
continue
} else if (wtok[0] == 'L' || wtok[0] == 'l') && strings.ToLower(wtok) == "limit" {
vs = nvs
if slimit != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
if vs, slimit, ok = tokenval(vs); !ok || slimit == "" {
err = errInvalidNumberOfArguments
return
}
continue
} else if (wtok[0] == 'S' || wtok[0] == 's') && strings.ToLower(wtok) == "sparse" {
vs = nvs
if ssparse != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
if vs, ssparse, ok = tokenval(vs); !ok || ssparse == "" {
err = errInvalidNumberOfArguments
return
}
continue
} else if (wtok[0] == 'F' || wtok[0] == 'f') && strings.ToLower(wtok) == "fence" {
vs = nvs
if t.fence {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.fence = true
continue
} else if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "commands" {
vs = nvs
if t.accept != nil {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.accept = make(map[string]bool)
var peek string
if vs, peek, ok = tokenval(vs); !ok || peek == "" {
err = errInvalidNumberOfArguments
return
}
for _, s := range strings.Split(peek, ",") {
part := strings.TrimSpace(strings.ToLower(s))
if t.accept[part] {
err = errDuplicateArgument(s)
return
}
t.accept[part] = true
}
if len(t.accept) == 0 {
t.accept = nil
}
continue
} else if (wtok[0] == 'D' || wtok[0] == 'd') && strings.ToLower(wtok) == "detect" {
vs = nvs
if t.detect != nil {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.detect = make(map[string]bool)
var peek string
if vs, peek, ok = tokenval(vs); !ok || peek == "" {
err = errInvalidNumberOfArguments
return
}
for _, s := range strings.Split(peek, ",") {
part := strings.TrimSpace(strings.ToLower(s))
switch part {
default:
err = errInvalidArgument(peek)
return
case "inside", "outside", "enter", "exit", "cross":
}
if t.detect[part] {
err = errDuplicateArgument(s)
return
}
t.detect[part] = true
}
if len(t.detect) == 0 {
t.detect = map[string]bool{
"inside": true,
"outside": true,
"enter": true,
"exit": true,
"cross": true,
}
}
continue
} else if (wtok[0] == 'D' || wtok[0] == 'd') && strings.ToLower(wtok) == "desc" {
vs = nvs
if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
t.desc = true
continue
} else if (wtok[0] == 'A' || wtok[0] == 'a') && strings.ToLower(wtok) == "asc" {
vs = nvs
if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
asc = true
continue
} else if (wtok[0] == 'M' || wtok[0] == 'm') && strings.ToLower(wtok) == "match" {
vs = nvs
if t.glob != "" {
err = errDuplicateArgument(strings.ToUpper(wtok))
return
}
if vs, t.glob, ok = tokenval(vs); !ok || t.glob == "" {
err = errInvalidNumberOfArguments
return
}
continue
}
}
break
}
// check to make sure that there aren't any conflicts
if cmd == "scan" || cmd == "search" {
if ssparse != "" {
err = errors.New("SPARSE is not allowed for " + strings.ToUpper(cmd))
return
}
if t.fence {
err = errors.New("FENCE is not allowed for " + strings.ToUpper(cmd))
return
}
} else {
if t.desc {
err = errors.New("DESC is not allowed for " + strings.ToUpper(cmd))
return
}
if asc {
err = errors.New("ASC is not allowed for " + strings.ToUpper(cmd))
return
}
}
if ssparse != "" && slimit != "" {
err = errors.New("LIMIT is not allowed when SPARSE is specified")
return
}
if scursor != "" && ssparse != "" {
err = errors.New("CURSOR is not allowed when SPARSE is specified")
return
}
if scursor != "" && t.fence {
err = errors.New("CURSOR is not allowed when FENCE is specified")
return
}
if t.detect != nil && !t.fence {
err = errors.New("DETECT is not allowed when FENCE is not specified")
return
}
t.output = defaultSearchOutput
var nvs []resp.Value
var sprecision string
var which string
if nvs, which, ok = tokenval(vs); ok && which != "" {
updline := true
switch strings.ToLower(which) {
default:
if cmd == "scan" {
err = errInvalidArgument(which)
return
}
updline = false
case "count":
t.output = outputCount
case "objects":
t.output = outputObjects
case "points":
t.output = outputPoints
case "hashes":
t.output = outputHashes
if nvs, sprecision, ok = tokenval(nvs); !ok || sprecision == "" {
err = errInvalidNumberOfArguments
return
}
case "bounds":
t.output = outputBounds
case "ids":
t.output = outputIDs
}
if updline {
vs = nvs
}
}
if scursor != "" {
if t.cursor, err = strconv.ParseUint(scursor, 10, 64); err != nil {
err = errInvalidArgument(scursor)
return
}
}
if sprecision != "" {
if t.precision, err = strconv.ParseUint(sprecision, 10, 64); err != nil || t.precision == 0 || t.precision > 64 {
err = errInvalidArgument(sprecision)
return
}
}
if slimit != "" {
if t.limit, err = strconv.ParseUint(slimit, 10, 64); err != nil || t.limit == 0 {
err = errInvalidArgument(slimit)
return
}
}
if ssparse != "" {
var sparse uint64
if sparse, err = strconv.ParseUint(ssparse, 10, 8); err != nil || sparse == 0 || sparse > 8 {
err = errInvalidArgument(ssparse)
return
}
t.sparse = uint8(sparse)
t.limit = math.MaxUint64
}
vsout = vs
return
}