mirror of
https://github.com/safing/portbase
synced 2025-09-02 02:29:59 +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