From 3905efd059bfb9d4c7d6a30471b3ed01e6a06adc Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:03:40 -0800 Subject: [PATCH 01/49] Add the initial rql package layout and the predicate interfaces The separate Eval* methods will let us come up with a generic predicate expression type while also giving us compile-time type-checking where it is needed (the compile-time type-checking ensures that something like the Action primary doesn't accidentally get a numeric predicate). The *InDomain methods let us properly handle negation. Both these changes result in a simpler predicate implementation than the previous `wash find` code because we no longer have to create custom And/Or/Not operator classes. Note that the previous `wash find` negation code was complicated because the predicate interface design was conflating the "check if this predicate makes sense for this kind of entry/metadata value" [domain] and "evaluate the predicate for this OK value" [the corresponding Eval* method]) parts of an entry/value predicate. Signed-off-by: Enis Inan --- api/rql/ast/json.go | 21 ++++++++++ api/rql/astNode.go | 13 ++++++ api/rql/entry.go | 12 ++++++ api/rql/interfaces.go | 97 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 10 +++++ 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 api/rql/ast/json.go create mode 100644 api/rql/astNode.go create mode 100644 api/rql/entry.go create mode 100644 api/rql/interfaces.go diff --git a/api/rql/ast/json.go b/api/rql/ast/json.go new file mode 100644 index 000000000..6726d84ae --- /dev/null +++ b/api/rql/ast/json.go @@ -0,0 +1,21 @@ +package ast + +import ( + "encoding/json" + + "github.com/puppetlabs/wash/api/rql" +) + +// MarshalJSON marshals the node into JSON +func MarshalJSON(n rql.ASTNode) ([]byte, error) { + return json.Marshal(n.Marshal()) +} + +// UnmarshalJSON unmarshals the node from json +func UnmarshalJSON(b []byte, n rql.ASTNode) error { + var input interface{} + if err := json.Unmarshal(b, &input); err != nil { + return err + } + return n.Unmarshal(input) +} diff --git a/api/rql/astNode.go b/api/rql/astNode.go new file mode 100644 index 000000000..e3968e06e --- /dev/null +++ b/api/rql/astNode.go @@ -0,0 +1,13 @@ +package rql + +// ASTNode represents an AST node in the RQL. Marshal should return +// an interface{} value that works with ast.MarshalJSON. This would +// typically be either a map[string]interface{} (JSON object), +// an []interface{} (JSON array), or a primitive type like +// nil (null), float64 (number), string, boolean, and time.Time. +// Similarly, the input in Unmarshal is an interface{} value that +// was decoded by ast.UnmarshalJSON. +type ASTNode interface { + Marshal() interface{} + Unmarshal(interface{}) error +} diff --git a/api/rql/entry.go b/api/rql/entry.go new file mode 100644 index 000000000..3003cad77 --- /dev/null +++ b/api/rql/entry.go @@ -0,0 +1,12 @@ +package rql + +import apitypes "github.com/puppetlabs/wash/api/types" + +// EntrySchema represents an RQL entry's schema +type EntrySchema = apitypes.EntrySchema + +// Entry represents an RQL entry +type Entry struct { + apitypes.Entry + Schema *EntrySchema +} diff --git a/api/rql/interfaces.go b/api/rql/interfaces.go new file mode 100644 index 000000000..4fbe0e95c --- /dev/null +++ b/api/rql/interfaces.go @@ -0,0 +1,97 @@ +package rql + +import ( + "time" + + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +// Primary is the interface implemented by all the primaries. A primary's +// domain is the set of all entries that it applies to. The domain can be +// specified at the instance-level (EntryInDomain), the schema-level +// (EntrySchemaInDomain) or both. +// +// Note that a primary can either be an EntryPredicate, an EntrySchemaPredicate, +// both or neither. In practice, the RQL will use EvalEntrySchema when pruning the +// stree and EvalEntry when filtering the entries. EntryInDomain and EntrySchemaInDomain +// are only needed to correctly negate the primaries (for without it, strict negation +// would return true for entries that are outside the primary's domain). +// +// For a given primary p, here are the possible scenarios for EvalEntrySchema +// and EvalEntry (including negation). Note that p.EntrySchemaInDomain and +// p.EvalEntrySchema can assume that s != nil, and that evaluation order is from +// left-to-right (so EntrySchemaInDomain first then EvalEntrySchema) with +// appropriate '&&' short-circuiting. +// * If p implements EntrySchemaPredicate, then for RQL +// EvalEntrySchema(s) == p.EntrySchemaInDomain(s) && p.EvalEntrySchema(s) +// NOT(EvalEntrySchema(s)) == p.EntrySchemaInDomain(s) && !p.EvalEntrySchema(s) +// otherwise +// EvalEntrySchema(s) == p.EntrySchemaInDomain(s) +// NOT(EvalEntrySchema(s)) == p.EntrySchemaInDomain(s) +// +// * If p implements EntryPredicate, then for RQL +// EvalEntry(e) == p.EntryInDomain(e) && p.EvalEntry(e) +// NOT(EvalEntry(e)) == p.EntryInDomain(e) && !p.EvalEntry(e) +// otherwise +// EvalEntry(e) == p.EntryInDomain(e) +// NOT(EvalEntry(e)) == p.EntryInDomain(e) +// +type Primary interface { + ASTNode + EntryInDomain(Entry) bool + EntrySchemaInDomain(*EntrySchema) bool +} + +// EntryPredicate represents a predicate on an entry +type EntryPredicate interface { + Primary + EvalEntry(Entry) bool +} + +// EntrySchemaPredicate represents a predicate on an entry schema object +type EntrySchemaPredicate interface { + Primary + EvalEntrySchema(*EntrySchema) bool +} + +// ValuePredicate represents a predicate on a metadata (JSON) value. Its +// domain is the set of all value types that the predicate applies to. +// +// In practice, the RQL will use EvalValue; ValueInDomain's only needed +// to correctly negate the predicates (like EntryInDomain/EntrySchemaInDomain). +// Here are the semantics for a given predicate. Note that evaluation order +// is from left-to-right (so ValueInDomain first then EvalValue) with appropriate +// '&&' short-circuiting. +// EvalValue(v) == p.ValueInDomain(v) && p.EvalValue(v) +// NOT(EvalValue(v)) == p.ValueInDomain(v) && !p.EvalValue(v) +type ValuePredicate interface { + ASTNode + ValueInDomain(interface{}) bool + EvalValue(interface{}) bool +} + +// StringPredicate represents a predicate on a string value +type StringPredicate interface { + ASTNode + EvalString(string) bool +} + +// NumericPredicate represents a predicate on a numeric value. The +// decimal.Decimal type lets us handle arbitrarily large numbers. +type NumericPredicate interface { + ASTNode + EvalNumeric(decimal.Decimal) bool +} + +// TimePredicate represents a predicate on a time value. +type TimePredicate interface { + ASTNode + EvalTime(time.Time) bool +} + +// ActionPredicate represents a predicate on a Wash action. +type ActionPredicate interface { + ASTNode + EvalAction(plugin.Action) bool +} diff --git a/go.mod b/go.mod index 3e810778c..ed8c4bb28 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/pkg/errors v0.8.1 github.com/shirou/gopsutil v2.18.12+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect + github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 github.com/simplereach/timeutils v1.2.0 // indirect github.com/sirupsen/logrus v1.4.2 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff // indirect @@ -87,9 +88,10 @@ require ( github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca go.mongodb.org/mongo-driver v1.0.4 // indirect go.opencensus.io v0.22.1 // indirect - golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 + golang.org/x/tools v0.0.0-20200121192408-9375b12bd86f // indirect google.golang.org/api v0.13.0 google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a gopkg.in/go-ini/ini.v1 v1.42.0 diff --git a/go.sum b/go.sum index fba34562d..479d936e6 100644 --- a/go.sum +++ b/go.sum @@ -233,6 +233,8 @@ github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAri github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 h1:Pm6R878vxWWWR+Sa3ppsLce/Zq+JNTs6aVvRu13jv9A= +github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA= github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -291,6 +293,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -313,6 +317,7 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -379,7 +384,12 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200121192408-9375b12bd86f h1:72ViAqybyE4ZxcMJgHTmiYXfUa+6rHEk/2tJpem4OBQ= +golang.org/x/tools v0.0.0-20200121192408-9375b12bd86f/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= From facf99c80e9c4701e6e04d95daeeff7e66cb72ae Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:15:23 -0800 Subject: [PATCH 02/49] Add the errz package and internal.NonterminalNode NonterminalNode gives us a generic way of implementing a non-terminal node in the AST (like e.g. Primary). Signed-off-by: Enis Inan --- api/rql/internal/errz/matchError.go | 38 +++++++++++++++++ api/rql/internal/nonterminalNode.go | 64 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 api/rql/internal/errz/matchError.go create mode 100644 api/rql/internal/nonterminalNode.go diff --git a/api/rql/internal/errz/matchError.go b/api/rql/internal/errz/matchError.go new file mode 100644 index 000000000..8f7321cd4 --- /dev/null +++ b/api/rql/internal/errz/matchError.go @@ -0,0 +1,38 @@ +package errz + +import "fmt" + +// MatchError represents the case when the input tokens did not +// match a given node +type MatchError struct { + reason string +} + +func (m MatchError) Error() string { + return m.reason +} + +// MatchErrorf creates a new MatchError object +func MatchErrorf(format string, a ...interface{}) MatchError { + return MatchError{fmt.Sprintf(format, a...)} +} + +// IsMatchError returns true if err is a MatchError, +// false otherwise. +func IsMatchError(err error) bool { + _, ok := err.(MatchError) + return ok +} + +// IsSyntaxError returns true if err is a syntax error, false otherwise. +func IsSyntaxError(err error) bool { + if err == nil { + return false + } + switch err.(type) { + case MatchError: + return false + default: + return true + } +} diff --git a/api/rql/internal/nonterminalNode.go b/api/rql/internal/nonterminalNode.go new file mode 100644 index 000000000..057a5151f --- /dev/null +++ b/api/rql/internal/nonterminalNode.go @@ -0,0 +1,64 @@ +package internal + +import "github.com/puppetlabs/wash/api/rql" + +import "github.com/puppetlabs/wash/api/rql/internal/errz" + +// A NonterminalNode is a node that can be matched by one or +// more other nodes when it is unmarshaled. MatchedNode returns +// the matched node +type NonterminalNode interface { + rql.ASTNode + MatchedNode() rql.ASTNode + SetMatchedNode(rql.ASTNode) NonterminalNode + SetMatchErrMsg(msg string) NonterminalNode +} + +func NewNonterminalNode(n rql.ASTNode, ns ...rql.ASTNode) NonterminalNode { + return &nonterminalNode{ + nodes: append(ns, n), + } +} + +type nonterminalNode struct { + matchedNode rql.ASTNode + errMsg string + nodes []rql.ASTNode +} + +func (nt *nonterminalNode) Marshal() interface{} { + return nt.matchedNode.Marshal() +} + +func (nt *nonterminalNode) Unmarshal(input interface{}) error { + for _, n := range nt.nodes { + err := n.Unmarshal(input) + if err == nil { + nt.SetMatchedNode(n) + return nil + } + if !errz.IsMatchError(err) { + return err + } + } + return errz.MatchErrorf(nt.errMsg) +} + +func (nt *nonterminalNode) MatchedNode() rql.ASTNode { + return nt.matchedNode +} + +func (nt *nonterminalNode) SetMatchedNode(n rql.ASTNode) NonterminalNode { + if mnt, ok := n.(NonterminalNode); ok { + n = mnt.MatchedNode() + } + nt.matchedNode = n + return nt +} + +func (nt *nonterminalNode) SetMatchErrMsg(errMsg string) NonterminalNode { + nt.errMsg = errMsg + return nt +} + +var _ = NonterminalNode(&nonterminalNode{}) From 7189d964c36bea82a611af90d511b1eac69aee93 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:30:17 -0800 Subject: [PATCH 03/49] Add the matcher package This will slightly simplify unmarshalling all the arrays for each of the predicates and primaries. It is a bit hacky, but it does DRY up some redundant code. Signed-off-by: Enis Inan --- api/rql/internal/matcher/core.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 api/rql/internal/matcher/core.go diff --git a/api/rql/internal/matcher/core.go b/api/rql/internal/matcher/core.go new file mode 100644 index 000000000..6f89f3fd8 --- /dev/null +++ b/api/rql/internal/matcher/core.go @@ -0,0 +1,20 @@ +package matcher + +// Matcher contains some useful helpers meant to make unmarshaling +// AST nodes easy. This is a bit hacky, but it DRY's up much of the +// unmarshaling code (for now). + +type Matcher = func(interface{}) bool + +func Array(firstElemMatcher Matcher) Matcher { + return func(v interface{}) bool { + array, ok := v.([]interface{}) + return ok && len(array) >= 1 && firstElemMatcher(array[0]) + } +} + +func Value(v interface{}) Matcher { + return func(v2 interface{}) bool { + return v == v2 + } +} From 475f4b18a7c701c9385e71413409158b82a37661 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:16:31 -0800 Subject: [PATCH 04/49] Add asttest package to centralize common test code Most of these methods are wrappers to the *InDomain and *Eval methods. MUM and AssertNotImplemented are a useful way to test that the generic predicate expression class panics if one tries to call an interface method that isn't implemented by the wrapped predicate (see the subsequent commit). Signed-off-by: Enis Inan --- api/rql/ast/asttest/suite.go | 259 +++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 api/rql/ast/asttest/suite.go diff --git a/api/rql/ast/asttest/suite.go b/api/rql/ast/asttest/suite.go new file mode 100644 index 000000000..7106aaf8c --- /dev/null +++ b/api/rql/ast/asttest/suite.go @@ -0,0 +1,259 @@ +package asttest + +import ( + "fmt" + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" + + "github.com/stretchr/testify/suite" +) + +// Suite represents a type that tests RQL AST nodes +type Suite struct { + suite.Suite +} + +// A ("Array") is a helper meant to make []interface{} input +// specs more readable. For example, instead of []interface{"foo", "bar", "baz"}, +// you can use s.A("foo", "bar", "baz") +func (s *Suite) A(vs ...interface{}) []interface{} { + return vs +} + +// N ("Number") is a helper meant to make decimal.NewFromString +// input specs more readable. For example, instead of +// decimal.NewFromString("3"), you can use s.N("3"). +func (s *Suite) N(n string) decimal.Decimal { + d, err := decimal.NewFromString(n) + if err != nil { + panic(fmt.Sprintf("s.N unexpected error: %v", err)) + } + return d +} + +// TM ("Time") is a helper meant to make time.Unix input +// specs more readable. For example, instead of +// time.Unix(1000, 0), you can use s.T(1000). +func (s *Suite) TM(t int64) time.Time { + return time.Unix(t, 0) +} + +// MTC => MarshalTestCase +func (s *Suite) MTC(n rql.ASTNode, expected interface{}) { + s.Equal(expected, n.Marshal()) +} + +// UMETC => UmarshalErrorTestCase +func (s *Suite) UMETC(n rql.ASTNode, input interface{}, errRegex string, isMatchErr bool) { + if err := n.Unmarshal(input); s.Error(err) { + if isMatchErr { + s.True(errz.IsMatchError(err), "err is not a MatchError") + } else { + s.False(errz.IsMatchError(err), "err is a MatchError") + } + s.Regexp(errRegex, err) + } +} + +// UMTC => UmarshalTestCase +func (s *Suite) UMTC(n rql.ASTNode, input interface{}, expected rql.ASTNode) { + if s.NoError(n.Unmarshal(input)) { + if nt, ok := n.(internal.NonterminalNode); ok { + n = nt.MatchedNode() + } + s.Equal(expected, n) + } +} + +// VIDTTC => ValueInDomainTrueTestCases +func (s *Suite) VIDTTC(n rql.ASTNode, trueVs ...interface{}) { + for _, trueV := range trueVs { + s.True(n.(rql.ValuePredicate).ValueInDomain(trueV)) + } +} + +// VIDFTC => ValueInDomainFalseTestCases +func (s *Suite) VIDFTC(n rql.ASTNode, falseVs ...interface{}) { + for _, falseV := range falseVs { + s.False(n.(rql.ValuePredicate).ValueInDomain(falseV)) + } +} + +// EVTTC => EvalValueTrueTestCases +func (s *Suite) EVTTC(n rql.ASTNode, trueVs ...interface{}) { + for _, trueV := range trueVs { + s.True(n.(rql.ValuePredicate).EvalValue(trueV)) + } +} + +// EVFTC => EvalValueFalseTestCases +func (s *Suite) EVFTC(n rql.ASTNode, falseVs ...interface{}) { + for _, falseV := range falseVs { + s.False(n.(rql.ValuePredicate).EvalValue(falseV)) + } +} + +// ESTTC => EvalStringTrueTestCases +func (s *Suite) ESTTC(n rql.ASTNode, trueVs ...string) { + for _, trueV := range trueVs { + s.True(n.(rql.StringPredicate).EvalString(trueV)) + } +} + +// ESFTC => EvalStringFalseTestCases +func (s *Suite) ESFTC(n rql.ASTNode, falseVs ...string) { + for _, falseV := range falseVs { + s.False(n.(rql.StringPredicate).EvalString(falseV)) + } +} + +// ENTTC => EvalNumericTrueTestCases +func (s *Suite) ENTTC(n rql.ASTNode, trueVs ...decimal.Decimal) { + for _, trueV := range trueVs { + s.True(n.(rql.NumericPredicate).EvalNumeric(trueV)) + } +} + +// ENFTC => EvalNumericFalseTestCases +func (s *Suite) ENFTC(n rql.ASTNode, falseVs ...decimal.Decimal) { + for _, falseV := range falseVs { + s.False(n.(rql.NumericPredicate).EvalNumeric(falseV)) + } +} + +// ETTTC => EvalTimeTrueTestCases +func (s *Suite) ETTTC(t rql.ASTNode, trueVs ...time.Time) { + for _, trueV := range trueVs { + s.True(t.(rql.TimePredicate).EvalTime(trueV)) + } +} + +// ETFTC => EvalTimeFalseTestCases +func (s *Suite) ETFTC(t rql.ASTNode, falseVs ...time.Time) { + for _, falseV := range falseVs { + s.False(t.(rql.TimePredicate).EvalTime(falseV)) + } +} + +// EIDTTC => EntryInDomainTrueTestCases +func (s *Suite) EIDTTC(e rql.ASTNode, trueVs ...rql.Entry) { + for _, trueV := range trueVs { + s.True(e.(rql.Primary).EntryInDomain(trueV)) + } +} + +// EIDFTC => EntryInDomainFalseTestCases +func (s *Suite) EIDFTC(e rql.ASTNode, falseVs ...rql.Entry) { + for _, falseV := range falseVs { + s.False(e.(rql.Primary).EntryInDomain(falseV)) + } +} + +// EETTC => EvalEntryTrueTestCases +func (s *Suite) EETTC(e rql.ASTNode, trueVs ...rql.Entry) { + for _, trueV := range trueVs { + s.True(e.(rql.EntryPredicate).EvalEntry(trueV)) + } +} + +// EEFTC => EvalEntryFalseTestCases +func (s *Suite) EEFTC(e rql.ASTNode, falseVs ...rql.Entry) { + for _, falseV := range falseVs { + s.False(e.(rql.EntryPredicate).EvalEntry(falseV)) + } +} + +// ESIDTTC => EntrySchemaInDomainTrueTestCases +func (s *Suite) ESIDTTC(e rql.ASTNode, trueVs ...*rql.EntrySchema) { + for _, trueV := range trueVs { + s.True(e.(rql.Primary).EntrySchemaInDomain(trueV)) + } +} + +// ESIDFTC => EntrySchemaInDomainFalseTestCases +func (s *Suite) ESIDFTC(e rql.ASTNode, falseVs ...*rql.EntrySchema) { + for _, falseV := range falseVs { + s.False(e.(rql.Primary).EntrySchemaInDomain(falseV)) + } +} + +// EESTTC => EvalEntrySchemaTrueTestCases +func (s *Suite) EESTTC(e rql.ASTNode, trueVs ...*rql.EntrySchema) { + for _, trueV := range trueVs { + s.True(e.(rql.EntrySchemaPredicate).EvalEntrySchema(trueV)) + } +} + +// EESFTC => EvalEntrySchemaFalseTestCases +func (s *Suite) EESFTC(e rql.ASTNode, falseVs ...*rql.EntrySchema) { + for _, falseV := range falseVs { + s.False(e.(rql.EntrySchemaPredicate).EvalEntrySchema(falseV)) + } +} + +// EATTC => EvalActionTrueTestCases +func (s *Suite) EATTC(e rql.ASTNode, trueVs ...plugin.Action) { + for _, trueV := range trueVs { + s.True(e.(rql.ActionPredicate).EvalAction(trueV)) + } +} + +// EAFTC => EvalActionFalseTestCases +func (s *Suite) EAFTC(e rql.ASTNode, falseVs ...plugin.Action) { + for _, falseV := range falseVs { + s.False(e.(rql.ActionPredicate).EvalAction(falseV)) + } +} + +// MUM => MustUnmarshal is a wrapper to ASTNode#Unmarshal. It will fail the +// test if unmarshaling fails +func (s *Suite) MUM(n rql.ASTNode, input interface{}) { + if err := n.Unmarshal(input); err != nil { + s.FailNow(fmt.Sprintf("unexpectedly failed to unmarshal n: %v", err.Error())) + } +} + +type InterfaceCode int8 + +// Here, C => Code. So EntryPredicateC => EntryPredicateCode +const ( + PrimaryC InterfaceCode = iota + EntryPredicateC + EntrySchemaPredicateC + ValuePredicateC + StringPredicateC + NumericPredicateC + TimePredicateC + ActionPredicateC +) + +func (s *Suite) AssertNotImplemented(n rql.ASTNode, interfaceCs ...InterfaceCode) { + for _, interfaceC := range interfaceCs { + switch interfaceC { + case PrimaryC: + s.FailNow("AssertNotImplemented should take the *Predicate interfaces' interface codes, _not_ Primary") + case EntryPredicateC: + s.Panics(func() { n.(rql.EntryPredicate).EntryInDomain(rql.Entry{}) }, "EntryPredicate") + s.Panics(func() { n.(rql.EntryPredicate).EvalEntry(rql.Entry{}) }, "EntryPredicate") + case EntrySchemaPredicateC: + s.Panics(func() { n.(rql.EntrySchemaPredicate).EntrySchemaInDomain(&rql.EntrySchema{}) }, "EntryPredicate") + s.Panics(func() { n.(rql.EntrySchemaPredicate).EvalEntrySchema(&rql.EntrySchema{}) }, "EntrySchemaPredicate") + case ValuePredicateC: + s.Panics(func() { n.(rql.ValuePredicate).ValueInDomain(nil) }, "ValuePredicate") + s.Panics(func() { n.(rql.ValuePredicate).EvalValue(nil) }, "ValuePredicate") + case StringPredicateC: + s.Panics(func() { n.(rql.StringPredicate).EvalString("") }, "StringPredicate") + case NumericPredicateC: + s.Panics(func() { n.(rql.NumericPredicate).EvalNumeric(s.N("0")) }, "NumericPredicate") + case TimePredicateC: + s.Panics(func() { n.(rql.TimePredicate).EvalTime(s.TM(0)) }, "TimePredicate") + case ActionPredicateC: + s.Panics(func() { n.(rql.ActionPredicate).EvalAction(plugin.Action{}) }, "ActionPredicate") + } + } +} From 09acf2349d63f1448316dd241504468c87e71cbe Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:19:34 -0800 Subject: [PATCH 05/49] Add the generic predicate expression type Note that all subsequent predicates and primaries will be wrapped in the Atom type. The Atom type's a useful place to centralize the EntryPredicate, EntrySchemaPredicate, and ValuePredicate semantics mentioned in rql/interfaces.go. Also, the primary predicate expression code is in expression.go. Some of the tests use predicates that haven't yet been shown. These tests are meant to test And/Or's truth tables for the appropriate Eval* methods; hopefully that part is clear from the code. Subsequent commits will include the separate predicate/primary implementations. These implementations will have a corresponding TestExpression_AtomAndNot test case that ensures that they can be marshaled as atoms, that they are negated correctly and they will likely work with the general predicate expression interface. Signed-off-by: Enis Inan --- api/rql/internal/predicate/expression/and.go | 71 +++++++++ .../internal/predicate/expression/and_test.go | 116 ++++++++++++++ api/rql/internal/predicate/expression/atom.go | 89 +++++++++++ .../predicate/expression/atom_test.go | 30 ++++ api/rql/internal/predicate/expression/base.go | 37 +++++ .../internal/predicate/expression/binOp.go | 45 ++++++ .../predicate/expression/expression.go | 142 ++++++++++++++++++ .../predicate/expression/expression_test.go | 93 ++++++++++++ api/rql/internal/predicate/expression/not.go | 98 ++++++++++++ .../internal/predicate/expression/not_test.go | 34 +++++ api/rql/internal/predicate/expression/or.go | 72 +++++++++ .../internal/predicate/expression/or_test.go | 116 ++++++++++++++ 12 files changed, 943 insertions(+) create mode 100644 api/rql/internal/predicate/expression/and.go create mode 100644 api/rql/internal/predicate/expression/and_test.go create mode 100644 api/rql/internal/predicate/expression/atom.go create mode 100644 api/rql/internal/predicate/expression/atom_test.go create mode 100644 api/rql/internal/predicate/expression/base.go create mode 100644 api/rql/internal/predicate/expression/binOp.go create mode 100644 api/rql/internal/predicate/expression/expression.go create mode 100644 api/rql/internal/predicate/expression/expression_test.go create mode 100644 api/rql/internal/predicate/expression/not.go create mode 100644 api/rql/internal/predicate/expression/not_test.go create mode 100644 api/rql/internal/predicate/expression/or.go create mode 100644 api/rql/internal/predicate/expression/or_test.go diff --git a/api/rql/internal/predicate/expression/and.go b/api/rql/internal/predicate/expression/and.go new file mode 100644 index 000000000..2f08784fb --- /dev/null +++ b/api/rql/internal/predicate/expression/and.go @@ -0,0 +1,71 @@ +package expression + +import ( + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +func And(p1 rql.ASTNode, p2 rql.ASTNode) rql.ASTNode { + return &and{binOp{ + op: "AND", + p1: toAtom(p1), + p2: toAtom(p2), + }} +} + +type and struct { + binOp +} + +func (a *and) EvalEntry(e rql.Entry) bool { + ep1 := a.p1.(rql.EntryPredicate) + ep2 := a.p2.(rql.EntryPredicate) + return ep1.EvalEntry(e) && ep2.EvalEntry(e) +} + +func (a *and) EvalEntrySchema(s *rql.EntrySchema) bool { + esp1 := a.p1.(rql.EntrySchemaPredicate) + esp2 := a.p2.(rql.EntrySchemaPredicate) + return esp1.EvalEntrySchema(s) && esp2.EvalEntrySchema(s) +} + +func (a *and) EvalValue(v interface{}) bool { + vp1 := a.p1.(rql.ValuePredicate) + vp2 := a.p2.(rql.ValuePredicate) + return vp1.EvalValue(v) && vp2.EvalValue(v) +} + +func (a *and) EvalString(str string) bool { + sp1 := a.p1.(rql.StringPredicate) + sp2 := a.p2.(rql.StringPredicate) + return sp1.EvalString(str) && sp2.EvalString(str) +} + +func (a *and) EvalNumeric(x decimal.Decimal) bool { + np1 := a.p1.(rql.NumericPredicate) + np2 := a.p2.(rql.NumericPredicate) + return np1.EvalNumeric(x) && np2.EvalNumeric(x) +} + +func (a *and) EvalTime(t time.Time) bool { + tp1 := a.p1.(rql.TimePredicate) + tp2 := a.p2.(rql.TimePredicate) + return tp1.EvalTime(t) && tp2.EvalTime(t) +} + +func (a *and) EvalAction(action plugin.Action) bool { + ap1 := a.p1.(rql.ActionPredicate) + ap2 := a.p2.(rql.ActionPredicate) + return ap1.EvalAction(action) && ap2.EvalAction(action) +} + +var _ = rql.EntryPredicate(&and{}) +var _ = rql.EntrySchemaPredicate(&and{}) +var _ = rql.ValuePredicate(&and{}) +var _ = rql.StringPredicate(&and{}) +var _ = rql.NumericPredicate(&and{}) +var _ = rql.TimePredicate(&and{}) +var _ = rql.ActionPredicate(&and{}) diff --git a/api/rql/internal/predicate/expression/and_test.go b/api/rql/internal/predicate/expression/and_test.go new file mode 100644 index 000000000..a433f53f3 --- /dev/null +++ b/api/rql/internal/predicate/expression/and_test.go @@ -0,0 +1,116 @@ +package expression + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate" + "github.com/puppetlabs/wash/api/rql/internal/primary" + "github.com/puppetlabs/wash/plugin" + "github.com/stretchr/testify/suite" +) + +type AndTestSuite struct { + asttest.Suite +} + +func (s *AndTestSuite) TestMarshal() { + p := And(predicate.Boolean(true), predicate.Boolean(false)) + s.MTC(p, s.A("AND", predicate.Boolean(true).Marshal(), predicate.Boolean(false).Marshal())) +} + +func (s *AndTestSuite) TestUnmarshal() { + p := And(predicate.Boolean(false), predicate.Boolean(false)) + s.UMETC(p, "foo", "formatted.*'AND'.*.*", true) + s.UMETC(p, s.A("AND", "foo", "bar", "baz"), "'AND'.*.*", false) + s.UMETC(p, s.A("AND"), "AND.*LHS.*RHS.*expression", false) + s.UMETC(p, s.A("AND", true), "AND.*LHS.*RHS.*expression", false) + s.UMETC(p, s.A("AND", "foo", true), "AND.*LHS.*", false) + s.UMETC(p, s.A("AND", true, "foo"), "AND.*RHS.*", false) + s.UMTC(p, s.A("AND", true, true), And(predicate.Boolean(true), predicate.Boolean(true))) +} + +func (s *AndTestSuite) TestEvalEntry() { + s.EEFTC(And(primary.Boolean(false), primary.Boolean(false)), rql.Entry{}) + s.EEFTC(And(primary.Boolean(false), primary.Boolean(true)), rql.Entry{}) + s.EEFTC(And(primary.Boolean(true), primary.Boolean(false)), rql.Entry{}) + s.EETTC(And(primary.Boolean(true), primary.Boolean(true)), rql.Entry{}) +} + +func (s *AndTestSuite) TestEvalEntrySchema() { + s.EESFTC(And(primary.Boolean(false), primary.Boolean(false)), &rql.EntrySchema{}) + s.EESFTC(And(primary.Boolean(false), primary.Boolean(true)), &rql.EntrySchema{}) + s.EESFTC(And(primary.Boolean(true), primary.Boolean(false)), &rql.EntrySchema{}) + s.EESTTC(And(primary.Boolean(true), primary.Boolean(true)), &rql.EntrySchema{}) +} + +func (s *AndTestSuite) TestEvalValue() { + // Note that we can't use predicate.Boolean(val) here because those return true if v == val + p := And(predicate.NumericValue(predicate.LT, s.N("10")), predicate.NumericValue(predicate.GT, s.N("10"))) + // p1 == false, p2 == false + s.EVFTC(p, float64(10)) + // false, true + s.EVFTC(p, float64(11)) + // true, false + s.EVFTC(p, float64(9)) + // true, true + p.(*and).p2 = p.(*and).p1 + s.EVTTC(p, float64(9)) +} + +func (s *AndTestSuite) TestEvalString() { + p := And(predicate.StringValueEqual("one"), predicate.StringValueEqual("two")) + // p1 == false, p2 == false + s.ESFTC(p, "foo") + // false, true + s.ESFTC(p, "two") + // true, false + s.ESFTC(p, "one") + // true, true + p.(*and).p2 = p.(*and).p1 + s.ESTTC(p, "one") +} + +func (s *AndTestSuite) TestEvalNumeric() { + p := And(predicate.NumericValue(predicate.LT, s.N("10")), predicate.NumericValue(predicate.GT, s.N("10"))) + // p1 == false, p2 == false + s.ENFTC(p, s.N("10")) + // false, true + s.ENFTC(p, s.N("11")) + // true, false + s.ENFTC(p, s.N("9")) + // true, true + p.(*and).p2 = p.(*and).p1 + s.ENTTC(p, s.N("9")) +} + +func (s *AndTestSuite) TestEvalTime() { + p := And(predicate.TimeValue(predicate.LT, s.TM(10)), predicate.TimeValue(predicate.GT, s.TM(10))) + // p1 == false, p2 == false + s.ETFTC(p, s.TM(10)) + // false, true + s.ETFTC(p, s.TM(11)) + // true, false + s.ETFTC(p, s.TM(9)) + // true, true + p.(*and).p2 = p.(*and).p1 + s.ETTTC(p, s.TM(9)) +} + +func (s *AndTestSuite) TestEvalAction() { + p := And(predicate.Action(plugin.ExecAction()), predicate.Action(plugin.ListAction())) + // p1 == false, p2 == false + s.EAFTC(p, plugin.DeleteAction()) + // false, true + s.EAFTC(p, plugin.ListAction()) + // true, false + s.EAFTC(p, plugin.ExecAction()) + // true, true + p.(*and).p2 = p.(*and).p1 + s.EATTC(p, plugin.ExecAction()) +} + +func TestAnd(t *testing.T) { + suite.Run(t, new(AndTestSuite)) +} diff --git a/api/rql/internal/predicate/expression/atom.go b/api/rql/internal/predicate/expression/atom.go new file mode 100644 index 000000000..1bd9aa849 --- /dev/null +++ b/api/rql/internal/predicate/expression/atom.go @@ -0,0 +1,89 @@ +package expression + +import ( + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +func Atom(p rql.ASTNode) rql.ASTNode { + if _, ok := p.(expressionNode); ok { + panic("expression.Atom was called with an expression node") + } + return &atom{ + base: base{}, + p: p, + } +} + +func toAtom(p rql.ASTNode) rql.ASTNode { + if _, ok := p.(expressionNode); ok { + return p + } + return Atom(p) +} + +type atom struct { + base + p rql.ASTNode +} + +func (a *atom) Marshal() interface{} { + return a.p.Marshal() +} + +func (a *atom) Unmarshal(input interface{}) error { + if err := a.p.Unmarshal(input); err != nil { + return err + } + a.p = unravelNTN(a.p) + return nil +} + +func (a *atom) EvalEntry(e rql.Entry) bool { + result := a.p.(rql.Primary).EntryInDomain(e) + if ep, ok := a.p.(rql.EntryPredicate); ok { + result = result && ep.EvalEntry(e) + } + return result +} + +func (a *atom) EvalEntrySchema(s *rql.EntrySchema) bool { + result := a.p.(rql.Primary).EntrySchemaInDomain(s) + if sp, ok := a.p.(rql.EntrySchemaPredicate); ok { + result = result && sp.EvalEntrySchema(s) + } + return result +} + +func (a *atom) EvalValue(v interface{}) bool { + vp := a.p.(rql.ValuePredicate) + return vp.ValueInDomain(v) && vp.EvalValue(v) +} + +func (a *atom) EvalString(str string) bool { + return a.p.(rql.StringPredicate).EvalString(str) +} + +func (a *atom) EvalNumeric(x decimal.Decimal) bool { + return a.p.(rql.NumericPredicate).EvalNumeric(x) +} + +func (a *atom) EvalTime(t time.Time) bool { + return a.p.(rql.TimePredicate).EvalTime(t) +} + +func (a *atom) EvalAction(action plugin.Action) bool { + return a.p.(rql.ActionPredicate).EvalAction(action) +} + +var _ = expressionNode(&atom{}) +var _ = rql.EntryPredicate(&atom{}) +var _ = rql.EntrySchemaPredicate(&atom{}) +var _ = rql.ValuePredicate(&atom{}) +var _ = rql.StringPredicate(&atom{}) +var _ = rql.NumericPredicate(&atom{}) +var _ = rql.TimePredicate(&atom{}) +var _ = rql.ActionPredicate(&atom{}) diff --git a/api/rql/internal/predicate/expression/atom_test.go b/api/rql/internal/predicate/expression/atom_test.go new file mode 100644 index 000000000..787bcbf73 --- /dev/null +++ b/api/rql/internal/predicate/expression/atom_test.go @@ -0,0 +1,30 @@ +package expression + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate" + "github.com/stretchr/testify/suite" +) + +// These test Marshal/Unmarshal. Correctness of the Eval* methods +// is contained in the relevant predicate's unit tests + +type AtomTestSuite struct { + asttest.Suite +} + +func (s *AtomTestSuite) TestMarshal() { + s.MTC(Atom(predicate.Boolean(true)), predicate.Boolean(true).Marshal()) +} + +func (s *AtomTestSuite) TestUnmarshal() { + p := Atom(predicate.Boolean(false)) + s.UMETC(p, "foo", "formatted.*", true) + s.UMTC(p, true, Atom(predicate.Boolean(true))) +} + +func TestAtom(t *testing.T) { + suite.Run(t, new(AtomTestSuite)) +} diff --git a/api/rql/internal/predicate/expression/base.go b/api/rql/internal/predicate/expression/base.go new file mode 100644 index 000000000..b71b5ee3d --- /dev/null +++ b/api/rql/internal/predicate/expression/base.go @@ -0,0 +1,37 @@ +package expression + +import "github.com/puppetlabs/wash/api/rql" + +import "github.com/puppetlabs/wash/api/rql/internal" + +type expressionNode interface { + rql.ASTNode + valid() bool +} + +// base is a base class for expression nodes +type base struct{} + +func (b *base) EntryInDomain(_ rql.Entry) bool { + panic("Only the primaries implement EntryInDomain") +} + +func (b *base) EntrySchemaInDomain(_ *rql.EntrySchema) bool { + panic("Only the primaries implement EntrySchemaInDomain") +} + +func (b *base) ValueInDomain(v interface{}) bool { + panic("Only value predicates implement ValueInDomain") +} + +func (b *base) valid() bool { + return true +} + +// unravelNTN => unravelNonterminalNode +func unravelNTN(p rql.ASTNode) rql.ASTNode { + if nt, ok := p.(internal.NonterminalNode); ok { + return nt.MatchedNode() + } + return p +} diff --git a/api/rql/internal/predicate/expression/binOp.go b/api/rql/internal/predicate/expression/binOp.go new file mode 100644 index 000000000..2e7869f5c --- /dev/null +++ b/api/rql/internal/predicate/expression/binOp.go @@ -0,0 +1,45 @@ +package expression + +import ( + "fmt" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" +) + +// Note that each binOp implements its own Eval* methods so that we +// can take advantage of short-circuiting + +type binOp struct { + base + op string + p1 rql.ASTNode + p2 rql.ASTNode +} + +func (o *binOp) Marshal() interface{} { + return []interface{}{o.op, o.p1.Marshal(), o.p2.Marshal()} +} + +func (o *binOp) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value(o.op))(input) { + return errz.MatchErrorf("must be formatted as ['%v', , ]", o.op) + } + array := input.([]interface{}) + if len(array) > 3 { + return fmt.Errorf("must be formatted as ['%v', , ]", o.op) + } + if len(array) != 3 { + return fmt.Errorf("%v: missing one or both of the LHS and RHS expressions", o.op) + } + if err := o.p1.Unmarshal(array[1]); err != nil { + return fmt.Errorf("%v: error unmarshaling LHS expression: %w", o.op, err) + } + if err := o.p2.Unmarshal(array[2]); err != nil { + return fmt.Errorf("%v error unmarshaling RHS expression: %w", o.op, err) + } + o.p1 = unravelNTN(o.p1) + o.p2 = unravelNTN(o.p2) + return nil +} diff --git a/api/rql/internal/predicate/expression/expression.go b/api/rql/internal/predicate/expression/expression.go new file mode 100644 index 000000000..19d728861 --- /dev/null +++ b/api/rql/internal/predicate/expression/expression.go @@ -0,0 +1,142 @@ +package expression + +import ( + "fmt" + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +type PtypeGenerator func() rql.ASTNode + +/* +New returns a new predicate expression of 'ptype' predicates (PE). +The AtomGenerator should generate "emtpy" structs representing +a 'ptype' predicate. + +A PE is described by the following grammar: + PE := NOT(PE) | AND(PE, PE) | OR(PE, PE) | Atom(ptype) +When evaluating the PE, we use a reduced version of the expression. The reduced +version ensures that we correctly implement predicates with a *InDomain method +(like EntryPredicates, EntrySchemaPredicates, ValuePredicates) without having to +write a lot of code. See the "reduce" method's implementation for details on how a +given PE's reduced. +*/ +func New(ptype string, g PtypeGenerator) rql.ASTNode { + e := &expression{ + base: base{}, + ptype: ptype, + g: g, + } + return e +} + +type expression struct { + base + internal.NonterminalNode + ptype string + g PtypeGenerator + reducedForm expressionNode +} + +func (expr *expression) Unmarshal(input interface{}) error { + expr.NonterminalNode = internal.NewNonterminalNode( + Not(New(expr.ptype, expr.g)), + And(New(expr.ptype, expr.g), New(expr.ptype, expr.g)), + Or(New(expr.ptype, expr.g), New(expr.ptype, expr.g)), + Atom(expr.g()), + ) + expr.SetMatchErrMsg(fmt.Sprintf("expected PE %v", expr.ptype)) + if err := expr.NonterminalNode.Unmarshal(input); err != nil { + if errz.IsMatchError(err) { + return err + } + return fmt.Errorf("failed to unmarshal PE %v: %w", expr.ptype, err) + } + expr.reducedForm = reduce(expr.MatchedNode()) + return nil +} + +func (expr *expression) EvalEntry(e rql.Entry) bool { + return expr.reducedForm.(rql.EntryPredicate).EvalEntry(e) +} + +func (expr *expression) EvalEntrySchema(s *rql.EntrySchema) bool { + return expr.reducedForm.(rql.EntrySchemaPredicate).EvalEntrySchema(s) +} + +func (expr *expression) EvalValue(v interface{}) bool { + return expr.reducedForm.(rql.ValuePredicate).EvalValue(v) +} + +func (expr *expression) EvalString(str string) bool { + return expr.reducedForm.(rql.StringPredicate).EvalString(str) +} + +func (expr *expression) EvalNumeric(x decimal.Decimal) bool { + return expr.reducedForm.(rql.NumericPredicate).EvalNumeric(x) +} + +func (expr *expression) EvalTime(t time.Time) bool { + return expr.reducedForm.(rql.TimePredicate).EvalTime(t) +} + +func (expr *expression) EvalAction(action plugin.Action) bool { + return expr.reducedForm.(rql.ActionPredicate).EvalAction(action) +} + +/* +reduce reduces the given PE. A reduced predicate expression of 'ptype' predicates +(RPE) has the following grammar: + RPE := Not(Atom(ptype)) | And(RPE, RPE) | Or(RPE, RPE) | Atom(ptype) +Note that the key difference between a PE and an RPE is that the NOT operator +in an RPE can only be associated with Atoms instead of other RPEs. As an example, +given the following PE + AND(OR(A1, NOT(A2)), NOT(OR(NOT(AND(A3, A4))), A5)) +Its corresponding RPE is + AND(OR(A1, NOT(A2)), AND(AND(A3, A4), NOT(A5))) +where we used De'Morgan's law to distribute the NOT inside the OR, and noted +that NOT(NOT(p)) == p. +*/ +func reduce(exp rql.ASTNode) expressionNode { + switch t := exp.(type) { + default: + panic(fmt.Sprintf("unknown predicate expression node %T", t)) + case *atom: + return t + case *and: + return And(reduce(t.p1), reduce(t.p2)).(expressionNode) + case *or: + return Or(reduce(t.p1), reduce(t.p2)).(expressionNode) + case *not: + switch p := t.p.(type) { + default: + panic(fmt.Sprintf("unknown predicate expression node %T", p)) + case *atom: + // NOT(p) is already reduced + return t + case *not: + // NOT(NOT(p)) == p + return reduce(p.p) + case *and: + // NOT(AND(p1, p2)) == OR(NOT(p1), NOT(p2)) + return reduce(Or(Not(p.p1), Not(p.p2))) + case *or: + // NOT(OR(p1, p2)) == AND(NOT(p1), NOT(p2)) + return reduce(And(Not(p.p1), Not(p.p2))) + } + } +} + +var _ = expressionNode(&expression{}) +var _ = rql.EntryPredicate(&expression{}) +var _ = rql.EntrySchemaPredicate(&expression{}) +var _ = rql.ValuePredicate(&expression{}) +var _ = rql.StringPredicate(&expression{}) +var _ = rql.NumericPredicate(&expression{}) +var _ = rql.TimePredicate(&expression{}) +var _ = rql.ActionPredicate(&expression{}) diff --git a/api/rql/internal/predicate/expression/expression_test.go b/api/rql/internal/predicate/expression/expression_test.go new file mode 100644 index 000000000..16f502785 --- /dev/null +++ b/api/rql/internal/predicate/expression/expression_test.go @@ -0,0 +1,93 @@ +package expression + +import ( + "fmt" + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/stretchr/testify/suite" +) + +/* +These tests are meant to test that we can unmarshal a PE +to its reduced version (and that we correctly return any +errors). Testing the correctness of the Eval* methods +themselves are left to the places that use the expression +type +*/ + +type ExpressionTestSuite struct { + asttest.Suite +} + +func (s *ExpressionTestSuite) UMTC(input interface{}, expected interface{}) { + e := s.mockExpression().(*expression) + if s.NoError(e.Unmarshal(input)) { + s.Equal(expected, e.reducedForm.Marshal()) + } +} + +func (s *ExpressionTestSuite) TestUnmarshal_Errors() { + e := s.mockExpression() + s.UMETC(e, "bar", "expected.*PE.*mock.*predicate", true) + s.UMETC(e, "foo", "failed.*unmarshal.*PE.*mock.*predicate.*syntax.*error", false) +} + +func (s *ExpressionTestSuite) TestUnmarshal() { + // Test simple unmarshaling (Atom, Not, Binop) + s.UMTC("p", "p") + s.UMTC(s.A("NOT", "p"), s.A("NOT", "p")) + s.UMTC(s.A("AND", "p", "p"), s.A("AND", "p", "p")) + s.UMTC(s.A("OR", "p", "p"), s.A("OR", "p", "p")) + + // Test nested unmarshaling + s.UMTC(s.A("AND", s.A("NOT", "p"), s.A("OR", "p", "p")), s.A("AND", s.A("NOT", "p"), s.A("OR", "p", "p"))) + s.UMTC(s.A("OR", s.A("NOT", "p"), s.A("AND", "p", "p")), s.A("OR", s.A("NOT", "p"), s.A("AND", "p", "p"))) + + // Test NOT reductions + // + // NOT(NOT(p)) == p + s.UMTC(s.A("NOT", s.A("NOT", "p")), "p") + // NOT(AND(p, p)) == OR(NOT(p), NOT(p)) + s.UMTC(s.A("NOT", s.A("AND", "p", "p")), s.A("OR", s.A("NOT", "p"), s.A("NOT", "p"))) + // NOT(OR(p, p)) == AND(NOT(p), NOT(p)) + s.UMTC(s.A("NOT", s.A("OR", "p", "p")), s.A("AND", s.A("NOT", "p"), s.A("NOT", "p"))) + + // Test a more complicated reduction + // + // AND(NOT(OR(p, NOT(p))), OR(NOT(AND(NOT(p), p)), NOT(p))) == + // AND(AND(NOT(p), p), OR(OR(p, NOT(p)), NOT(p))) + s.UMTC( + s.A("AND", s.A("NOT", s.A("OR", "p", s.A("NOT", "p"))), s.A("OR", s.A("NOT", s.A("AND", s.A("NOT", "p"), "p")), s.A("NOT", "p"))), + s.A("AND", s.A("AND", s.A("NOT", "p"), "p"), s.A("OR", s.A("OR", "p", s.A("NOT", "p")), s.A("NOT", "p"))), + ) +} + +func TestExpression(t *testing.T) { + suite.Run(t, new(ExpressionTestSuite)) +} + +func (s *ExpressionTestSuite) mockExpression() rql.ASTNode { + return New("mock predicate", func() rql.ASTNode { return &mockPtype{} }) +} + +type mockPtype struct{} + +func (p *mockPtype) Marshal() interface{} { + return "p" +} + +func (p *mockPtype) Unmarshal(input interface{}) error { + if input != "p" { + if input == "foo" { + // Mock a syntax error + return fmt.Errorf("syntax error") + } + return errz.MatchErrorf("expected 'p', got %v", input) + } + return nil +} + +var _ = rql.ASTNode(&mockPtype{}) diff --git a/api/rql/internal/predicate/expression/not.go b/api/rql/internal/predicate/expression/not.go new file mode 100644 index 000000000..7f9559d64 --- /dev/null +++ b/api/rql/internal/predicate/expression/not.go @@ -0,0 +1,98 @@ +package expression + +import ( + "fmt" + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +func Not(p rql.ASTNode) rql.ASTNode { + return ¬{ + p: toAtom(p), + } +} + +type not struct { + base + p rql.ASTNode +} + +func (n *not) Marshal() interface{} { + return []interface{}{"NOT", n.p.Marshal()} +} + +func (n *not) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("NOT"))(input) { + return errz.MatchErrorf("must be formatted as ['NOT', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['NOT', ]") + } + if len(array) != 2 { + return fmt.Errorf("NOT: missing the expression") + } + if err := n.p.Unmarshal(array[1]); err != nil { + return fmt.Errorf("NOT: error unmarshaling expression: %w", err) + } + n.p = unravelNTN(n.p) + return nil +} + +// INVARIANT: n.p is an atom by the time each of these Eval* +// methods are called. This only matters for EvalEntry, +// EvalEntrySchema and EvalValue. + +func (n *not) EvalEntry(e rql.Entry) bool { + a := n.p.(*atom) + result := a.p.(rql.Primary).EntryInDomain(e) + if ep, ok := a.p.(rql.EntryPredicate); ok { + result = result && !ep.EvalEntry(e) + } + return result +} + +func (n *not) EvalEntrySchema(s *rql.EntrySchema) bool { + a := n.p.(*atom) + result := a.p.(rql.Primary).EntrySchemaInDomain(s) + if sp, ok := a.p.(rql.EntrySchemaPredicate); ok { + result = result && !sp.EvalEntrySchema(s) + } + return result +} + +func (n *not) EvalValue(v interface{}) bool { + a := n.p.(*atom) + vp := a.p.(rql.ValuePredicate) + return vp.ValueInDomain(v) && !vp.EvalValue(v) +} + +func (n *not) EvalString(str string) bool { + return !n.p.(rql.StringPredicate).EvalString(str) +} + +func (n *not) EvalNumeric(x decimal.Decimal) bool { + return !n.p.(rql.NumericPredicate).EvalNumeric(x) +} + +func (n *not) EvalTime(t time.Time) bool { + return !n.p.(rql.TimePredicate).EvalTime(t) +} + +func (n *not) EvalAction(action plugin.Action) bool { + return !n.p.(rql.ActionPredicate).EvalAction(action) +} + +var _ = expressionNode(¬{}) +var _ = rql.EntryPredicate(¬{}) +var _ = rql.EntrySchemaPredicate(¬{}) +var _ = rql.ValuePredicate(¬{}) +var _ = rql.StringPredicate(¬{}) +var _ = rql.NumericPredicate(¬{}) +var _ = rql.TimePredicate(¬{}) +var _ = rql.ActionPredicate(¬{}) diff --git a/api/rql/internal/predicate/expression/not_test.go b/api/rql/internal/predicate/expression/not_test.go new file mode 100644 index 000000000..c0957ed36 --- /dev/null +++ b/api/rql/internal/predicate/expression/not_test.go @@ -0,0 +1,34 @@ +package expression + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate" + "github.com/stretchr/testify/suite" +) + +// These test Marshal/Unmarshal. Correctness of the Eval* methods +// is contained in the relevant predicate's unit tests to ensure +// that negation semantics are correct. + +type NotTestSuite struct { + asttest.Suite +} + +func (s *NotTestSuite) TestMarshal() { + s.MTC(Not(predicate.Boolean(true)), s.A("NOT", predicate.Boolean(true).Marshal())) +} + +func (s *NotTestSuite) TestUnmarshal() { + p := Not(predicate.Boolean(false)) + s.UMETC(p, "foo", "formatted.*'NOT'.*", true) + s.UMETC(p, s.A("NOT", "foo", "bar"), "formatted.*'NOT'.*", false) + s.UMETC(p, s.A("NOT"), "NOT.*expression", false) + s.UMETC(p, s.A("NOT", s.A()), "NOT.*error.*expression.*formatted.*", false) + s.UMTC(p, s.A("NOT", true), Not(predicate.Boolean(true))) +} + +func TestNot(t *testing.T) { + suite.Run(t, new(NotTestSuite)) +} diff --git a/api/rql/internal/predicate/expression/or.go b/api/rql/internal/predicate/expression/or.go new file mode 100644 index 000000000..040cd95f4 --- /dev/null +++ b/api/rql/internal/predicate/expression/or.go @@ -0,0 +1,72 @@ +package expression + +import ( + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/plugin" + "github.com/shopspring/decimal" +) + +func Or(p1 rql.ASTNode, p2 rql.ASTNode) rql.ASTNode { + return &or{binOp{ + op: "OR", + p1: toAtom(p1), + p2: toAtom(p2), + }} +} + +type or struct { + binOp +} + +func (o *or) EvalEntry(e rql.Entry) bool { + ep1 := o.p1.(rql.EntryPredicate) + ep2 := o.p2.(rql.EntryPredicate) + return ep1.EvalEntry(e) || ep2.EvalEntry(e) +} + +func (o *or) EvalEntrySchema(s *rql.EntrySchema) bool { + esp1 := o.p1.(rql.EntrySchemaPredicate) + esp2 := o.p2.(rql.EntrySchemaPredicate) + return esp1.EvalEntrySchema(s) || esp2.EvalEntrySchema(s) +} + +func (o *or) EvalValue(v interface{}) bool { + vp1 := o.p1.(rql.ValuePredicate) + vp2 := o.p2.(rql.ValuePredicate) + return vp1.EvalValue(v) || vp2.EvalValue(v) +} + +func (o *or) EvalString(str string) bool { + sp1 := o.p1.(rql.StringPredicate) + sp2 := o.p2.(rql.StringPredicate) + return sp1.EvalString(str) || sp2.EvalString(str) +} + +func (o *or) EvalNumeric(x decimal.Decimal) bool { + np1 := o.p1.(rql.NumericPredicate) + np2 := o.p2.(rql.NumericPredicate) + return np1.EvalNumeric(x) || np2.EvalNumeric(x) +} + +func (o *or) EvalTime(t time.Time) bool { + tp1 := o.p1.(rql.TimePredicate) + tp2 := o.p2.(rql.TimePredicate) + return tp1.EvalTime(t) || tp2.EvalTime(t) +} + +func (o *or) EvalAction(action plugin.Action) bool { + ap1 := o.p1.(rql.ActionPredicate) + ap2 := o.p2.(rql.ActionPredicate) + return ap1.EvalAction(action) || ap2.EvalAction(action) +} + +var _ = expressionNode(&or{}) +var _ = rql.EntryPredicate(&or{}) +var _ = rql.EntrySchemaPredicate(&or{}) +var _ = rql.ValuePredicate(&or{}) +var _ = rql.StringPredicate(&or{}) +var _ = rql.NumericPredicate(&or{}) +var _ = rql.TimePredicate(&or{}) +var _ = rql.ActionPredicate(&or{}) diff --git a/api/rql/internal/predicate/expression/or_test.go b/api/rql/internal/predicate/expression/or_test.go new file mode 100644 index 000000000..498f33aab --- /dev/null +++ b/api/rql/internal/predicate/expression/or_test.go @@ -0,0 +1,116 @@ +package expression + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate" + "github.com/puppetlabs/wash/api/rql/internal/primary" + "github.com/puppetlabs/wash/plugin" + "github.com/stretchr/testify/suite" +) + +type OrTestSuite struct { + asttest.Suite +} + +func (s *OrTestSuite) TestMarshal() { + p := Or(predicate.Boolean(true), predicate.Boolean(false)) + s.MTC(p, s.A("OR", predicate.Boolean(true).Marshal(), predicate.Boolean(false).Marshal())) +} + +func (s *OrTestSuite) TestUnmarshal() { + p := Or(predicate.Boolean(false), predicate.Boolean(false)) + s.UMETC(p, "foo", "formatted.*'OR'.*.*", true) + s.UMETC(p, s.A("OR", "foo", "bar", "baz"), "'OR'.*.*", false) + s.UMETC(p, s.A("OR"), "OR.*LHS.*RHS.*expression", false) + s.UMETC(p, s.A("OR", true), "OR.*LHS.*RHS.*expression", false) + s.UMETC(p, s.A("OR", "foo", true), "OR.*LHS.*", false) + s.UMETC(p, s.A("OR", true, "foo"), "OR.*RHS.*", false) + s.UMTC(p, s.A("OR", true, true), Or(predicate.Boolean(true), predicate.Boolean(true))) +} + +func (s *OrTestSuite) TestEvalEntry() { + s.EEFTC(Or(primary.Boolean(false), primary.Boolean(false)), rql.Entry{}) + s.EETTC(Or(primary.Boolean(false), primary.Boolean(true)), rql.Entry{}) + s.EETTC(Or(primary.Boolean(true), primary.Boolean(false)), rql.Entry{}) + s.EETTC(Or(primary.Boolean(true), primary.Boolean(true)), rql.Entry{}) +} + +func (s *OrTestSuite) TestEvalEntrySchema() { + s.EESFTC(Or(primary.Boolean(false), primary.Boolean(false)), &rql.EntrySchema{}) + s.EESTTC(Or(primary.Boolean(false), primary.Boolean(true)), &rql.EntrySchema{}) + s.EESTTC(Or(primary.Boolean(true), primary.Boolean(false)), &rql.EntrySchema{}) + s.EESTTC(Or(primary.Boolean(true), primary.Boolean(true)), &rql.EntrySchema{}) +} + +func (s *OrTestSuite) TestEvalValue() { + // Note that we can't use predicate.Boolean(val) here because those return true if v == val + p := Or(predicate.NumericValue(predicate.LT, s.N("10")), predicate.NumericValue(predicate.GT, s.N("10"))) + // p1 == false, p2 == false + s.EVFTC(p, float64(10)) + // false, true + s.EVTTC(p, float64(11)) + // true, false + s.EVTTC(p, float64(9)) + // true, true + p.(*or).p2 = p.(*or).p1 + s.EVTTC(p, float64(9)) +} + +func (s *OrTestSuite) TestEvalString() { + p := Or(predicate.StringValueEqual("one"), predicate.StringValueEqual("two")) + // p1 == false, p2 == false + s.ESFTC(p, "foo") + // false, true + s.ESTTC(p, "two") + // true, false + s.ESTTC(p, "one") + // true, true + p.(*or).p2 = p.(*or).p1 + s.ESTTC(p, "one") +} + +func (s *OrTestSuite) TestEvalNumeric() { + p := Or(predicate.NumericValue(predicate.LT, s.N("10")), predicate.NumericValue(predicate.GT, s.N("10"))) + // p1 == false, p2 == false + s.ENFTC(p, s.N("10")) + // false, true + s.ENTTC(p, s.N("11")) + // true, false + s.ENTTC(p, s.N("9")) + // true, true + p.(*or).p2 = p.(*or).p1 + s.ENTTC(p, s.N("9")) +} + +func (s *OrTestSuite) TestEvalTime() { + p := Or(predicate.TimeValue(predicate.LT, s.TM(10)), predicate.TimeValue(predicate.GT, s.TM(10))) + // p1 == false, p2 == false + s.ETFTC(p, s.TM(10)) + // false, true + s.ETTTC(p, s.TM(11)) + // true, false + s.ETTTC(p, s.TM(9)) + // true, true + p.(*or).p2 = p.(*or).p1 + s.ETTTC(p, s.TM(9)) +} + +func (s *OrTestSuite) TestEvalAction() { + p := Or(predicate.Action(plugin.ExecAction()), predicate.Action(plugin.ListAction())) + // p1 == false, p2 == false + s.EAFTC(p, plugin.DeleteAction()) + // false, true + s.EATTC(p, plugin.ListAction()) + // true, false + s.EATTC(p, plugin.ExecAction()) + // true, true + p.(*or).p2 = p.(*or).p1 + s.EATTC(p, plugin.ExecAction()) +} + +func TestOr(t *testing.T) { + suite.Run(t, new(OrTestSuite)) +} From 2e9116b7cfa342a167557dbb7e7bb0c3025c1b32 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:22:57 -0800 Subject: [PATCH 06/49] Implement the action predicate Signed-off-by: Enis Inan --- api/rql/internal/predicate/action.go | 45 ++++++++++++++++ api/rql/internal/predicate/action_test.go | 65 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 api/rql/internal/predicate/action.go create mode 100644 api/rql/internal/predicate/action_test.go diff --git a/api/rql/internal/predicate/action.go b/api/rql/internal/predicate/action.go new file mode 100644 index 000000000..66ec4caed --- /dev/null +++ b/api/rql/internal/predicate/action.go @@ -0,0 +1,45 @@ +package predicate + +import ( + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/plugin" +) + +func Action(a plugin.Action) rql.ActionPredicate { + return &action{ + a: a, + } +} + +type action struct { + a plugin.Action +} + +func (p *action) Marshal() interface{} { + return p.a.Name +} + +func (p *action) Unmarshal(input interface{}) error { + name, ok := input.(string) + if !ok { + return errz.MatchErrorf("must be formatted as ") + } + a, ok := plugin.Actions()[name] + if !ok { + return errz.MatchErrorf("must be formatted as ") + } + p.a = a + return nil +} + +func (p *action) EvalAction(action plugin.Action) bool { + return p.a.Name == action.Name +} + +// This is for the tests +func EqualAction(p rql.ASTNode, a string) bool { + return p.(*action).a.Name == a +} + +var _ = rql.ActionPredicate(&action{}) diff --git a/api/rql/internal/predicate/action_test.go b/api/rql/internal/predicate/action_test.go new file mode 100644 index 000000000..c8ab5b600 --- /dev/null +++ b/api/rql/internal/predicate/action_test.go @@ -0,0 +1,65 @@ +package predicate + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + "github.com/puppetlabs/wash/plugin" + "github.com/stretchr/testify/suite" +) + +type ActionTestSuite struct { + asttest.Suite +} + +func (s *ActionTestSuite) TestMarshal() { + s.MTC(Action(plugin.ExecAction()), "exec") +} + +func (s *ActionTestSuite) TestUnmarshal() { + a := Action(plugin.Action{}) + s.UMETC(a, 1, "formatted.*", true) + s.UMETC(a, "foo", "formatted.*", true) + // UMTC doesn't work because s.Equal doesn't work for the Action + // type. My best guess is because the Action type has a function + // as its field, and s.Equal doesn't work with functions. Thus, we + // do our own assertion here. + if s.NoError(a.Unmarshal("exec")) { + s.True(EqualAction(a, "exec")) + } +} + +func (s *ActionTestSuite) TestEvalAction() { + a := Action(plugin.ExecAction()) + s.EAFTC(a, plugin.ListAction()) + s.EATTC(a, plugin.ExecAction()) +} + +func (s *ActionTestSuite) TestExpression_AtomAndNot() { + expr := expression.New("action", func() rql.ASTNode { + return Action(plugin.Action{}) + }) + + s.MUM(expr, "exec") + s.EAFTC(expr, plugin.ListAction()) + s.EATTC(expr, plugin.ExecAction()) + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.ValuePredicateC, + asttest.StringPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", "exec"}) + s.EATTC(expr, plugin.ListAction()) + s.EAFTC(expr, plugin.ExecAction()) +} + +func TestAction(t *testing.T) { + suite.Run(t, new(ActionTestSuite)) +} From 45d8d5d38ae4cd1c938127078e0d54beaaa65880 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:23:19 -0800 Subject: [PATCH 07/49] Implement the Boolean predicate Signed-off-by: Enis Inan --- api/rql/internal/predicate/boolean.go | 58 ++++++++++ api/rql/internal/predicate/boolean_test.go | 120 +++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 api/rql/internal/predicate/boolean.go create mode 100644 api/rql/internal/predicate/boolean_test.go diff --git a/api/rql/internal/predicate/boolean.go b/api/rql/internal/predicate/boolean.go new file mode 100644 index 000000000..ee50445a4 --- /dev/null +++ b/api/rql/internal/predicate/boolean.go @@ -0,0 +1,58 @@ +package predicate + +import ( + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" +) + +func Boolean(val bool) rql.ValuePredicate { + return &boolean{ + val: val, + } +} + +type boolean struct { + val bool +} + +func (p *boolean) Marshal() interface{} { + return p.val +} + +func (p *boolean) Unmarshal(input interface{}) error { + val, ok := input.(bool) + if !ok { + return errz.MatchErrorf("must be formatted as ") + } + p.val = val + return nil +} + +func (p *boolean) ValueInDomain(v interface{}) bool { + _, ok := v.(bool) + return ok +} + +func (p *boolean) EvalValue(v interface{}) bool { + return v.(bool) == p.val +} + +func (p *boolean) EntryInDomain(rql.Entry) bool { + return true +} + +func (p *boolean) EvalEntry(_ rql.Entry) bool { + return p.val +} + +func (p *boolean) EntrySchemaInDomain(*rql.EntrySchema) bool { + return true +} + +func (p *boolean) EvalEntrySchema(_ *rql.EntrySchema) bool { + return p.val +} + +var _ = rql.ValuePredicate(&boolean{}) +var _ = rql.EntryPredicate(&boolean{}) +var _ = rql.EntrySchemaPredicate(&boolean{}) diff --git a/api/rql/internal/predicate/boolean_test.go b/api/rql/internal/predicate/boolean_test.go new file mode 100644 index 000000000..602b47e7b --- /dev/null +++ b/api/rql/internal/predicate/boolean_test.go @@ -0,0 +1,120 @@ +package predicate + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + apitypes "github.com/puppetlabs/wash/api/types" + "github.com/stretchr/testify/suite" +) + +type BooleanTestSuite struct { + asttest.Suite +} + +func (s *BooleanTestSuite) TestMarshal() { + s.MTC(Boolean(true), true) + s.MTC(Boolean(false), false) +} + +func (s *BooleanTestSuite) TestUnmarshal() { + b := Boolean(true) + s.UMETC(b, "foo", "formatted.*", true) + s.UMTC(b, true, Boolean(true)) + s.UMTC(b, false, Boolean(false)) +} + +func (s *BooleanTestSuite) TestValueInDomain() { + // Test true + b := Boolean(true) + s.VIDFTC(b, "foo") + s.VIDTTC(b, false) + + // Test false + b = Boolean(false) + s.VIDFTC(b, "foo") + s.VIDTTC(b, true) +} + +func (s *BooleanTestSuite) TestEvalValue() { + // Test true + b := Boolean(true) + s.EVFTC(b, false) + s.EVTTC(b, true) + + // Test false + b = Boolean(false) + s.EVFTC(b, true) + s.EVTTC(b, false) +} + +func (s *BooleanTestSuite) TestEntryInDomain() { + // Test true + b := Boolean(true) + s.EIDTTC(b, rql.Entry{}) + + // Test false + b = Boolean(false) + s.EIDTTC(b, rql.Entry{}) +} + +func (s *BooleanTestSuite) TestEvalEntry() { + // Test true + b := Boolean(true).(rql.EntryPredicate) + s.EETTC(b, rql.Entry{}) + + // Test false + b = Boolean(false).(rql.EntryPredicate) + s.EEFTC(b, rql.Entry{}) +} + +func (s *BooleanTestSuite) TestEntrySchemaInDomain() { + // Test true + b := Boolean(true) + s.ESIDTTC(b, &rql.EntrySchema{}) + + // Test false + b = Boolean(false) + s.ESIDTTC(b, &rql.EntrySchema{}) +} + +func (s *BooleanTestSuite) TestEvalEntrySchema() { + // Test true + b := Boolean(true).(rql.EntrySchemaPredicate) + s.EESTTC(b, &apitypes.EntrySchema{}) + + // Test false + b = Boolean(false).(rql.EntrySchemaPredicate) + s.EESFTC(b, &apitypes.EntrySchema{}) +} + +func (s *BooleanTestSuite) TestExpression_AtomAndNot() { + expr := expression.New("boolean", func() rql.ASTNode { + return Boolean(false) + }) + + s.MUM(expr, true) + s.EVFTC(expr, false, "foo") + s.EVTTC(expr, true) + s.EETTC(expr, rql.Entry{}) + s.EESTTC(expr, &rql.EntrySchema{}) + s.AssertNotImplemented( + expr, + asttest.StringPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", true}) + s.EVTTC(expr, false) + s.EVFTC(expr, true, "foo") + s.EEFTC(expr, rql.Entry{}) + s.EESFTC(expr, &rql.EntrySchema{}) +} + +func TestBoolean(t *testing.T) { + suite.Run(t, new(BooleanTestSuite)) +} From f6c019590f134f265bace497ea9e5ec3d4716eb9 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:23:43 -0800 Subject: [PATCH 08/49] Implement the null predicate Signed-off-by: Enis Inan --- api/rql/internal/predicate/null.go | 34 ++++++++++++++ api/rql/internal/predicate/null_test.go | 62 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 api/rql/internal/predicate/null.go create mode 100644 api/rql/internal/predicate/null_test.go diff --git a/api/rql/internal/predicate/null.go b/api/rql/internal/predicate/null.go new file mode 100644 index 000000000..d6d93e708 --- /dev/null +++ b/api/rql/internal/predicate/null.go @@ -0,0 +1,34 @@ +package predicate + +import ( + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" +) + +func Null() rql.ValuePredicate { + return &null{} +} + +type null struct{} + +func (p *null) Marshal() interface{} { + return nil +} + +func (p *null) Unmarshal(input interface{}) error { + if input != nil { + return errz.MatchErrorf("must be null") + } + return nil +} + +func (p *null) ValueInDomain(v interface{}) bool { + // Any value works with the null predicate + return true +} + +func (p *null) EvalValue(v interface{}) bool { + return v == nil +} + +var _ = rql.ValuePredicate(&null{}) diff --git a/api/rql/internal/predicate/null_test.go b/api/rql/internal/predicate/null_test.go new file mode 100644 index 000000000..494c8b2a0 --- /dev/null +++ b/api/rql/internal/predicate/null_test.go @@ -0,0 +1,62 @@ +package predicate + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + "github.com/stretchr/testify/suite" +) + +type NullTestSuite struct { + asttest.Suite +} + +func (s *NullTestSuite) TestMarshal() { + s.MTC(Null(), nil) +} + +func (s *NullTestSuite) TestUnmarshal() { + n := Null() + s.UMETC(n, "foo", ".*null", true) + s.UMTC(n, nil, Null()) +} + +func (s *NullTestSuite) TestValueInDomain() { + n := Null() + s.VIDTTC(n, "foo", 1) +} + +func (s *NullTestSuite) TestEvalValue() { + n := Null() + s.EVFTC(n, "foo", 1, true) + s.EVTTC(n, nil) +} + +func (s *NullTestSuite) TestExpression_AtomAndNot() { + expr := expression.New("null", func() rql.ASTNode { + return Null() + }) + + s.MUM(expr, nil) + s.EVFTC(expr, "foo", 1, true) + s.EVTTC(expr, nil) + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.StringPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", nil}) + s.EVTTC(expr, "foo", 1, true) + s.EVFTC(expr, nil) +} + +func TestNull(t *testing.T) { + suite.Run(t, new(NullTestSuite)) +} From 1c6964bb55a97fee47d4a3b6690f81d64c9846bb Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:24:01 -0800 Subject: [PATCH 09/49] Implement the numeric predicates NumericValue is needed for the meta primary Signed-off-by: Enis Inan --- api/rql/internal/predicate/comparisonOp.go | 21 +++ api/rql/internal/predicate/numeric.go | 136 +++++++++++++++++++ api/rql/internal/predicate/numeric_test.go | 145 +++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 api/rql/internal/predicate/comparisonOp.go create mode 100644 api/rql/internal/predicate/numeric.go create mode 100644 api/rql/internal/predicate/numeric_test.go diff --git a/api/rql/internal/predicate/comparisonOp.go b/api/rql/internal/predicate/comparisonOp.go new file mode 100644 index 000000000..a04e53b8e --- /dev/null +++ b/api/rql/internal/predicate/comparisonOp.go @@ -0,0 +1,21 @@ +package predicate + +type ComparisonOp string + +const ( + LT ComparisonOp = "<" + LTE ComparisonOp = "<=" + GT ComparisonOp = ">" + GTE ComparisonOp = ">=" + EQL ComparisonOp = "=" + NEQL ComparisonOp = "!=" +) + +var comparisonOpMap = map[ComparisonOp]bool{ + LT: true, + LTE: true, + GT: true, + GTE: true, + EQL: true, + NEQL: true, +} diff --git a/api/rql/internal/predicate/numeric.go b/api/rql/internal/predicate/numeric.go new file mode 100644 index 000000000..d4f4b47d5 --- /dev/null +++ b/api/rql/internal/predicate/numeric.go @@ -0,0 +1,136 @@ +package predicate + +import ( + "fmt" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" + "github.com/shopspring/decimal" +) + +func Numeric(op ComparisonOp, n decimal.Decimal) rql.NumericPredicate { + return &numeric{ + op: op, + n: n, + } +} + +func UnsignedNumeric(op ComparisonOp, n decimal.Decimal) rql.NumericPredicate { + p := Numeric(op, n).(*numeric) + p.unsigned = true + return p +} + +type numeric struct { + op ComparisonOp + n decimal.Decimal + unsigned bool +} + +func (p *numeric) Marshal() interface{} { + return []interface{}{string(p.op), p.n.String()} +} + +func (p *numeric) Unmarshal(input interface{}) error { + m := matcher.Array(func(v interface{}) bool { + opStr, ok := v.(string) + return ok && comparisonOpMap[ComparisonOp(opStr)] + }) + if !m(input) { + return errz.MatchErrorf("must be formatted as [, ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as [, ]") + } + if len(array) != 2 { + return fmt.Errorf("missing the number") + } + op := ComparisonOp(array[0].(string)) + var n decimal.Decimal + var err error + switch t := array[1].(type) { + case float64: + n = decimal.NewFromFloat(t) + case string: + n, err = decimal.NewFromString(t) + if err != nil { + return fmt.Errorf("failed to parse %v as a number: %w", t, err) + } + default: + return fmt.Errorf("%v is not a valid number", t) + } + p.op = op + if p.unsigned && n.LessThan(decimal.Zero) { + return fmt.Errorf("%v must be an unsigned (non-negative) number", n) + } + p.n = n + return nil +} + +func (p *numeric) EvalNumeric(n decimal.Decimal) bool { + switch p.op { + case LT: + return n.LessThan(p.n) + case LTE: + return n.LessThanOrEqual(p.n) + case GT: + return n.GreaterThan(p.n) + case GTE: + return n.GreaterThanOrEqual(p.n) + case EQL: + return n.Equal(p.n) + case NEQL: + return !n.Equal(p.n) + default: + // We should never hit this code path + panic(fmt.Sprintf("p.op (%v) is not a valid comparison operator", p.op)) + } +} + +var _ = rql.NumericPredicate(&numeric{}) + +func NumericValue(op ComparisonOp, n decimal.Decimal) rql.ValuePredicate { + return &numericValue{numeric{ + op: op, + n: n, + }} +} + +type numericValue struct { + numeric +} + +func (p *numericValue) Marshal() interface{} { + return []interface{}{"number", p.numeric.Marshal()} +} + +func (p *numericValue) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("number"))(input) { + return errz.MatchErrorf("must be formatted as ['number', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['number', ]") + } + if len(array) < 2 { + return fmt.Errorf("missing the numeric predicate") + } + if err := p.numeric.Unmarshal(array[1]); err != nil { + return fmt.Errorf("%w", err) + } + return nil +} + +func (p *numericValue) ValueInDomain(v interface{}) bool { + // TODO: Support stringified numbers? + _, ok := v.(float64) + return ok +} + +func (p *numericValue) EvalValue(v interface{}) bool { + return p.EvalNumeric(decimal.NewFromFloat(v.(float64))) +} + +var _ = rql.ValuePredicate(&numericValue{}) diff --git a/api/rql/internal/predicate/numeric_test.go b/api/rql/internal/predicate/numeric_test.go new file mode 100644 index 000000000..c395c7a79 --- /dev/null +++ b/api/rql/internal/predicate/numeric_test.go @@ -0,0 +1,145 @@ +package predicate + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + "github.com/stretchr/testify/suite" +) + +type NumericTestSuite struct { + asttest.Suite +} + +func (s *NumericTestSuite) TestNumeric_Marshal() { + s.MTC(Numeric(LT, s.N("2.3")), s.A("<", "2.3")) +} + +func (s *NumericTestSuite) TestNumeric_Unmarshal() { + n := Numeric("", s.N("0")) + s.UMETC(n, "foo", "formatted.*.*", true) + s.UMETC(n, s.A(), "formatted.*.*", true) + s.UMETC(n, s.A("<", "foo", "bar"), "formatted.*.*", false) + s.UMETC(n, s.A("<"), "missing.*number", false) + s.UMETC(n, s.A("<", true), "valid.*number", false) + s.UMETC(n, s.A("<", "true"), "parse.*true.*number.*exponent", false) + s.UMTC(n, s.A("<", 2.3), Numeric(LT, s.N("2.3"))) + s.UMTC(n, s.A("<", "2.3"), Numeric(LT, s.N("2.3"))) + // Test unmarshaling a very large value + largeV := "10000000000000000000000000000000000000000000000000000000000000000000" + s.UMTC(n, s.A("<", largeV), Numeric(LT, s.N(largeV))) +} + +func (s *NumericTestSuite) TestNumeric_UnmarshalUnsigned() { + n := UnsignedNumeric("", s.N("0")) + s.UMETC(n, s.A("<", "-10"), "unsigned.*number", false) +} + +func (s *NumericTestSuite) TestNumeric_EvalNumeric() { + // Test LT + n := Numeric(LT, s.N("1")) + s.ENFTC(n, s.N("2"), s.N("1")) + s.ENTTC(n, s.N("0")) + + // Test LTE + n = Numeric(LTE, s.N("1")) + s.ENFTC(n, s.N("2")) + s.ENTTC(n, s.N("0"), s.N("1")) + + // Test GT + n = Numeric(GT, s.N("1")) + s.ENFTC(n, s.N("0"), s.N("1")) + s.ENTTC(n, s.N("2")) + + // Test GTE + n = Numeric(GTE, s.N("1")) + s.ENFTC(n, s.N("0")) + s.ENTTC(n, s.N("2"), s.N("1")) + + // Test EQL + n = Numeric(EQL, s.N("1")) + s.ENFTC(n, s.N("0"), s.N("2")) + s.ENTTC(n, s.N("1")) + + // TEST NEQL + n = Numeric(NEQL, s.N("1")) + s.ENFTC(n, s.N("1")) + s.ENTTC(n, s.N("0"), s.N("2")) +} + +func (s *NumericTestSuite) TestNumeric_Expression_AtomAndNot() { + expr := expression.New("numeric", func() rql.ASTNode { + return Numeric("", s.N("0")) + }) + + s.MUM(expr, []interface{}{"<", "1"}) + s.ENFTC(expr, s.N("1")) + s.ENTTC(expr, s.N("0")) + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.ValuePredicateC, + asttest.StringPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", []interface{}{"<", "1"}}) + s.ENTTC(expr, s.N("1")) + s.ENFTC(expr, s.N("0")) +} + +func (s *NumericTestSuite) TestNumericValue_Marshal() { + s.MTC(NumericValue(LT, s.N("2.3")), s.A("number", s.A("<", "2.3"))) +} + +func (s *NumericTestSuite) TestNumericValue_Unmarshal() { + n := NumericValue("", s.N("0")) + s.UMETC(n, "foo", "formatted.*number.*", true) + s.UMETC(n, s.A("number", "foo", "bar"), "formatted.*number.*", false) + s.UMETC(n, s.A("number"), "missing.*numeric.*predicate", false) + s.UMETC(n, s.A("number", s.A()), "formatted.*.*", false) + s.UMTC(n, s.A("number", s.A("<", "2.3")), NumericValue(LT, s.N("2.3"))) +} + +func (s *NumericTestSuite) TestNumericValue_ValueInDomain() { + n := NumericValue(LT, s.N("2.0")) + s.VIDFTC(n, "bar", "123456") + s.VIDTTC(n, float64(10)) +} + +func (s *NumericTestSuite) TestNumericValue_EvalValue() { + n := NumericValue(LT, s.N("2.0")) + s.EVFTC(n, float64(3)) + s.EVTTC(n, float64(1)) + // TestEvalNumeric contained the operator-specific test-cases +} + +func (s *NumericTestSuite) TestNumericValue_Expression_AtomAndNot() { + expr := expression.New("numeric", func() rql.ASTNode { + return NumericValue("", s.N("0")) + }) + + s.MUM(expr, []interface{}{"number", []interface{}{"<", "1"}}) + s.EVFTC(expr, float64(1), "1") + s.EVTTC(expr, float64(0)) + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.StringPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", []interface{}{"number", []interface{}{"<", "1"}}}) + s.EVTTC(expr, float64(1)) + s.EVFTC(expr, float64(0), "1") +} + +func TestNumeric(t *testing.T) { + suite.Run(t, new(NumericTestSuite)) +} From 17ad818acccf135bf46ec02944db675c4fbfef14 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:24:45 -0800 Subject: [PATCH 10/49] Implement the size predicate This acts on an entry's size attribute and, for a metadata value, on the number of elements in the object/array Signed-off-by: Enis Inan --- api/rql/internal/predicate/size.go | 81 +++++++++++++++++++ api/rql/internal/predicate/size_test.go | 103 ++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 api/rql/internal/predicate/size.go create mode 100644 api/rql/internal/predicate/size_test.go diff --git a/api/rql/internal/predicate/size.go b/api/rql/internal/predicate/size.go new file mode 100644 index 000000000..c9d621ac4 --- /dev/null +++ b/api/rql/internal/predicate/size.go @@ -0,0 +1,81 @@ +package predicate + +import ( + "fmt" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" + "github.com/shopspring/decimal" +) + +// As a value predicate, Size is a predicate on the size +// of an object/array. As an entry predicate, Size is a +// predicate on the entry's size attribute. +func Size(p rql.NumericPredicate) rql.ValuePredicate { + return &size{ + p: p, + } +} + +type size struct { + p rql.NumericPredicate +} + +func (p *size) Marshal() interface{} { + return []interface{}{"size", p.p.Marshal()} +} + +func (p *size) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("size"))(input) { + return errz.MatchErrorf("must be formatted as ['size', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['size', ]") + } + if len(array) < 2 { + return fmt.Errorf("missing the numeric predicate expression") + } + if err := p.p.Unmarshal(array[1]); err != nil { + return fmt.Errorf("%w", err) + } + return nil +} + +func (p *size) ValueInDomain(v interface{}) bool { + switch v.(type) { + case map[string]interface{}: + return true + case []interface{}: + return true + default: + return false + } +} + +func (p *size) EvalValue(v interface{}) bool { + switch t := v.(type) { + case map[string]interface{}: + return p.p.EvalNumeric(decimal.NewFromInt(int64(len(t)))) + case []interface{}: + return p.p.EvalNumeric(decimal.NewFromInt(int64(len(t)))) + default: + panic("sizePredicate: EvalValue called with an invalid value") + } +} + +func (p *size) EntryInDomain(rql.Entry) bool { + return true +} + +func (p *size) EvalEntry(e rql.Entry) bool { + return p.p.EvalNumeric(decimal.NewFromInt(int64(e.Attributes.Size()))) +} + +func (p *size) EntrySchemaInDomain(*rql.EntrySchema) bool { + return true +} + +var _ = rql.ValuePredicate(&size{}) +var _ = rql.EntryPredicate(&size{}) diff --git a/api/rql/internal/predicate/size_test.go b/api/rql/internal/predicate/size_test.go new file mode 100644 index 000000000..ad69fdb72 --- /dev/null +++ b/api/rql/internal/predicate/size_test.go @@ -0,0 +1,103 @@ +package predicate + +import ( + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + "github.com/stretchr/testify/suite" +) + +type SizeTestSuite struct { + asttest.Suite +} + +func (s *SizeTestSuite) TestMarshal() { + s.MTC(Size(UnsignedNumeric(LT, s.N("10"))), s.A("size", s.A("<", "10"))) +} + +func (s *SizeTestSuite) TestUnmarshal() { + p := Size(UnsignedNumeric("", s.N("0"))) + s.UMETC(p, "foo", "formatted.*'size'.*", true) + s.UMETC(p, s.A("foo"), "formatted.*'size'.*", true) + s.UMETC(p, s.A("size", "foo", "bar"), "formatted.*'size'.*", false) + s.UMETC(p, s.A("size"), "missing.*predicate.*expression", false) + s.UMETC(p, s.A("size", s.A("<", true)), "valid.*number", false) + s.UMETC(p, s.A("size", s.A("<", "-10")), "unsigned.*number", false) + s.UMTC(p, s.A("size", s.A("<", "10")), Size(UnsignedNumeric(LT, s.N("10")))) +} + +func (s *SizeTestSuite) TestValueInDomain() { + p := Size(UnsignedNumeric(GT, s.N("0"))) + s.VIDFTC(p, "foo", true) + s.VIDTTC(p, map[string]interface{}{}, []interface{}{}) +} + +func (s *SizeTestSuite) EvalValue() { + p := Size(UnsignedNumeric(GT, s.N("0"))) + s.EVFTC(p, map[string]interface{}{}, []interface{}{}) + s.EVTTC(p, map[string]interface{}{"foo": "bar"}, []interface{}{"foo"}) +} + +func (s *SizeTestSuite) TestEntryInDomain() { + p := Size(UnsignedNumeric(GT, s.N("0"))) + s.EIDTTC(p, rql.Entry{}) +} + +func (s *SizeTestSuite) TestEvalEntry() { + p := Size(UnsignedNumeric(GT, s.N("0"))) + e := rql.Entry{} + e.Attributes.SetSize(uint64(0)) + s.EEFTC(p, e) + e.Attributes.SetSize(uint64(1)) + s.EETTC(p, e) +} + +func (s *SizeTestSuite) TestEntrySchemaInDomain() { + p := Size(UnsignedNumeric(GT, s.N("0"))) + s.ESIDTTC(p, &rql.EntrySchema{}) +} + +func (s *SizeTestSuite) TestExpression_AtomAndNot() { + expr := expression.New("size", func() rql.ASTNode { + return Size(UnsignedNumeric("", s.N("0"))) + }) + + s.MUM(expr, []interface{}{"size", []interface{}{">", "0"}}) + s.EVFTC(expr, map[string]interface{}{}, []interface{}{}, "foo") + s.EVTTC(expr, map[string]interface{}{"foo": "bar"}, []interface{}{"foo"}) + + e := rql.Entry{} + e.Attributes.SetSize(uint64(0)) + s.EEFTC(expr, e) + e.Attributes.SetSize(uint64(1)) + s.EETTC(expr, e) + + schema := &rql.EntrySchema{} + s.EESTTC(expr, schema) + + s.AssertNotImplemented( + expr, + asttest.StringPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + // Test Not + s.MUM(expr, []interface{}{"NOT", []interface{}{"size", []interface{}{">", "0"}}}) + s.EVTTC(expr, map[string]interface{}{}, []interface{}{}) + s.EVFTC(expr, map[string]interface{}{"foo": "bar"}, []interface{}{"foo"}, "foo") + + e.Attributes.SetSize(uint64(0)) + s.EETTC(expr, e) + e.Attributes.SetSize(uint64(1)) + s.EEFTC(expr, e) + + s.EESTTC(expr, schema) +} + +func TestSize(t *testing.T) { + suite.Run(t, new(SizeTestSuite)) +} From ef5d02318264a5eb09cf2bf5cd154e294ca87c9f Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:27:43 -0800 Subject: [PATCH 11/49] Implement the string predicate StringValue's necessary for the meta primary Signed-off-by: Enis Inan --- api/rql/internal/predicate/string.go | 234 ++++++++++++++++++++++ api/rql/internal/predicate/string_test.go | 181 +++++++++++++++++ 2 files changed, 415 insertions(+) create mode 100644 api/rql/internal/predicate/string.go create mode 100644 api/rql/internal/predicate/string_test.go diff --git a/api/rql/internal/predicate/string.go b/api/rql/internal/predicate/string.go new file mode 100644 index 000000000..44150cd09 --- /dev/null +++ b/api/rql/internal/predicate/string.go @@ -0,0 +1,234 @@ +package predicate + +import ( + "fmt" + "regexp" + + "github.com/gobwas/glob" + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" +) + +/* +These are the individual string predicates +*/ + +func StringGlob(g string) rql.StringPredicate { + return &stringGlob{ + gStr: g, + g: glob.MustCompile(g), + } +} + +type stringGlob struct { + gStr string + g glob.Glob +} + +func (p *stringGlob) Marshal() interface{} { + return []interface{}{"glob", p.gStr} +} + +func (p *stringGlob) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("glob"))(input) { + return errz.MatchErrorf("must be formatted as ['glob', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['glob', ]") + } + if len(array) < 2 { + return fmt.Errorf("missing the glob") + } + globStr, ok := array[1].(string) + if !ok { + return fmt.Errorf("glob must be a string") + } + g, err := glob.Compile(globStr) + if err != nil { + return fmt.Errorf("invalid glob %v: %w", globStr, err) + } + p.gStr = globStr + p.g = g + return nil +} + +func (p *stringGlob) EvalString(str string) bool { + return p.g.Match(str) +} + +var _ = rql.StringPredicate(&stringGlob{}) + +func StringRegex(r *regexp.Regexp) rql.StringPredicate { + return &stringRegex{ + r: r, + } +} + +type stringRegex struct { + r *regexp.Regexp +} + +func (p *stringRegex) Marshal() interface{} { + return []interface{}{"regex", p.r.String()} +} + +func (p *stringRegex) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("regex"))(input) { + return errz.MatchErrorf("must be formatted as ['regex', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['regex', ]") + } + if len(array) < 2 { + return fmt.Errorf("missing the regex") + } + regexStr, ok := array[1].(string) + if !ok { + return fmt.Errorf("regex must be a string") + } + r, err := regexp.Compile(regexStr) + if err != nil { + return fmt.Errorf("invalid regex %v: %w", regexStr, err) + } + p.r = r + return nil +} + +func (p *stringRegex) EvalString(str string) bool { + return p.r.MatchString(str) +} + +var _ = rql.StringPredicate(&stringRegex{}) + +func StringEqual(s string) rql.StringPredicate { + return &stringEqual{ + s: s, + } +} + +type stringEqual struct { + s string +} + +func (p *stringEqual) Marshal() interface{} { + return []interface{}{EQL, p.s} +} + +func (p *stringEqual) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value(EQL))(input) { + return errz.MatchErrorf("must be formatted as ['%v', ]", EQL) + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['%v', ]", EQL) + } + if len(array) < 2 { + return fmt.Errorf("missing the string") + } + s, ok := array[1].(string) + if !ok { + return fmt.Errorf("must provide a string") + } + p.s = s + return nil +} + +func (p *stringEqual) EvalString(str string) bool { + return str == p.s +} + +var _ = rql.StringPredicate(&stringEqual{}) + +/* +This is the main string predicate type +*/ + +type stringP struct { + internal.NonterminalNode +} + +func String() rql.StringPredicate { + p := &stringP{ + NonterminalNode: internal.NewNonterminalNode( + StringGlob(""), + StringRegex(nil), + StringEqual(""), + ), + } + p.SetMatchErrMsg("must be formatted as either ['glob', ], ['regex', ], or ['=', ]") + return p +} + +func (p *stringP) EvalString(str string) bool { + return p.MatchedNode().(rql.StringPredicate).EvalString(str) +} + +/* +This is the string predicate type that's also a value predicate. We make +it take an rql.StringPredicate instead of stringP so that it can be used +by parsers +*/ + +type stringValue struct { + rql.StringPredicate +} + +func (p *stringValue) Marshal() interface{} { + return []interface{}{"string", p.StringPredicate.Marshal()} +} + +func (p *stringValue) Unmarshal(input interface{}) error { + if !matcher.Array(matcher.Value("string"))(input) { + return errz.MatchErrorf("must be formatted as ['string', ]") + } + array := input.([]interface{}) + if len(array) > 2 { + return fmt.Errorf("must be formatted as ['string', ]") + } + if len(array) < 2 { + return fmt.Errorf("missing the string predicate") + } + if err := p.StringPredicate.Unmarshal(array[1]); err != nil { + return fmt.Errorf("%w", err) + } + return nil +} + +func (p *stringValue) ValueInDomain(v interface{}) bool { + _, ok := v.(string) + return ok +} + +func (p *stringValue) EvalValue(v interface{}) bool { + return p.EvalString(v.(string)) +} + +func StringValue() rql.ValuePredicate { + return &stringValue{ + String(), + } +} + +func StringValueGlob(g string) rql.ValuePredicate { + return &stringValue{ + StringGlob(g), + } +} + +func StringValueRegex(r *regexp.Regexp) rql.ValuePredicate { + return &stringValue{ + StringRegex(r), + } +} + +func StringValueEqual(str string) rql.ValuePredicate { + return &stringValue{ + StringEqual(str), + } +} + +var _ = rql.ValuePredicate(&stringValue{}) diff --git a/api/rql/internal/predicate/string_test.go b/api/rql/internal/predicate/string_test.go new file mode 100644 index 000000000..60927c100 --- /dev/null +++ b/api/rql/internal/predicate/string_test.go @@ -0,0 +1,181 @@ +package predicate + +import ( + "regexp" + "testing" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/ast/asttest" + "github.com/puppetlabs/wash/api/rql/internal" + "github.com/puppetlabs/wash/api/rql/internal/predicate/expression" + "github.com/stretchr/testify/suite" +) + +type StringTestSuite struct { + asttest.Suite +} + +func (s *StringTestSuite) TestGlob_Marshal() { + s.MTC(StringGlob("foo"), s.A("glob", "foo")) +} + +func (s *StringTestSuite) TestGlob_Unmarshal() { + g := StringGlob("") + s.UMETC(g, "foo", "formatted.*'glob'.*", true) + s.UMETC(g, s.A("foo"), "formatted.*'glob'.*", true) + s.UMETC(g, s.A("glob", "foo", "bar"), "formatted.*'glob'.*", false) + s.UMETC(g, s.A("glob"), "missing.*glob", false) + s.UMETC(g, s.A("glob", 1), "glob.*string", false) + s.UMETC(g, s.A("glob", "["), "invalid.*glob.*[.*closing.*]", false) + s.UMTC(g, s.A("glob", "foo"), StringGlob("foo")) +} + +func (s *StringTestSuite) TestGlob_EvalString() { + g := StringGlob("foo") + s.ESFTC(g, "bar") + s.ESTTC(g, "foo") +} + +func (s *StringTestSuite) TestRegex_Marshal() { + s.MTC(StringRegex(regexp.MustCompile("foo")), s.A("regex", "foo")) +} + +func (s *StringTestSuite) TestRegex_Unmarshal() { + r := StringRegex(nil) + s.UMETC(r, "foo", "formatted.*'regex'.*", true) + s.UMETC(r, s.A("foo"), "formatted.*'regex'.*", true) + s.UMETC(r, s.A("regex", "foo", "bar"), "formatted.*'regex'.*", false) + s.UMETC(r, s.A("regex"), "missing.*regex", false) + s.UMETC(r, s.A("regex", 1), "regex.*string", false) + s.UMETC(r, s.A("regex", "["), "invalid.*regex.*[.*closing.*]", false) + s.UMTC(r, s.A("regex", "foo"), StringRegex(regexp.MustCompile("foo"))) +} + +func (s *StringTestSuite) TestRegex_EvalString() { + r := StringRegex(regexp.MustCompile("foo")) + s.ESFTC(r, "bar") + s.ESTTC(r, "foo") +} + +func (s *StringTestSuite) TestEqual_Marshal() { + s.MTC(StringEqual("foo"), s.A("=", "foo")) +} + +func (s *StringTestSuite) TestEqual_Unmarshal() { + e := StringEqual("") + s.UMETC(e, "foo", "formatted.*'='.*", true) + s.UMETC(e, s.A("foo"), "formatted.*'='.*", true) + s.UMETC(e, s.A("=", "foo", "bar"), "formatted.*'='.*", false) + s.UMETC(e, s.A("="), "missing.*string", false) + s.UMETC(e, s.A("=", 1), "string", false) + s.UMTC(e, s.A("=", "foo"), StringEqual("foo")) +} + +func (s *StringTestSuite) TestEqual_EvalString() { + e := StringEqual("foo") + s.ESFTC(e, "bar") + s.ESTTC(e, "foo") +} + +func (s *StringTestSuite) TestString_Marshal() { + p := String().(internal.NonterminalNode) + p.SetMatchedNode(StringGlob("foo")) + s.MTC(p, StringGlob("foo").Marshal()) +} + +func (s *StringTestSuite) TestString_Unmarshal() { + p := String() + s.UMETC(p, "foo", "formatted.*'glob'.*'regex'.*'='", true) + s.UMETC(p, s.A("glob", "["), "invalid.*glob", false) + s.UMETC(p, s.A("regex", "["), "invalid.*regex", false) + s.UMETC(p, s.A("=", true), "string", false) + + s.UMTC(p, s.A("glob", "foo"), StringGlob("foo")) + s.UMTC(p, s.A("regex", "foo"), StringRegex(regexp.MustCompile("foo"))) + s.UMTC(p, s.A("=", "foo"), StringEqual("foo")) +} + +func (s *StringTestSuite) TestString_EvalString() { + p := String().(internal.NonterminalNode) + p.SetMatchedNode(StringGlob("foo")) + s.ESFTC(p, "bar") + s.ESTTC(p, "foo") +} + +func (s *StringTestSuite) TestString_Expression_AtomAndNot() { + expr := expression.New("string", func() rql.ASTNode { + return String() + }) + + for _, ptype := range []string{"glob", "regex", "="} { + s.MUM(expr, []interface{}{ptype, "foo"}) + s.ESFTC(expr, "bar") + s.ESTTC(expr, "foo") + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", []interface{}{ptype, "foo"}}) + s.ESTTC(expr, "bar") + s.ESFTC(expr, "foo") + } +} + +func (s *StringTestSuite) TestStringValue_Marshal() { + // This also tests that the StringValue* methods do the right thing + s.MTC(StringValueGlob("foo"), s.A("string", s.A("glob", "foo"))) + s.MTC(StringValueRegex(regexp.MustCompile("foo")), s.A("string", s.A("regex", "foo"))) + s.MTC(StringValueEqual("foo"), s.A("string", s.A(EQL, "foo"))) +} + +func (s *StringTestSuite) TestStringValue_Unmarshal() { + g := StringValueGlob("") + s.UMETC(g, "foo", "formatted.*'string'.*", true) + s.UMETC(g, s.A("string", "foo", "bar"), "formatted.*'string'.*", false) + s.UMETC(g, s.A("string"), "missing.*string.*predicate", false) + s.UMETC(g, s.A("string", s.A()), "formatted.*'glob'.*", false) + s.UMTC(g, s.A("string", s.A("glob", "foo")), StringValueGlob("foo")) +} + +func (s *StringTestSuite) TestStringValue_ValueInDomain() { + g := StringValueGlob("foo") + s.VIDFTC(g, 1) + s.VIDTTC(g, "bar") +} + +func (s *StringTestSuite) TestStringValue_EvalValue() { + g := StringValueGlob("foo") + s.EVFTC(g, "bar") + s.EVTTC(g, "foo") +} + +func (s *StringTestSuite) TestStringValue_AtomAndNot() { + expr := expression.New("string", func() rql.ASTNode { + return StringValue() + }) + + s.MUM(expr, []interface{}{"string", []interface{}{"glob", "foo"}}) + s.EVFTC(expr, "bar", 1) + s.EVTTC(expr, "foo") + s.AssertNotImplemented( + expr, + asttest.EntryPredicateC, + asttest.EntrySchemaPredicateC, + asttest.NumericPredicateC, + asttest.TimePredicateC, + asttest.ActionPredicateC, + ) + + s.MUM(expr, []interface{}{"NOT", []interface{}{"string", []interface{}{"glob", "foo"}}}) + s.EVTTC(expr, "bar") + s.EVFTC(expr, "foo", 1) +} + +func TestString(t *testing.T) { + suite.Run(t, new(StringTestSuite)) +} From 2a47cf1cd28de71c2fc3f6b716f71eebfb505907 Mon Sep 17 00:00:00 2001 From: Enis Inan Date: Thu, 30 Jan 2020 02:28:21 -0800 Subject: [PATCH 12/49] Implement the time predicate TimeValue's needed for the meta primary Signed-off-by: Enis Inan --- api/rql/internal/predicate/time.go | 121 ++++++++++++++++++++++ api/rql/internal/predicate/time_test.go | 132 ++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 api/rql/internal/predicate/time.go create mode 100644 api/rql/internal/predicate/time_test.go diff --git a/api/rql/internal/predicate/time.go b/api/rql/internal/predicate/time.go new file mode 100644 index 000000000..efd9f1a3f --- /dev/null +++ b/api/rql/internal/predicate/time.go @@ -0,0 +1,121 @@ +package predicate + +import ( + "fmt" + "time" + + "github.com/puppetlabs/wash/api/rql" + "github.com/puppetlabs/wash/api/rql/internal/errz" + "github.com/puppetlabs/wash/api/rql/internal/matcher" + "github.com/puppetlabs/wash/munge" +) + +func Time(op ComparisonOp, t time.Time) rql.TimePredicate { + return &tm{ + op: op, + t: t, + } +} + +// tm => time. Have to name it as such to avoid conflicting with +// the 'time' package +type tm struct { + op ComparisonOp + t time.Time +} + +func (p *tm) Marshal() interface{} { + return []interface{}{string(p.op), p.t} +} + +func (p *tm) Unmarshal(input interface{}) error { + m := matcher.Array(func(v interface{}) bool { + opStr, ok := v.(string) + return ok && comparisonOpMap[ComparisonOp(opStr)] + }) + if !m(input) { + return errz.MatchErrorf("must be formatted as [,