mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Add query package with first set of conditions and tests
This commit is contained in:
parent
1bf7e42d8a
commit
6ed50f34fb
16 changed files with 933 additions and 0 deletions
31
database/query/condition-and.go
Normal file
31
database/query/condition-and.go
Normal 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
|
||||
}
|
64
database/query/condition-bool.go
Normal file
64
database/query/condition-bool.go
Normal 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
|
||||
}
|
19
database/query/condition-error.go
Normal file
19
database/query/condition-error.go
Normal 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
|
||||
}
|
28
database/query/condition-exists.go
Normal file
28
database/query/condition-exists.go
Normal 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
|
||||
}
|
90
database/query/condition-float.go
Normal file
90
database/query/condition-float.go
Normal 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
|
||||
}
|
86
database/query/condition-int.go
Normal file
86
database/query/condition-int.go
Normal 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
|
||||
}
|
20
database/query/condition-not.go
Normal file
20
database/query/condition-not.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package query
|
||||
|
||||
// Not negates the supplied condition.
|
||||
func Not(c Condition) Condition {
|
||||
return ¬Cond{
|
||||
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()
|
||||
}
|
31
database/query/condition-or.go
Normal file
31
database/query/condition-or.go
Normal 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
|
||||
}
|
57
database/query/condition-regex.go
Normal file
57
database/query/condition-regex.go
Normal 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 ®exCondition{
|
||||
key: fmt.Sprintf("could not compile regex \"%s\": %s", v, err),
|
||||
operator: errorPresent,
|
||||
}
|
||||
}
|
||||
return ®exCondition{
|
||||
key: key,
|
||||
operator: operator,
|
||||
regex: r,
|
||||
}
|
||||
default:
|
||||
return ®exCondition{
|
||||
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
|
||||
}
|
56
database/query/condition-string.go
Normal file
56
database/query/condition-string.go
Normal 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
|
||||
}
|
60
database/query/condition-stringslice.go
Normal file
60
database/query/condition-stringslice.go
Normal 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
|
||||
}
|
67
database/query/condition.go
Normal file
67
database/query/condition.go
Normal 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
78
database/query/fetcher.go
Normal 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
44
database/query/parser.go
Normal 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
98
database/query/query.go
Normal 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
|
||||
}
|
104
database/query/query_test.go
Normal file
104
database/query/query_test.go
Normal 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"))
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue