Add query package with first set of conditions and tests

This commit is contained in:
Daniel 2018-08-29 20:02:02 +02:00
parent 1bf7e42d8a
commit 6ed50f34fb
16 changed files with 933 additions and 0 deletions

View file

@ -0,0 +1,31 @@
package query
// And combines multiple conditions with a logical _AND_ operator.
func And(conditions ...Condition) Condition {
return &andCond{
conditions: conditions,
}
}
type andCond struct {
conditions []Condition
}
func (c *andCond) complies(f Fetcher) bool {
for _, cond := range c.conditions {
if !cond.complies(f) {
return false
}
}
return true
}
func (c *andCond) check() (err error) {
for _, cond := range c.conditions {
err = cond.check()
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,64 @@
package query
import (
"errors"
"fmt"
"strconv"
)
type boolCondition struct {
key string
operator uint8
value bool
}
func newBoolCondition(key string, operator uint8, value interface{}) *boolCondition {
var parsedValue bool
switch v := value.(type) {
case bool:
parsedValue = v
case string:
var err error
parsedValue, err = strconv.ParseBool(v)
if err != nil {
return &boolCondition{
key: fmt.Sprintf("could not parse \"%s\" to bool: %s", v, err),
operator: errorPresent,
}
}
default:
return &boolCondition{
key: fmt.Sprintf("incompatible value %v for int64", value),
operator: errorPresent,
}
}
return &boolCondition{
key: key,
operator: operator,
value: parsedValue,
}
}
func (c *boolCondition) complies(f Fetcher) bool {
comp, ok := f.GetBool(c.key)
if !ok {
return false
}
switch c.operator {
case Is:
return comp == c.value
default:
return false
}
}
func (c *boolCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,19 @@
package query
type errorCondition struct {
err error
}
func newErrorCondition(err error) *errorCondition {
return &errorCondition{
err: err,
}
}
func (c *errorCondition) complies(f Fetcher) bool {
return false
}
func (c *errorCondition) check() error {
return c.err
}

View file

@ -0,0 +1,28 @@
package query
import (
"errors"
)
type existsCondition struct {
key string
operator uint8
}
func newExistsCondition(key string, operator uint8) *existsCondition {
return &existsCondition{
key: key,
operator: operator,
}
}
func (c *existsCondition) complies(f Fetcher) bool {
return f.Exists(c.key)
}
func (c *existsCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,90 @@
package query
import (
"errors"
"fmt"
"strconv"
)
type floatCondition struct {
key string
operator uint8
value float64
}
func newFloatCondition(key string, operator uint8, value interface{}) *floatCondition {
var parsedValue float64
switch v := value.(type) {
case int:
parsedValue = float64(v)
case int8:
parsedValue = float64(v)
case int16:
parsedValue = float64(v)
case int32:
parsedValue = float64(v)
case int64:
parsedValue = float64(v)
case uint:
parsedValue = float64(v)
case uint8:
parsedValue = float64(v)
case uint32:
parsedValue = float64(v)
case float32:
parsedValue = float64(v)
case float64:
parsedValue = v
case string:
var err error
parsedValue, err = strconv.ParseFloat(v, 64)
if err != nil {
return &floatCondition{
key: fmt.Sprintf("could not parse %s to float64: %s", v, err),
operator: errorPresent,
}
}
default:
return &floatCondition{
key: fmt.Sprintf("incompatible value %v for float64", value),
operator: errorPresent,
}
}
return &floatCondition{
key: key,
operator: operator,
value: parsedValue,
}
}
func (c *floatCondition) complies(f Fetcher) bool {
comp, ok := f.GetFloat(c.key)
if !ok {
return false
}
switch c.operator {
case FloatEquals:
return comp == c.value
case FloatGreaterThan:
return comp > c.value
case FloatGreaterThanOrEqual:
return comp >= c.value
case FloatLessThan:
return comp < c.value
case FloatLessThanOrEqual:
return comp <= c.value
default:
return false
}
}
func (c *floatCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,86 @@
package query
import (
"errors"
"fmt"
"strconv"
)
type intCondition struct {
key string
operator uint8
value int64
}
func newIntCondition(key string, operator uint8, value interface{}) *intCondition {
var parsedValue int64
switch v := value.(type) {
case int:
parsedValue = int64(v)
case int8:
parsedValue = int64(v)
case int16:
parsedValue = int64(v)
case int32:
parsedValue = int64(v)
case int64:
parsedValue = int64(v)
case uint:
parsedValue = int64(v)
case uint8:
parsedValue = int64(v)
case uint32:
parsedValue = int64(v)
case string:
var err error
parsedValue, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return &intCondition{
key: fmt.Sprintf("could not parse %s to int64: %s (hint: use \"sameas\" to compare strings)", v, err),
operator: errorPresent,
}
}
default:
return &intCondition{
key: fmt.Sprintf("incompatible value %v for int64", value),
operator: errorPresent,
}
}
return &intCondition{
key: key,
operator: operator,
value: parsedValue,
}
}
func (c *intCondition) complies(f Fetcher) bool {
comp, ok := f.GetInt(c.key)
if !ok {
return false
}
switch c.operator {
case Equals:
return comp == c.value
case GreaterThan:
return comp > c.value
case GreaterThanOrEqual:
return comp >= c.value
case LessThan:
return comp < c.value
case LessThanOrEqual:
return comp <= c.value
default:
return false
}
}
func (c *intCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,20 @@
package query
// Not negates the supplied condition.
func Not(c Condition) Condition {
return &notCond{
notC: c,
}
}
type notCond struct {
notC Condition
}
func (c *notCond) complies(f Fetcher) bool {
return !c.notC.complies(f)
}
func (c *notCond) check() error {
return c.notC.check()
}

View file

@ -0,0 +1,31 @@
package query
// Or combines multiple conditions with a logical _OR_ operator.
func Or(conditions ...Condition) Condition {
return &orCond{
conditions: conditions,
}
}
type orCond struct {
conditions []Condition
}
func (c *orCond) complies(f Fetcher) bool {
for _, cond := range c.conditions {
if cond.complies(f) {
return true
}
}
return false
}
func (c *orCond) check() (err error) {
for _, cond := range c.conditions {
err = cond.check()
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,57 @@
package query
import (
"errors"
"fmt"
"regexp"
)
type regexCondition struct {
key string
operator uint8
regex *regexp.Regexp
}
func newRegexCondition(key string, operator uint8, value interface{}) *regexCondition {
switch v := value.(type) {
case string:
r, err := regexp.Compile(v)
if err != nil {
return &regexCondition{
key: fmt.Sprintf("could not compile regex \"%s\": %s", v, err),
operator: errorPresent,
}
}
return &regexCondition{
key: key,
operator: operator,
regex: r,
}
default:
return &regexCondition{
key: fmt.Sprintf("incompatible value %v for string", value),
operator: errorPresent,
}
}
}
func (c *regexCondition) complies(f Fetcher) bool {
comp, ok := f.GetString(c.key)
if !ok {
return false
}
switch c.operator {
case Matches:
return c.regex.MatchString(comp)
default:
return false
}
}
func (c *regexCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,56 @@
package query
import (
"errors"
"fmt"
"strings"
)
type stringCondition struct {
key string
operator uint8
value string
}
func newStringCondition(key string, operator uint8, value interface{}) *stringCondition {
switch v := value.(type) {
case string:
return &stringCondition{
key: key,
operator: operator,
value: v,
}
default:
return &stringCondition{
key: fmt.Sprintf("incompatible value %v for string", value),
operator: errorPresent,
}
}
}
func (c *stringCondition) complies(f Fetcher) bool {
comp, ok := f.GetString(c.key)
if !ok {
return false
}
switch c.operator {
case Matches:
return c.value == comp
case Contains:
return strings.Contains(comp, c.value)
case StartsWith:
return strings.HasPrefix(comp, c.value)
case EndsWith:
return strings.HasSuffix(comp, c.value)
default:
return false
}
}
func (c *stringCondition) check() error {
if c.operator == errorPresent {
return errors.New(c.key)
}
return nil
}

View file

@ -0,0 +1,60 @@
package query
import (
"fmt"
"strings"
"github.com/Safing/portbase/utils"
)
type stringSliceCondition struct {
key string
operator uint8
value []string
}
func newStringSliceCondition(key string, operator uint8, value interface{}) *stringSliceCondition {
switch v := value.(type) {
case string:
parsedValue := strings.Split(v, ",")
if len(parsedValue) < 2 {
return &stringSliceCondition{
key: fmt.Sprintf("could not parse \"%s\" to []string", v),
operator: errorPresent,
}
}
return &stringSliceCondition{
key: key,
operator: operator,
value: parsedValue,
}
default:
return &stringSliceCondition{
key: fmt.Sprintf("incompatible value %v for []string", value),
operator: errorPresent,
}
}
}
func (c *stringSliceCondition) complies(f Fetcher) bool {
comp, ok := f.GetString(c.key)
if !ok {
return false
}
switch c.operator {
case In:
return utils.StringInSlice(c.value, comp)
default:
return false
}
}
func (c *stringSliceCondition) check() error {
if c.operator == errorPresent {
return fmt.Errorf("could not parse \"%s\" to []string", c.key)
}
return nil
}

View file

@ -0,0 +1,67 @@
package query
import "fmt"
// Condition is an interface to provide a common api to all condition types.
type Condition interface {
complies(f Fetcher) bool
check() error
// string() string
}
// Operators
const (
Equals uint8 = iota // int
GreaterThan // int
GreaterThanOrEqual // int
LessThan // int
LessThanOrEqual // int
FloatEquals // float
FloatGreaterThan // float
FloatGreaterThanOrEqual // float
FloatLessThan // float
FloatLessThanOrEqual // float
SameAs // string
Contains // string
StartsWith // string
EndsWith // string
In // stringSlice
Matches // regex
Is // bool: accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE
Exists // any
errorPresent uint8 = 255
)
// Where returns a condition to add to a query.
func Where(key string, operator uint8, value interface{}) Condition {
switch operator {
case Equals,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual:
return newIntCondition(key, operator, value)
case FloatEquals,
FloatGreaterThan,
FloatGreaterThanOrEqual,
FloatLessThan,
FloatLessThanOrEqual:
return newFloatCondition(key, operator, value)
case SameAs,
Contains,
StartsWith,
EndsWith:
return newStringCondition(key, operator, value)
case In:
return newStringSliceCondition(key, operator, value)
case Matches:
return newRegexCondition(key, operator, value)
case Is:
return newBoolCondition(key, operator, value)
case Exists:
return newExistsCondition(key, operator)
default:
return newErrorCondition(fmt.Errorf("no operator with ID %d", operator))
}
}

78
database/query/fetcher.go Normal file
View file

@ -0,0 +1,78 @@
package query
import (
"github.com/tidwall/gjson"
)
const (
emptyString = ""
)
// Fetcher provides an interface to supply the query matcher a method to retrieve values from an object.
type Fetcher interface {
GetString(key string) (value string, ok bool)
GetInt(key string) (value int64, ok bool)
GetFloat(key string) (value float64, ok bool)
GetBool(key string) (value bool, ok bool)
Exists(key string) bool
}
// JSONFetcher is a json string with get functions.
type JSONFetcher struct {
json string
}
// NewJSONFetcher adds the Fetcher interface to a JSON string.
func NewJSONFetcher(json string) *JSONFetcher {
return &JSONFetcher{
json: json,
}
}
// GetString returns the string found by the given json key and whether it could be successfully extracted.
func (jf *JSONFetcher) GetString(key string) (value string, ok bool) {
result := gjson.Get(jf.json, key)
if !result.Exists() || result.Type != gjson.String {
return emptyString, false
}
return result.String(), true
}
// GetInt returns the int found by the given json key and whether it could be successfully extracted.
func (jf *JSONFetcher) GetInt(key string) (value int64, ok bool) {
result := gjson.Get(jf.json, key)
if !result.Exists() || result.Type != gjson.Number {
return 0, false
}
return result.Int(), true
}
// GetFloat returns the float found by the given json key and whether it could be successfully extracted.
func (jf *JSONFetcher) GetFloat(key string) (value float64, ok bool) {
result := gjson.Get(jf.json, key)
if !result.Exists() || result.Type != gjson.Number {
return 0, false
}
return result.Float(), true
}
// GetBool returns the bool found by the given json key and whether it could be successfully extracted.
func (jf *JSONFetcher) GetBool(key string) (value bool, ok bool) {
result := gjson.Get(jf.json, key)
switch {
case !result.Exists():
return false, false
case result.Type == gjson.True:
return true, true
case result.Type == gjson.False:
return false, true
default:
return false, false
}
}
// Exists returns the whether the given key exists.
func (jf *JSONFetcher) Exists(key string) bool {
result := gjson.Get(jf.json, key)
return result.Exists()
}

44
database/query/parser.go Normal file
View file

@ -0,0 +1,44 @@
package query
var (
operatorNames = map[string]uint8{
"==": Equals,
">": GreaterThan,
">=": GreaterThanOrEqual,
"<": LessThan,
"<=": LessThanOrEqual,
"f==": FloatEquals,
"f>": FloatGreaterThan,
"f>=": FloatGreaterThanOrEqual,
"f<": FloatLessThan,
"f<=": FloatLessThanOrEqual,
"sameas": SameAs,
"s==": SameAs,
"contains": Contains,
"co": Contains,
"startswith": StartsWith,
"sw": StartsWith,
"endswith": EndsWith,
"ew": EndsWith,
"in": In,
"matches": Matches,
"re": Matches,
"is": Is,
"exists": Exists,
"ex": Exists,
}
)
func getOpName(operator uint8) string {
for opName, op := range operatorNames {
if op == operator {
return opName
}
}
return "[unknown]"
}
// ParseQuery parses a plaintext query.
func ParseQuery(query string) (*Query, error) {
return nil, nil
}

98
database/query/query.go Normal file
View file

@ -0,0 +1,98 @@
package query
import (
"fmt"
"regexp"
)
// Example:
// q.New("core:/",
// q.Where("a", q.GreaterThan, 0),
// q.Where("b", q.Equals, 0),
// q.Or(
// q.Where("c", q.StartsWith, "x"),
// q.Where("d", q.Contains, "y")
// )
// )
var (
prefixExpr = regexp.MustCompile("^[a-z-]+:")
)
// Query contains a compiled query.
type Query struct {
prefix string
conditions []Condition
}
// New creates a new query.
func New(prefix string, conditions ...Condition) (*Query, error) {
// check prefix
if !prefixExpr.MatchString(prefix) {
return nil, fmt.Errorf("invalid prefix: %s", prefix)
}
// check conditions
var err error
for _, cond := range conditions {
err = cond.check()
if err != nil {
return nil, err
}
}
// return query
return &Query{
prefix: prefix,
conditions: conditions,
}, nil
}
// MustCompile creates a new query and panics on an error.
func MustCompile(prefix string, conditions ...Condition) *Query {
q, err := New(prefix, conditions...)
if err != nil {
panic(err)
}
return q
}
// Prepend prepends (check first) new query conditions to the query.
func (q *Query) Prepend(conditions ...Condition) error {
// check conditions
var err error
for _, cond := range conditions {
err = cond.check()
if err != nil {
return err
}
}
q.conditions = append(conditions, q.conditions...)
return nil
}
// Append appends (check last) new query conditions to the query.
func (q *Query) Append(conditions ...Condition) error {
// check conditions
var err error
for _, cond := range conditions {
err = cond.check()
if err != nil {
return err
}
}
q.conditions = append(q.conditions, conditions...)
return nil
}
// Matches checks whether the query matches the supplied data object.
func (q *Query) Matches(f Fetcher) bool {
for _, cond := range q.conditions {
if !cond.complies(f) {
return false
}
}
return true
}

View file

@ -0,0 +1,104 @@
package query
import (
"testing"
)
var (
// copied from https://github.com/tidwall/gjson/blob/master/gjson_test.go
testJson = `{"age":100, "name":{"here":"B\\\"R"},
"noop":{"what is a wren?":"a bird"},
"happy":true,"immortal":false,
"items":[1,2,3,{"tags":[1,2,3],"points":[[1,2],[3,4]]},4,5,6,7],
"arr":["1",2,"3",{"hello":"world"},"4",5],
"vals":[1,2,3,{"sadf":sdf"asdf"}],"name":{"first":"tom","last":null},
"created":"2014-05-16T08:28:06.989Z",
"loggy":{
"programmers": [
{
"firstName": "Brett",
"lastName": "McLaughlin",
"email": "aaaa",
"tag": "good"
},
{
"firstName": "Jason",
"lastName": "Hunter",
"email": "bbbb",
"tag": "bad"
},
{
"firstName": "Elliotte",
"lastName": "Harold",
"email": "cccc",
"tag":, "good"
},
{
"firstName": 1002.3,
"age": 101
}
]
},
"lastly":{"yay":"final"},
"temperature": 120.413
}`
)
func testQuery(t *testing.T, f Fetcher, shouldMatch bool, condition Condition) {
q := MustCompile("test:")
err := q.Append(condition)
if err != nil {
t.Errorf("append failed: %s", err)
}
matched := q.Matches(f)
switch {
case matched && !shouldMatch:
t.Errorf("query should match")
case !matched && shouldMatch:
t.Errorf("query should not match")
}
}
func TestQuery(t *testing.T) {
// if !gjson.Valid(testJson) {
// t.Fatal("test json is invalid")
// }
f := NewJSONFetcher(testJson)
testQuery(t, f, true, Where("age", Equals, 100))
testQuery(t, f, true, Where("age", GreaterThan, uint8(99)))
testQuery(t, f, true, Where("age", GreaterThanOrEqual, 99))
testQuery(t, f, true, Where("age", GreaterThanOrEqual, 100))
testQuery(t, f, true, Where("age", LessThan, 101))
testQuery(t, f, true, Where("age", LessThanOrEqual, "101"))
testQuery(t, f, true, Where("age", LessThanOrEqual, 100))
testQuery(t, f, true, Where("temperature", FloatEquals, 120.413))
testQuery(t, f, true, Where("temperature", FloatGreaterThan, 120))
testQuery(t, f, true, Where("temperature", FloatGreaterThanOrEqual, 120))
testQuery(t, f, true, Where("temperature", FloatGreaterThanOrEqual, 120.413))
testQuery(t, f, true, Where("temperature", FloatLessThan, 121))
testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "121"))
testQuery(t, f, true, Where("temperature", FloatLessThanOrEqual, "120.413"))
testQuery(t, f, true, Where("lastly.yay", Matches, "final"))
testQuery(t, f, true, Where("lastly.yay", Contains, "ina"))
testQuery(t, f, true, Where("lastly.yay", StartsWith, "fin"))
testQuery(t, f, true, Where("lastly.yay", EndsWith, "nal"))
testQuery(t, f, true, Where("lastly.yay", In, "draft,final"))
testQuery(t, f, true, Where("lastly.yay", In, "final,draft"))
testQuery(t, f, true, Where("happy", Is, "true"))
testQuery(t, f, true, Where("happy", Is, "t"))
testQuery(t, f, true, Where("happy", Is, "1"))
testQuery(t, f, true, Not(Where("happy", Is, "false")))
testQuery(t, f, true, Not(Where("happy", Is, "f")))
testQuery(t, f, true, Not(Where("happy", Is, "0")))
testQuery(t, f, true, Where("happy", Exists, nil))
testQuery(t, f, true, Where("created", Matches, "^2014-[0-9]{2}-[0-9]{2}T"))
}