cel 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +23 -1
- data/lib/cel/ast/elements/protobuf.rb +411 -66
- data/lib/cel/ast/elements.rb +339 -113
- data/lib/cel/ast/types.rb +151 -15
- data/lib/cel/checker.rb +133 -30
- data/lib/cel/context.rb +55 -6
- data/lib/cel/encoder.rb +31 -13
- data/lib/cel/environment.rb +58 -13
- data/lib/cel/errors.rb +19 -1
- data/lib/cel/macro.rb +91 -26
- data/lib/cel/parser.rb +428 -396
- data/lib/cel/program.rb +255 -48
- data/lib/cel/version.rb +1 -1
- data/lib/cel.rb +232 -1
- metadata +18 -9
- data/lib/cel/parser.tab.rb +0 -1023
data/lib/cel/environment.rb
CHANGED
@@ -2,10 +2,33 @@
|
|
2
2
|
|
3
3
|
module Cel
|
4
4
|
class Environment
|
5
|
-
|
5
|
+
attr_reader :declarations, :package
|
6
|
+
|
7
|
+
def initialize(declarations: nil, container: nil, disable_check: false)
|
8
|
+
@disable_check = disable_check
|
6
9
|
@declarations = declarations
|
7
|
-
@
|
8
|
-
@
|
10
|
+
@container = container
|
11
|
+
@package = nil
|
12
|
+
@checker = Checker.new(self)
|
13
|
+
|
14
|
+
if container && !container.empty?
|
15
|
+
|
16
|
+
module_dir = container.split(".")
|
17
|
+
|
18
|
+
Dir[File.join(*module_dir, "*.rb")].each do |path|
|
19
|
+
require path
|
20
|
+
end
|
21
|
+
|
22
|
+
proto_module_name = container.split(".").map do |sub|
|
23
|
+
sub.capitalize.gsub(/_([a-z\d]*)/) do
|
24
|
+
::Regexp.last_match(1).capitalize
|
25
|
+
end
|
26
|
+
end.join("::")
|
27
|
+
|
28
|
+
@package = Object.const_get(proto_module_name) if Object.const_defined?(proto_module_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
@parser = Parser.new(package)
|
9
32
|
end
|
10
33
|
|
11
34
|
def compile(expr)
|
@@ -20,7 +43,7 @@ module Cel
|
|
20
43
|
|
21
44
|
def decode(encoded_expr)
|
22
45
|
ast = Encoder.decode(encoded_expr)
|
23
|
-
@checker.check(ast)
|
46
|
+
@checker.check(ast) unless @disable_check
|
24
47
|
ast
|
25
48
|
end
|
26
49
|
|
@@ -31,15 +54,36 @@ module Cel
|
|
31
54
|
|
32
55
|
def program(expr)
|
33
56
|
expr = @parser.parse(expr) if expr.is_a?(::String)
|
34
|
-
@checker.check(expr)
|
35
|
-
Runner.new(
|
57
|
+
@checker.check(expr) unless @disable_check
|
58
|
+
Runner.new(self, expr)
|
36
59
|
end
|
37
60
|
|
38
61
|
def evaluate(expr, bindings = nil)
|
39
|
-
|
62
|
+
declarations, bindings = process_bindings(bindings)
|
63
|
+
context = Context.new(declarations, bindings)
|
40
64
|
expr = @parser.parse(expr) if expr.is_a?(::String)
|
41
|
-
@checker.check(expr)
|
42
|
-
Program.new(context).evaluate(expr)
|
65
|
+
@checker.check(expr) unless @disable_check
|
66
|
+
Program.new(context, self).evaluate(expr)
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_bindings(bindings)
|
70
|
+
return [@declarations, bindings] unless @container && @package.nil?
|
71
|
+
|
72
|
+
declarations = @declarations.dup
|
73
|
+
bindings = bindings.dup
|
74
|
+
|
75
|
+
prefix = "#{@container}."
|
76
|
+
|
77
|
+
cbinding_keys = bindings.keys.select { |k| k.start_with?(prefix) }
|
78
|
+
|
79
|
+
cbinding_keys.each do |k|
|
80
|
+
cbinding = k.to_s.delete_prefix(prefix).to_sym
|
81
|
+
|
82
|
+
bindings[cbinding] = bindings.delete(k)
|
83
|
+
declarations[cbinding] = declarations.delete(k) if declarations.key?(k)
|
84
|
+
end
|
85
|
+
|
86
|
+
[declarations, bindings]
|
43
87
|
end
|
44
88
|
|
45
89
|
private
|
@@ -48,14 +92,15 @@ module Cel
|
|
48
92
|
end
|
49
93
|
|
50
94
|
class Runner
|
51
|
-
def initialize(
|
52
|
-
@
|
95
|
+
def initialize(environment, ast)
|
96
|
+
@environment = environment
|
53
97
|
@ast = ast
|
54
98
|
end
|
55
99
|
|
56
100
|
def evaluate(bindings = nil)
|
57
|
-
|
58
|
-
|
101
|
+
declarations, bindings = @environment.process_bindings(bindings)
|
102
|
+
context = Context.new(declarations, bindings)
|
103
|
+
Program.new(context, @environment).evaluate(@ast)
|
59
104
|
end
|
60
105
|
end
|
61
106
|
end
|
data/lib/cel/errors.rb
CHANGED
@@ -9,6 +9,24 @@ module Cel
|
|
9
9
|
|
10
10
|
class EvaluateError < Error; end
|
11
11
|
|
12
|
+
class InvalidArgumentError < EvaluateError
|
13
|
+
attr_reader :code
|
14
|
+
|
15
|
+
def initialize(key)
|
16
|
+
super("invalid argument #{key}")
|
17
|
+
@code = :invalid_argument
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NoSuchKeyError < EvaluateError
|
22
|
+
attr_reader :code
|
23
|
+
|
24
|
+
def initialize(var, attrib)
|
25
|
+
super("No such key: #{attrib} in #{var}")
|
26
|
+
@code = :no_such_key
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
12
30
|
class NoSuchFieldError < EvaluateError
|
13
31
|
attr_reader :code
|
14
32
|
|
@@ -18,7 +36,7 @@ module Cel
|
|
18
36
|
end
|
19
37
|
end
|
20
38
|
|
21
|
-
class NoMatchingOverloadError <
|
39
|
+
class NoMatchingOverloadError < EvaluateError
|
22
40
|
attr_reader :code
|
23
41
|
|
24
42
|
def initialize(op)
|
data/lib/cel/macro.rb
CHANGED
@@ -12,66 +12,131 @@ module Cel
|
|
12
12
|
# If f is a oneof or singular message field, has(e.f) indicates whether the field is set.
|
13
13
|
# If f is some other singular field, has(e.f) indicates whether the field's value is its default
|
14
14
|
# value (zero for numeric fields, false for booleans, empty for strings and bytes).
|
15
|
-
def has(invoke)
|
15
|
+
def has(invoke, program:)
|
16
16
|
var = invoke.var
|
17
17
|
func = invoke.func
|
18
18
|
|
19
19
|
case var
|
20
20
|
when Message
|
21
|
+
var = program.evaluate(var)
|
21
22
|
# If e evaluates to a message and f is not a declared field for the message,
|
22
23
|
# has(e.f) raises a no_such_field error.
|
23
|
-
raise NoSuchFieldError.new(var, func) unless var.
|
24
|
+
raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
|
24
25
|
|
25
|
-
|
26
|
+
value = var.public_send(func)
|
27
|
+
field = var.class.descriptor.lookup(func.to_s)
|
28
|
+
|
29
|
+
if field.label == :repeated
|
30
|
+
# If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
|
31
|
+
Bool.cast(field.get(var).size.positive?)
|
32
|
+
elsif field.has_presence?
|
33
|
+
# If f is a oneof or singular message field, has(e.f) indicates whether the field is set.
|
34
|
+
Bool.cast(field.has?(var))
|
35
|
+
else
|
36
|
+
# If f is some other singular field, has(e.f) indicates whether the field's value is its
|
37
|
+
# default value (zero for numeric fields, false for booleans, empty for strings and bytes).
|
38
|
+
value = field.get(var)
|
39
|
+
case field.type
|
40
|
+
when :bool
|
41
|
+
Bool.cast(value == true)
|
42
|
+
when :string, :bytes
|
43
|
+
Bool.cast(!value.empty?)
|
44
|
+
when :enum
|
45
|
+
Bool.cast(value != field.default)
|
46
|
+
else
|
47
|
+
Bool.cast(value != 0)
|
48
|
+
end
|
49
|
+
end
|
26
50
|
when Map
|
27
51
|
# If e evaluates to a map, then has(e.f) indicates whether the string f
|
28
52
|
# is a key in the map (note that f must syntactically be an identifier).
|
29
|
-
Bool.
|
53
|
+
Bool.cast(var.respond_to?(func))
|
30
54
|
else
|
31
55
|
# In all other cases, has(e.f) evaluates to an error.
|
32
56
|
raise EvaluateError, "#{invoke} is not supported"
|
33
57
|
end
|
34
58
|
end
|
35
59
|
|
36
|
-
def size(literal)
|
37
|
-
literal.
|
60
|
+
def size(literal, program: nil)
|
61
|
+
literal = program.evaluate(literal) if program
|
62
|
+
Cel::Number.new(:int, program.evaluate(literal).size)
|
38
63
|
end
|
39
64
|
|
40
|
-
def matches(string, pattern)
|
65
|
+
def matches(string, pattern, program: nil)
|
66
|
+
pattern = program.evaluate(pattern) if program
|
41
67
|
pattern = Regexp.new(pattern)
|
42
|
-
Bool.
|
68
|
+
Bool.cast(pattern.match?(string))
|
43
69
|
end
|
44
70
|
|
45
|
-
def all(collection,
|
46
|
-
|
47
|
-
|
71
|
+
def all(collection, *identifiers, predicate, program:)
|
72
|
+
identifiers = identifiers.map(&:to_sym)
|
73
|
+
error = nil
|
74
|
+
|
75
|
+
return_value = with_context(collection, identifiers).map do |context|
|
76
|
+
program.with_extra_context(context).evaluate(predicate).value
|
77
|
+
rescue StandardError => e
|
78
|
+
error = e
|
48
79
|
end
|
49
|
-
|
80
|
+
|
81
|
+
has_false = return_value.include?(false)
|
82
|
+
|
83
|
+
# if any predicate evaluates to false, the macro evaluates to false, ignoring any errors
|
84
|
+
# from other predicates.
|
85
|
+
raise error if error && !has_false
|
86
|
+
|
87
|
+
Bool.cast(!has_false)
|
50
88
|
end
|
51
89
|
|
52
|
-
def exists(collection,
|
53
|
-
|
54
|
-
|
90
|
+
def exists(collection, *identifiers, predicate, program:)
|
91
|
+
identifiers = identifiers.map(&:to_sym)
|
92
|
+
|
93
|
+
return_value = with_context(collection, identifiers).any? do |context|
|
94
|
+
program.with_extra_context(context).evaluate(predicate).value
|
55
95
|
end
|
56
|
-
Bool.
|
96
|
+
Bool.cast(return_value)
|
57
97
|
end
|
58
98
|
|
59
|
-
def exists_one(collection,
|
60
|
-
|
61
|
-
|
99
|
+
def exists_one(collection, *identifiers, predicate, program:)
|
100
|
+
identifiers = identifiers.map(&:to_sym)
|
101
|
+
|
102
|
+
# This macro does not short-circuit in order to remain consistent with logical operators
|
103
|
+
# being the only operators which can absorb errors within CEL.
|
104
|
+
return_value = with_context(collection, identifiers).select do |context|
|
105
|
+
program.with_extra_context(context).evaluate(predicate).value
|
62
106
|
end
|
63
|
-
Bool.
|
107
|
+
Bool.cast(return_value.size == 1)
|
64
108
|
end
|
65
109
|
|
66
|
-
def filter(collection,
|
67
|
-
|
68
|
-
|
110
|
+
def filter(collection, *identifiers, predicate, program:)
|
111
|
+
identifiers = identifiers.map(&:to_sym)
|
112
|
+
|
113
|
+
return_value = with_context(collection, identifiers).filter_map do |context|
|
114
|
+
next unless program.with_extra_context(context).evaluate(predicate).value
|
115
|
+
|
116
|
+
context.values.last
|
69
117
|
end
|
118
|
+
List.new(return_value)
|
70
119
|
end
|
71
120
|
|
72
|
-
def map(collection,
|
73
|
-
|
74
|
-
|
121
|
+
def map(collection, *identifiers, predicate, program:)
|
122
|
+
identifiers = identifiers.map(&:to_sym)
|
123
|
+
|
124
|
+
return_value = with_context(collection, identifiers).map do |context|
|
125
|
+
program.with_extra_context(context).evaluate(predicate)
|
126
|
+
end
|
127
|
+
List.new(return_value)
|
128
|
+
end
|
129
|
+
|
130
|
+
def with_context(collection, identifiers)
|
131
|
+
case collection
|
132
|
+
when Map
|
133
|
+
raise EvaluateError, "can only support 2 identifiers" unless identifiers.size <= 2
|
134
|
+
else
|
135
|
+
collection = collection.each_cons(identifiers.size)
|
136
|
+
end
|
137
|
+
|
138
|
+
collection.map do |elements|
|
139
|
+
identifiers.zip(elements).to_h
|
75
140
|
end
|
76
141
|
end
|
77
142
|
end
|