mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-26 05:58:27 -05:00
feat(ocis): move ast and kql pkg to ocis-pkg
Signed-off-by: jkoberg <jkoberg@owncloud.com>
This commit is contained in:
@@ -44,7 +44,7 @@ l10n-push:
|
||||
|
||||
.PHONY: l10n-read
|
||||
l10n-read: $(GO_XGETTEXT)
|
||||
go-xgettext -o $(OUTPUT_DIR)/userlog.pot --keyword=l10n.Template -s pkg/service/response.go
|
||||
go-xgettext -o $(OUTPUT_DIR)/activitylog.pot --keyword=l10n.Template -s pkg/service/response.go
|
||||
|
||||
.PHONY: l10n-write
|
||||
l10n-write:
|
||||
|
||||
@@ -2,8 +2,8 @@ package config
|
||||
|
||||
// Log defines the available log configuration.
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_USERLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_USERLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_USERLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_USERLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"`
|
||||
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;ACTIVITYLOG_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"5.0"`
|
||||
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;ACTIVITYLOG_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"5.0"`
|
||||
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;ACTIVITYLOG_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"5.0"`
|
||||
File string `mapstructure:"file" env:"OCIS_LOG_FILE;ACTIVITYLOG_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"5.0"`
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/l10n"
|
||||
ehmsg "github.com/owncloud/ocis/v2/protogen/gen/ocis/messages/eventhistory/v0"
|
||||
ehsvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/eventhistory/v0"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -26,7 +26,7 @@ include ../../.make/generate.mk
|
||||
.PHONY: ci-go-generate
|
||||
ci-go-generate: $(PIGEON) $(MOCKERY) # CI runs ci-node-generate automatically before this target
|
||||
$(MOCKERY)
|
||||
$(PIGEON) -optimize-grammar -optimize-parser -o pkg/query/kql/dictionary_gen.go pkg/query/kql/dictionary.peg
|
||||
$(PIGEON) -optimize-grammar -optimize-parser -o ../../ocis-pkg/kql/dictionary_gen.go ../../ocis-pkg/kql/dictionary.peg
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
ci-node-generate:
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// Package ast provides available ast nodes.
|
||||
package ast
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Node represents abstract syntax tree node
|
||||
type Node interface {
|
||||
Location() *Location
|
||||
}
|
||||
|
||||
// Position represents a specific location in the source
|
||||
type Position struct {
|
||||
Line int
|
||||
Column int
|
||||
}
|
||||
|
||||
// Location represents the location of a node in the AST
|
||||
type Location struct {
|
||||
Start Position `json:"start"`
|
||||
End Position `json:"end"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// Base contains shared node attributes
|
||||
// each node should inherit from this
|
||||
type Base struct {
|
||||
Loc *Location
|
||||
}
|
||||
|
||||
// Location is the source location of the Node
|
||||
func (b *Base) Location() *Location { return b.Loc }
|
||||
|
||||
// Ast represents the query - node structure as abstract syntax tree
|
||||
type Ast struct {
|
||||
*Base
|
||||
Nodes []Node `json:"body"`
|
||||
}
|
||||
|
||||
// StringNode represents a string value
|
||||
type StringNode struct {
|
||||
*Base
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// BooleanNode represents a bool value
|
||||
type BooleanNode struct {
|
||||
*Base
|
||||
Key string
|
||||
Value bool
|
||||
}
|
||||
|
||||
// DateTimeNode represents a time.Time value
|
||||
type DateTimeNode struct {
|
||||
*Base
|
||||
Key string
|
||||
Operator *OperatorNode
|
||||
Value time.Time
|
||||
}
|
||||
|
||||
// OperatorNode represents an operator value like
|
||||
// AND, OR, NOT, =, <= ... and so on
|
||||
type OperatorNode struct {
|
||||
*Base
|
||||
Value string
|
||||
}
|
||||
|
||||
// GroupNode represents a collection of many grouped nodes
|
||||
type GroupNode struct {
|
||||
*Base
|
||||
Key string
|
||||
Nodes []Node
|
||||
}
|
||||
|
||||
// NodeKey tries to return the node key
|
||||
func NodeKey(n Node) string {
|
||||
switch node := n.(type) {
|
||||
case *StringNode:
|
||||
return node.Key
|
||||
case *DateTimeNode:
|
||||
return node.Key
|
||||
case *BooleanNode:
|
||||
return node.Key
|
||||
case *GroupNode:
|
||||
return node.Key
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// NodeValue tries to return the node key
|
||||
func NodeValue(n Node) interface{} {
|
||||
switch node := n.(type) {
|
||||
case *StringNode:
|
||||
return node.Value
|
||||
case *DateTimeNode:
|
||||
return node.Value
|
||||
case *BooleanNode:
|
||||
return node.Value
|
||||
case *GroupNode:
|
||||
return node.Nodes
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Package test provides shared test primitives for ast testing.
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
// DiffAst returns a human-readable report of the differences between two values
|
||||
// by default it ignores every ast node Base field.
|
||||
func DiffAst(x, y interface{}, opts ...cmp.Option) string {
|
||||
return cmp.Diff(
|
||||
x,
|
||||
y,
|
||||
append(
|
||||
opts,
|
||||
cmpopts.IgnoreFields(ast.Ast{}, "Base"),
|
||||
cmpopts.IgnoreFields(ast.StringNode{}, "Base"),
|
||||
cmpopts.IgnoreFields(ast.OperatorNode{}, "Base"),
|
||||
cmpopts.IgnoreFields(ast.GroupNode{}, "Base"),
|
||||
cmpopts.IgnoreFields(ast.BooleanNode{}, "Base"),
|
||||
cmpopts.IgnoreFields(ast.DateTimeNode{}, "Base"),
|
||||
)...,
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,8 @@ package bleve
|
||||
import (
|
||||
bQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
// Creator is combines a Builder and a Compiler which is used to Create the query.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
bleveQuery "github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/kql"
|
||||
)
|
||||
|
||||
var _fields = map[string]string{
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
var timeMustParse = func(t *testing.T, ts string) time.Time {
|
||||
|
||||
@@ -3,7 +3,7 @@ package query
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
)
|
||||
|
||||
// StartsWithBinaryOperatorError records an error and the operation that caused it.
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/now"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
func toNode[T ast.Node](in interface{}) (T, error) {
|
||||
var t T
|
||||
out, ok := in.(T)
|
||||
if !ok {
|
||||
return t, fmt.Errorf("can't convert '%T' to '%T'", in, t)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func toNodes[T ast.Node](in interface{}) ([]T, error) {
|
||||
switch v := in.(type) {
|
||||
case T:
|
||||
return []T{v}, nil
|
||||
case []T:
|
||||
return v, nil
|
||||
case []*ast.OperatorNode, []*ast.DateTimeNode:
|
||||
return toNodes[T](v)
|
||||
case []interface{}:
|
||||
var nodes []T
|
||||
for _, el := range v {
|
||||
node, err := toNodes[T](el)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes = append(nodes, node...)
|
||||
}
|
||||
return nodes, nil
|
||||
case nil:
|
||||
return nil, nil
|
||||
default:
|
||||
var t T
|
||||
return nil, fmt.Errorf("can't convert '%T' to '%T'", in, t)
|
||||
}
|
||||
}
|
||||
|
||||
func toString(in interface{}) (string, error) {
|
||||
switch v := in.(type) {
|
||||
case []byte:
|
||||
return string(v), nil
|
||||
case []interface{}:
|
||||
var str string
|
||||
|
||||
for i := range v {
|
||||
sv, err := toString(v[i])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
str += sv
|
||||
}
|
||||
|
||||
return str, nil
|
||||
case string:
|
||||
return v, nil
|
||||
default:
|
||||
return "", fmt.Errorf("can't convert '%T' to string", v)
|
||||
}
|
||||
}
|
||||
|
||||
func toTime(in interface{}) (time.Time, error) {
|
||||
ts, err := toString(in)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
return now.Parse(ts)
|
||||
}
|
||||
|
||||
func toTimeRange(in interface{}) (*time.Time, *time.Time, error) {
|
||||
var from, to time.Time
|
||||
|
||||
value, err := toString(in)
|
||||
if err != nil {
|
||||
return &from, &to, &query.UnsupportedTimeRangeError{}
|
||||
}
|
||||
|
||||
c := &now.Config{
|
||||
WeekStartDay: time.Monday,
|
||||
}
|
||||
|
||||
n := c.With(timeNow())
|
||||
|
||||
switch value {
|
||||
case "today":
|
||||
from = n.BeginningOfDay()
|
||||
to = n.EndOfDay()
|
||||
case "yesterday":
|
||||
yesterday := n.With(n.AddDate(0, 0, -1))
|
||||
from = yesterday.BeginningOfDay()
|
||||
to = yesterday.EndOfDay()
|
||||
case "this week":
|
||||
from = n.BeginningOfWeek()
|
||||
to = n.EndOfWeek()
|
||||
case "last week":
|
||||
lastWeek := n.With(n.AddDate(0, 0, -7))
|
||||
from = lastWeek.BeginningOfWeek()
|
||||
to = lastWeek.EndOfWeek()
|
||||
case "last 7 days":
|
||||
from = n.With(n.AddDate(0, 0, -6)).BeginningOfDay()
|
||||
to = n.EndOfDay()
|
||||
case "this month":
|
||||
from = n.BeginningOfMonth()
|
||||
to = n.EndOfMonth()
|
||||
case "last month":
|
||||
lastMonth := n.With(n.BeginningOfMonth().AddDate(0, 0, -1))
|
||||
from = lastMonth.BeginningOfMonth()
|
||||
to = lastMonth.EndOfMonth()
|
||||
case "last 30 days":
|
||||
from = n.With(n.AddDate(0, 0, -29)).BeginningOfDay()
|
||||
to = n.EndOfDay()
|
||||
case "this year":
|
||||
from = n.BeginningOfYear()
|
||||
to = n.EndOfYear()
|
||||
case "last year":
|
||||
lastYear := n.With(n.AddDate(-1, 0, 0))
|
||||
from = lastYear.BeginningOfYear()
|
||||
to = lastYear.EndOfYear()
|
||||
}
|
||||
|
||||
if from.IsZero() || to.IsZero() {
|
||||
return nil, nil, &query.UnsupportedTimeRangeError{}
|
||||
}
|
||||
|
||||
return &from, &to, nil
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
// connectNodes connects given nodes
|
||||
func connectNodes(c Connector, nodes ...ast.Node) []ast.Node {
|
||||
var connectedNodes []ast.Node
|
||||
|
||||
for i := range nodes {
|
||||
ri := len(nodes) - 1 - i
|
||||
head := nodes[ri]
|
||||
|
||||
if connectionNodes := connectNode(c, head, connectedNodes...); len(connectionNodes) > 0 {
|
||||
connectedNodes = append(connectionNodes, connectedNodes...)
|
||||
}
|
||||
|
||||
connectedNodes = append([]ast.Node{head}, connectedNodes...)
|
||||
}
|
||||
|
||||
return connectedNodes
|
||||
}
|
||||
|
||||
// connectNode connects a tip node with the rest
|
||||
func connectNode(c Connector, headNode ast.Node, tailNodes ...ast.Node) []ast.Node {
|
||||
var nearestNeighborNode ast.Node
|
||||
var nearestNeighborOperators []*ast.OperatorNode
|
||||
|
||||
l:
|
||||
for _, tailNode := range tailNodes {
|
||||
switch node := tailNode.(type) {
|
||||
case *ast.OperatorNode:
|
||||
nearestNeighborOperators = append(nearestNeighborOperators, node)
|
||||
default:
|
||||
nearestNeighborNode = node
|
||||
break l
|
||||
}
|
||||
}
|
||||
|
||||
if nearestNeighborNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.Connect(headNode, nearestNeighborNode, nearestNeighborOperators)
|
||||
}
|
||||
|
||||
// Connector is responsible to decide what node connections are needed
|
||||
type Connector interface {
|
||||
Connect(head ast.Node, neighbor ast.Node, connections []*ast.OperatorNode) []ast.Node
|
||||
}
|
||||
|
||||
// DefaultConnector is the default node connector
|
||||
type DefaultConnector struct {
|
||||
sameKeyOPValue string
|
||||
}
|
||||
|
||||
// Connect implements the Connector interface and is used to connect the nodes using
|
||||
// the default logic defined by the kql spec.
|
||||
func (c DefaultConnector) Connect(head ast.Node, neighbor ast.Node, connections []*ast.OperatorNode) []ast.Node {
|
||||
switch head.(type) {
|
||||
case *ast.OperatorNode:
|
||||
return nil
|
||||
}
|
||||
|
||||
headKey := strings.ToLower(ast.NodeKey(head))
|
||||
neighborKey := strings.ToLower(ast.NodeKey(neighbor))
|
||||
|
||||
connection := &ast.OperatorNode{
|
||||
Base: &ast.Base{Loc: &ast.Location{Source: &[]string{"implicitly operator"}[0]}},
|
||||
Value: BoolAND,
|
||||
}
|
||||
|
||||
// if the current node and the neighbor node have the same key
|
||||
// the connection is of type OR
|
||||
//
|
||||
// spec: same
|
||||
// author:"John Smith" author:"Jane Smith"
|
||||
// author:"John Smith" OR author:"Jane Smith"
|
||||
//
|
||||
// if the nodes have NO key, the edge is a AND connection
|
||||
//
|
||||
// spec: same
|
||||
// cat dog
|
||||
// cat AND dog
|
||||
// from the spec:
|
||||
// To construct complex queries, you can combine multiple
|
||||
// free-text expressions with KQL query operators.
|
||||
// If there are multiple free-text expressions without any
|
||||
// operators in between them, the query behavior is the same
|
||||
// as using the AND operator.
|
||||
//
|
||||
// nodes inside of group node are handled differently,
|
||||
// if no explicit operator given, it uses AND
|
||||
//
|
||||
// spec: same
|
||||
// author:"John Smith" AND author:"Jane Smith"
|
||||
// author:("John Smith" "Jane Smith")
|
||||
if headKey == neighborKey && headKey != "" && neighborKey != "" {
|
||||
connection.Value = c.sameKeyOPValue
|
||||
}
|
||||
|
||||
// decisions based on nearest neighbor operators
|
||||
for i, node := range connections {
|
||||
// consider direct neighbor operator only
|
||||
if i == 0 {
|
||||
// no connection is necessary here because an `AND` or `OR` edge is already present
|
||||
// exit
|
||||
for _, skipValue := range []string{BoolOR, BoolAND} {
|
||||
if node.Value == skipValue {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// if neighbor node negotiates, an AND edge is needed
|
||||
//
|
||||
// spec: same
|
||||
// cat -dog
|
||||
// cat AND NOT dog
|
||||
if node.Value == BoolNOT {
|
||||
connection.Value = BoolAND
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []ast.Node{connection}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
{
|
||||
package kql
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// ast
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
AST <-
|
||||
n:Nodes {
|
||||
return buildAST(n, c.text, c.pos)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// nodes
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
Nodes <-
|
||||
(_ Node)+
|
||||
|
||||
Node <-
|
||||
GroupNode /
|
||||
PropertyRestrictionNodes /
|
||||
OperatorBooleanNodes /
|
||||
FreeTextKeywordNodes
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// nesting
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
GroupNode <-
|
||||
k:(Char+)? (OperatorColonNode / OperatorEqualNode)? "(" v:Nodes ")" {
|
||||
return buildGroupNode(k, v, c.text, c.pos)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// property restrictions
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
PropertyRestrictionNodes <-
|
||||
YesNoPropertyRestrictionNode /
|
||||
DateTimeRestrictionNode /
|
||||
TextPropertyRestrictionNode
|
||||
|
||||
YesNoPropertyRestrictionNode <-
|
||||
k:Char+ (OperatorColonNode / OperatorEqualNode) v:("true" / "false"){
|
||||
return buildBooleanNode(k, v, c.text, c.pos)
|
||||
}
|
||||
|
||||
DateTimeRestrictionNode <-
|
||||
k:Char+ o:(
|
||||
OperatorGreaterOrEqualNode /
|
||||
OperatorLessOrEqualNode /
|
||||
OperatorGreaterNode /
|
||||
OperatorLessNode /
|
||||
OperatorEqualNode /
|
||||
OperatorColonNode
|
||||
) '"'? v:(
|
||||
DateTime /
|
||||
FullDate /
|
||||
FullTime
|
||||
) '"'? {
|
||||
return buildDateTimeNode(k, o, v, c.text, c.pos)
|
||||
} /
|
||||
k:Char+ (
|
||||
OperatorEqualNode /
|
||||
OperatorColonNode
|
||||
) '"'? v:NaturalLanguageDateTime '"'? {
|
||||
return buildNaturalLanguageDateTimeNodes(k, v, c.text, c.pos)
|
||||
}
|
||||
|
||||
TextPropertyRestrictionNode <-
|
||||
k:Char+ (OperatorColonNode / OperatorEqualNode) v:(String / [^ ()]+){
|
||||
return buildStringNode(k, v, c.text, c.pos)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// free text-keywords
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
FreeTextKeywordNodes <-
|
||||
PhraseNode /
|
||||
WordNode
|
||||
|
||||
PhraseNode <-
|
||||
OperatorColonNode? _ v:String _ OperatorColonNode? {
|
||||
return buildStringNode("", v, c.text, c.pos)
|
||||
}
|
||||
|
||||
WordNode <-
|
||||
OperatorColonNode? _ v:[^ :()]+ _ OperatorColonNode? {
|
||||
return buildStringNode("", v, c.text, c.pos)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// operators
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
OperatorBooleanNodes <-
|
||||
OperatorBooleanAndNode /
|
||||
OperatorBooleanNotNode /
|
||||
OperatorBooleanOrNode
|
||||
|
||||
OperatorBooleanAndNode <-
|
||||
("AND" / "+") {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorBooleanNotNode <-
|
||||
("NOT" / "-") {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorBooleanOrNode <-
|
||||
("OR") {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorColonNode <-
|
||||
":" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorEqualNode <-
|
||||
"=" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorLessNode <-
|
||||
"<" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorLessOrEqualNode <-
|
||||
"<=" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorGreaterNode <-
|
||||
">" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
OperatorGreaterOrEqualNode <-
|
||||
">=" {
|
||||
return buildOperatorNode(c.text, c.pos)
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// time
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
TimeYear <-
|
||||
Digit Digit Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
TimeMonth <-
|
||||
Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
TimeDay <-
|
||||
Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
TimeHour <-
|
||||
Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
TimeMinute <-
|
||||
Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
TimeSecond <-
|
||||
Digit Digit {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
FullDate <-
|
||||
TimeYear "-" TimeMonth "-" TimeDay {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
FullTime <-
|
||||
TimeHour ":" TimeMinute ":" TimeSecond ("." Digit+)? ("Z" / ("+" / "-") TimeHour ":" TimeMinute) {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
DateTime <-
|
||||
FullDate "T" FullTime {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
NaturalLanguageDateTime <-
|
||||
"today" /
|
||||
"yesterday" /
|
||||
"this week" /
|
||||
"last week" /
|
||||
"last 7 days" /
|
||||
"this month" /
|
||||
"last month" /
|
||||
"last 30 days" /
|
||||
"this year" /
|
||||
"last year" {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// misc
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
Char <-
|
||||
[A-Za-z] {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
String <-
|
||||
'"' v:[^"]* '"' {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
Digit <-
|
||||
[0-9] {
|
||||
return c.text, nil
|
||||
}
|
||||
|
||||
_ <-
|
||||
[ \t]* {
|
||||
return nil, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
Package kql provides the ability to work with kql queries.
|
||||
|
||||
Not every aspect of the spec is implemented yet.
|
||||
The language support will grow over time if needed.
|
||||
|
||||
The following spec parts are supported and tested:
|
||||
- 2.1.2 AND Operator
|
||||
- 2.1.6 NOT Operator
|
||||
- 2.1.8 OR Operator
|
||||
- 2.1.12 Parentheses
|
||||
- 2.3.5 Date Tokens
|
||||
- 3.1.11 Implicit Operator
|
||||
- 3.1.12 Parentheses
|
||||
- 3.1.2 AND Operator
|
||||
- 3.1.6 NOT Operator
|
||||
- 3.1.8 OR Operator
|
||||
- 3.2.3 Implicit Operator for Property Restriction
|
||||
- 3.3.1.1.1 Implicit AND Operator
|
||||
- 3.3.5 Date Tokens
|
||||
|
||||
References:
|
||||
- https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference
|
||||
- https://learn.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-kql/3bbf06cd-8fc1-4277-bd92-8661ccd3c9b0
|
||||
- https://msopenspecs.azureedge.net/files/MS-KQL/%5bMS-KQL%5d.pdf
|
||||
*/
|
||||
package kql
|
||||
@@ -1,11 +0,0 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PatchTimeNow is here to patch the package time now func,
|
||||
// which is used in the test suite
|
||||
func PatchTimeNow(t func() time.Time) {
|
||||
timeNow = t
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
func base(text []byte, pos position) (*ast.Base, error) {
|
||||
source, err := toString(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.Base{
|
||||
Loc: &ast.Location{
|
||||
Start: ast.Position{
|
||||
Line: pos.line,
|
||||
Column: pos.col,
|
||||
},
|
||||
End: ast.Position{
|
||||
Line: pos.line,
|
||||
Column: pos.col + len(text),
|
||||
},
|
||||
Source: &source,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAST(n interface{}, text []byte, pos position) (*ast.Ast, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := toNodes[ast.Node](n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &ast.Ast{
|
||||
Base: b,
|
||||
Nodes: connectNodes(DefaultConnector{sameKeyOPValue: BoolOR}, nodes...),
|
||||
}
|
||||
|
||||
if err := validateAst(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func buildStringNode(k, v interface{}, text []byte, pos position) (*ast.StringNode, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := toString(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := toString(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.StringNode{
|
||||
Base: b,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDateTimeNode(k, o, v interface{}, text []byte, pos position) (*ast.DateTimeNode, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operator, err := toNode[*ast.OperatorNode](o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := toString(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := toTime(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.DateTimeNode{
|
||||
Base: b,
|
||||
Key: key,
|
||||
Operator: operator,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
func buildNaturalLanguageDateTimeNodes(k, v interface{}, text []byte, pos position) ([]ast.Node, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := toString(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
from, to, err := toTimeRange(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []ast.Node{
|
||||
&ast.DateTimeNode{
|
||||
Base: b,
|
||||
Value: *from,
|
||||
Key: key,
|
||||
Operator: &ast.OperatorNode{Value: ">="},
|
||||
},
|
||||
&ast.OperatorNode{Value: BoolAND},
|
||||
&ast.DateTimeNode{
|
||||
Base: b,
|
||||
Value: *to,
|
||||
Key: key,
|
||||
Operator: &ast.OperatorNode{Value: "<="},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func buildBooleanNode(k, v interface{}, text []byte, pos position) (*ast.BooleanNode, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := toString(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := toString(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.BooleanNode{
|
||||
Base: b,
|
||||
Key: key,
|
||||
Value: strings.ToLower(value) == "true",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildOperatorNode(text []byte, pos position) (*ast.OperatorNode, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := toString(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch value {
|
||||
case "+":
|
||||
value = BoolAND
|
||||
case "-":
|
||||
value = BoolNOT
|
||||
}
|
||||
|
||||
return &ast.OperatorNode{
|
||||
Base: b,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildGroupNode(k, n interface{}, text []byte, pos position) (*ast.GroupNode, error) {
|
||||
b, err := base(text, pos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, _ := toString(k)
|
||||
|
||||
nodes, err := toNodes[ast.Node](n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gn := &ast.GroupNode{
|
||||
Base: b,
|
||||
Key: key,
|
||||
Nodes: connectNodes(DefaultConnector{sameKeyOPValue: BoolOR}, nodes...),
|
||||
}
|
||||
|
||||
if err := validateGroupNode(gn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gn, nil
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package kql
|
||||
|
||||
//go:generate go run github.com/mna/pigeon -optimize-grammar -optimize-parser -o dictionary_gen.go dictionary.peg
|
||||
@@ -1,49 +0,0 @@
|
||||
// Package kql provides the ability to work with kql queries.
|
||||
package kql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
// The operator node value definition
|
||||
const (
|
||||
// BoolAND connect two nodes with "AND"
|
||||
BoolAND = "AND"
|
||||
// BoolOR connect two nodes with "OR"
|
||||
BoolOR = "OR"
|
||||
// BoolNOT connect two nodes with "NOT"
|
||||
BoolNOT = "NOT"
|
||||
)
|
||||
|
||||
// Builder implements kql Builder interface
|
||||
type Builder struct{}
|
||||
|
||||
// Build creates an ast.Ast based on a kql query
|
||||
func (b Builder) Build(q string) (*ast.Ast, error) {
|
||||
f, err := Parse("", []byte(q))
|
||||
if err != nil {
|
||||
var list errList
|
||||
errors.As(err, &list)
|
||||
|
||||
for _, listError := range list {
|
||||
var parserError *parserError
|
||||
switch {
|
||||
case errors.As(listError, &parserError):
|
||||
if parserError.Inner != nil {
|
||||
return nil, parserError.Inner
|
||||
}
|
||||
|
||||
return nil, listError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return f.(*ast.Ast), nil
|
||||
}
|
||||
|
||||
// timeNow mirrors time.Now by default, the only reason why this exists
|
||||
// is to monkey patch it from the tests. See PatchTimeNow
|
||||
var timeNow = time.Now
|
||||
@@ -1,55 +0,0 @@
|
||||
package kql_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
tAssert "github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/kql"
|
||||
)
|
||||
|
||||
func TestNewAST(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
givenQuery string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
givenQuery: "foo:bar",
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
givenQuery: kql.BoolAND,
|
||||
expectedError: query.StartsWithBinaryOperatorError{
|
||||
Node: &ast.OperatorNode{Value: kql.BoolAND},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert := tAssert.New(t)
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := kql.Builder{}.Build(tt.givenQuery)
|
||||
|
||||
if tt.expectedError != nil {
|
||||
if tt.expectedError.Error() != "" {
|
||||
assert.Equal(err.Error(), tt.expectedError.Error())
|
||||
} else {
|
||||
assert.NotNil(err)
|
||||
}
|
||||
|
||||
assert.Nil(got)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(err)
|
||||
assert.NotNil(got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package kql
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query"
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
|
||||
func validateAst(a *ast.Ast) error {
|
||||
switch node := a.Nodes[0].(type) {
|
||||
case *ast.OperatorNode:
|
||||
switch node.Value {
|
||||
case BoolAND, BoolOR:
|
||||
return &query.StartsWithBinaryOperatorError{Node: node}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateGroupNode(n *ast.GroupNode) error {
|
||||
switch node := n.Nodes[0].(type) {
|
||||
case *ast.OperatorNode:
|
||||
switch node.Value {
|
||||
case BoolAND, BoolOR:
|
||||
return &query.StartsWithBinaryOperatorError{Node: node}
|
||||
}
|
||||
}
|
||||
|
||||
if n.Key != "" {
|
||||
for _, node := range n.Nodes {
|
||||
if ast.NodeKey(node) != "" {
|
||||
return &query.NamedGroupInvalidNodesError{Node: node}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// Package query provides functions to work with the different search query flavours.
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/owncloud/ocis/v2/services/search/pkg/query/ast"
|
||||
)
|
||||
import "github.com/owncloud/ocis/v2/ocis-pkg/ast"
|
||||
|
||||
// Builder is the interface that wraps the basic Build method.
|
||||
type Builder interface {
|
||||
|
||||
Reference in New Issue
Block a user