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.
@@ -2,10 +2,33 @@
2
2
 
3
3
  module Cel
4
4
  class Environment
5
- def initialize(declarations = nil)
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
- @parser = Parser.new
8
- @checker = Checker.new(@declarations)
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(@declarations, expr)
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
- context = Context.new(@declarations, bindings)
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(declarations, ast)
52
- @declarations = declarations
95
+ def initialize(environment, ast)
96
+ @environment = environment
53
97
  @ast = ast
54
98
  end
55
99
 
56
100
  def evaluate(bindings = nil)
57
- context = Context.new(@declarations, bindings)
58
- Program.new(context).evaluate(@ast)
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 < CheckError
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.field?(func)
24
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
24
25
 
25
- Bool.new(!var.public_send(func).nil?)
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.new(var.respond_to?(func))
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.size
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.new(pattern.match?(string))
68
+ Bool.cast(pattern.match?(string))
43
69
  end
44
70
 
45
- def all(collection, identifier, predicate, context:)
46
- return_value = collection.all? do |element, *|
47
- Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
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
- Bool.new(return_value)
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, identifier, predicate, context:)
53
- return_value = collection.any? do |element, *|
54
- Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
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.new(return_value)
96
+ Bool.cast(return_value)
57
97
  end
58
98
 
59
- def exists_one(collection, identifier, predicate, context:)
60
- return_value = collection.one? do |element, *|
61
- Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
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.new(return_value)
107
+ Bool.cast(return_value.size == 1)
64
108
  end
65
109
 
66
- def filter(collection, identifier, predicate, context:)
67
- collection.select do |element, *|
68
- Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
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, identifier, predicate, context:)
73
- collection.map do |element, *|
74
- Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate)
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