cel 0.2.3 → 0.3.1

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.
data/lib/cel/program.rb CHANGED
@@ -2,8 +2,16 @@
2
2
 
3
3
  module Cel
4
4
  class Program
5
- def initialize(context)
5
+ attr_reader :context
6
+
7
+ def initialize(context, environment)
6
8
  @context = context
9
+ @environment = environment
10
+ @convert_to_cel = true
11
+ end
12
+
13
+ def with_extra_context(context)
14
+ self.class.new(@context.merge(context), @environment)
7
15
  end
8
16
 
9
17
  def evaluate(ast)
@@ -15,7 +23,7 @@ module Cel
15
23
  when Operation
16
24
  evaluate_operation(ast)
17
25
  when Message
18
- ast.struct
26
+ evaluate_message(ast)
19
27
  when Literal
20
28
  evaluate_literal(ast)
21
29
  when Identifier
@@ -27,13 +35,36 @@ module Cel
27
35
 
28
36
  alias_method :call, :evaluate
29
37
 
38
+ def disable_cel_conversion
39
+ convert_to_cel = @convert_to_cel
40
+ begin
41
+ @convert_to_cel = false
42
+ yield(convert_to_cel)
43
+ ensure
44
+ @convert_to_cel = convert_to_cel
45
+ end
46
+ end
47
+
30
48
  private
31
49
 
50
+ def try_evaluate_lookup(key)
51
+ Protobuf.convert_to_proto_type(key, @package) || begin
52
+ Literal.to_cel_type(evaluate_identifier(key))
53
+ rescue EvaluateError # rubocop:disable Lint/SuppressedException
54
+ end
55
+ end
56
+
32
57
  def evaluate_identifier(identifier)
33
58
  if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
34
59
  TYPES[identifier.to_sym]
35
60
  else
36
- @context.lookup(identifier)
61
+ val, func = @context.lookup(identifier)
62
+
63
+ if func
64
+ evaluate_invoke(Cel::Invoke.new(var: val, func: func))
65
+ else
66
+ Literal.to_cel_type(val)
67
+ end
37
68
  end
38
69
  end
39
70
 
@@ -46,30 +77,113 @@ module Cel
46
77
  end
47
78
  end
48
79
 
49
- def evaluate_operation(operation)
50
- op = operation.op
51
-
52
- values = operation.operands.map do |operand|
53
- ev_operand = call(operand)
80
+ def evaluate_message(val)
81
+ disable_cel_conversion do |should_have_converted|
82
+ values = val.struct.transform_values do |value|
83
+ v = call(value)
84
+ v = v.to_ruby_type if v.respond_to?(:to_ruby_type)
85
+ v
86
+ end
87
+ value = Protobuf.convert_to_proto(val.message_type, values)
54
88
 
55
- # return ev_operand if op == "||" && ev_operand == true
89
+ return value unless should_have_converted
56
90
 
57
- ev_operand
91
+ Literal.to_cel_type(value)
58
92
  end
93
+ end
94
+
95
+ def evaluate_operation(operation)
96
+ op = operation.op
97
+
98
+ operands = operation.operands
59
99
 
60
100
  if operation.unary? &&
61
101
  op != "!" # https://bugs.ruby-lang.org/issues/18246
62
102
  # unary operations
63
- Literal.to_cel_type(values.first.__send__(:"#{op}@"))
64
- elsif op == "&&"
65
- Bool.new(values.all? { |x| true == x.value }) # rubocop:disable Style/YodaCondition
66
- elsif op == "||"
67
- Bool.new(values.any? { |x| true == x.value }) # rubocop:disable Style/YodaCondition
68
- elsif op == "in"
69
- element, collection = values
70
- Bool.new(collection.include?(element))
103
+ operand = operands.first
104
+ op_value = call(operand)
105
+ value = Literal.to_cel_type(op_value.__send__(:"#{op}@"))
106
+ value.check_overflow unless operand.is_a?(Literal)
107
+ return value
108
+ end
109
+
110
+ case op
111
+ when "&&"
112
+ error = nil
113
+ last_op = nil
114
+
115
+ operands.each do |x|
116
+ op_value = call(x)
117
+
118
+ raise NoMatchingOverloadError, op unless op_value.type == TYPES[:bool]
119
+
120
+ result = true == op_value.value # rubocop:disable Style/YodaCondition
121
+
122
+ if error
123
+ # save for later processing
124
+ last_op = result
125
+ next
126
+ end
127
+
128
+ return Bool.cast(false) unless result
129
+ rescue StandardError => e
130
+ error = e
131
+ end
132
+
133
+ # if an error happened, but it was succeeded by an operation evaluating to true,
134
+ # then the latter wins
135
+ raise error if error && (last_op.nil? || last_op == true)
136
+
137
+ Bool.cast(last_op.nil? || last_op)
138
+ when "||"
139
+ error = nil
140
+ last_op = nil
141
+
142
+ operands.each do |x|
143
+ op_value = call(x)
144
+
145
+ raise NoMatchingOverloadError, op unless op_value.type == TYPES[:bool]
146
+
147
+ result = true == op_value.value # rubocop:disable Style/YodaCondition
148
+
149
+ if error
150
+ # save for later processing
151
+ last_op = result
152
+ next
153
+ end
154
+
155
+ return Bool.cast(true) if result
156
+ rescue StandardError => e
157
+ error = e
158
+ end
159
+
160
+ # if an error happened, but it was succeeded by an operation evaluating to true,
161
+ # then the latter wins
162
+ raise error if error && (last_op.nil? || last_op == false)
163
+
164
+ Bool.cast(last_op.nil? ? false : last_op)
165
+ when "in"
166
+ collection, element = operands.reverse_each.map(&method(:call))
167
+ Bool.cast(collection.include?(element))
168
+ when "!"
169
+ op_value, = operands.map(&method(:call))
170
+
171
+ raise InvalidArgumentError, op_value unless op_value.type == TYPES[:bool]
172
+
173
+ Bool.cast(!op_value.value)
174
+ when "=="
175
+ operands = operands.map(&method(:call))
176
+
177
+ # ensure that the op is called on the cel-side override
178
+ lhs, rhs = operands.sort_by do |t|
179
+ t.is_a?(Class) && t < Protobuf.base_class ? 1 : 0
180
+ end
181
+
182
+ val = lhs.public_send(op, rhs)
183
+
184
+ Literal.to_cel_type(val)
71
185
  else
72
- op_value, *values = values
186
+ op_value, *values = operands.map(&method(:call))
73
187
  val = op_value.public_send(op, *values)
74
188
 
75
189
  Literal.to_cel_type(val)
@@ -77,52 +191,138 @@ module Cel
77
191
  end
78
192
 
79
193
  def evaluate_invoke(invoke, var = invoke.var)
194
+ return evaluate_standard_func(invoke) unless var
195
+
80
196
  func = invoke.func
81
197
  args = invoke.args
82
198
 
83
- return evaluate_standard_func(invoke) unless var
199
+ var =
200
+ case var
201
+ when Identifier
202
+ if (proto_type = var.try_convert_to_proto_type)
203
+ return evaluate_invoke(invoke, proto_type)
204
+ end
84
205
 
85
- var = case var
86
- when Identifier
87
- evaluate_identifier(var)
88
- when Invoke
89
- evaluate_invoke(var)
90
- else
91
- var
92
- end
206
+ # try stuff like a.b.c
207
+ return evaluate_identifier(invoke.to_s) if args.nil?
208
+
209
+ evaluate_identifier(var)
210
+
211
+ when Invoke
212
+ if args.nil?
213
+ # try stuff like a.b.c before trying a.b then c
214
+ val = try_evaluate_lookup(invoke.to_s)
215
+
216
+ return val if val
217
+ end
218
+
219
+ evaluate_invoke(var)
220
+ else
221
+ call(var)
222
+ end || var
93
223
 
94
224
  case var
95
225
  when String
96
226
  raise EvaluateError, "#{invoke} is not supported" unless String.method_defined?(func, false)
97
227
 
98
228
  var.public_send(func, *args.map(&method(:call)))
99
- when Message
100
- # If e evaluates to a message and f is not declared in this message, the
101
- # runtime error no_such_field is raised.
102
- raise NoSuchFieldError.new(var, func) unless var.field?(func)
229
+ when Map
230
+ return Macro.__send__(func, var, *args, program: self) if !Module.respond_to?(func) && Macro.respond_to?(func)
103
231
 
104
- var.public_send(func)
105
- when Map, List
106
- return Macro.__send__(func, var, *args, context: @context) if Macro.respond_to?(func)
232
+ # If e evaluates to a map, then e.f is equivalent to e['f'] (where f is
233
+ # still being used as a meta-variable, e.g. the expression x.foo is equivalent
234
+ # to the expression x['foo'] when x evaluates to a map).
235
+
236
+ if args
237
+ var.public_send(func, *args)
238
+ else
239
+ begin
240
+ var.public_send(func)
241
+ rescue NoMethodError
242
+ raise NoSuchKeyError.new(var, func)
243
+ end
244
+ end
245
+ when List
246
+ return Macro.__send__(func, var, *args, program: self) if !Module.respond_to?(func) && Macro.respond_to?(func)
107
247
 
108
248
  # If e evaluates to a map, then e.f is equivalent to e['f'] (where f is
109
249
  # still being used as a meta-variable, e.g. the expression x.foo is equivalent
110
250
  # to the expression x['foo'] when x evaluates to a map).
111
251
 
112
252
  args ?
113
- var.public_send(func, *args) :
253
+ var.public_send(func, *Array(args).map(&method(:call))) :
114
254
  var.public_send(func)
115
255
  when Timestamp, Duration
116
256
  raise EvaluateError, "#{invoke} is not supported" unless var.class.method_defined?(func, false)
117
257
 
258
+ # eliminate protobuf API from the lookup
259
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
260
+
261
+ # If e evaluates to a message and f is not declared in this message, the
262
+ # runtime error no_such_field is raised.
263
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
264
+
118
265
  var.public_send(func, *args)
266
+ when Literal
267
+ # eliminate ruby API from the lookup
268
+ raise NoMatchingOverloadError.new(var, func) if Literal.instance_methods.include?(func)
269
+
270
+ # If e evaluates to a message and f is not declared in this message, the
271
+ # runtime error no_such_field is raised.
272
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
273
+
274
+ var.public_send(func)
275
+ when Protobuf.base_class
276
+ # eliminate protobuf API from the lookup
277
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
278
+
279
+ # If e evaluates to a message and f is not declared in this message, the
280
+ # runtime error no_such_field is raised.
281
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
282
+
283
+ value = Protobuf.lookup(var, func)
284
+
285
+ Literal.to_cel_type(value)
119
286
  else
287
+ if var.is_a?(Module) && var.const_defined?(func)
288
+ # this block assumes a message based call on a protobuf message, either to a
289
+ # subclass/namespace (Foo.Bar), or an enum (Foo.BAR)
290
+ # protobuf accessing enum module
291
+ enum = var.const_get(func)
292
+
293
+ if args.nil?
294
+ case enum
295
+ when Integer
296
+ # enum lookup
297
+ return Protobuf.convert_to_enum(enum, var)
298
+ else
299
+ return enum
300
+ end
301
+ else
302
+ arg = call(args.first)
303
+ value = case arg
304
+ when Number
305
+ Protobuf.convert_to_enum(arg.value, enum)
306
+ when String
307
+ resolved_arg = enum.resolve(arg.to_sym)
308
+ raise EvaluateError, "#{arg} is invalid" unless resolved_arg
309
+
310
+ Protobuf.convert_to_enum(resolved_arg, enum)
311
+ end
312
+ end
313
+ return Literal.to_cel_type(value)
314
+ end
315
+
120
316
  raise EvaluateError, "#{invoke} is not supported"
121
317
  end
122
318
  end
123
319
 
124
320
  def evaluate_condition(condition)
125
- call(condition.if).value ? call(condition.then) : call(condition.else)
321
+ if_value = call(condition.if)
322
+
323
+ raise InvalidArgument, condition.if unless if_value.type == TYPES[:bool]
324
+
325
+ if_value.value ? call(condition.then) : call(condition.else)
126
326
  end
127
327
 
128
328
  def evaluate_standard_func(funcall)
@@ -131,25 +331,39 @@ module Cel
131
331
 
132
332
  case func
133
333
  when :type
134
- val = call(args.first)
135
- return val.type if val.respond_to?(:type)
334
+ operand = args.first
335
+ val = call(operand)
336
+ case val
337
+ when Protobuf.enum_class
338
+ val.enum_type
339
+ else
340
+ if val.respond_to?(:type)
341
+ val.type
342
+ else
343
+ val.class
344
+ end
345
+ end
136
346
 
137
- val.class
138
347
  # MACROS
139
- when :has
140
- Macro.__send__(func, *args)
141
- when :size
142
- Cel::Number.new(:int, Macro.__send__(func, *args))
143
- when :matches
144
- Macro.__send__(func, *args.map(&method(:call)))
145
- when :int, :uint, :string, :double, :bytes, :duration, :timestamp
348
+ when :has, :matches, :size
349
+ Macro.__send__(func, *args, program: self)
350
+ when :int, :uint, :string, :double, :bytes, :duration, :timestamp, :bool
146
351
  type = TYPES[func]
147
- type.cast(call(args.first))
352
+ op_value = call(args.first)
353
+ type.cast(op_value)
148
354
  when :dyn
149
355
  call(args.first)
150
356
  else
151
357
  return evaluate_custom_func(@context.declarations[func], funcall) if @context.declarations.key?(func)
152
358
 
359
+ # enum conversion
360
+ proto_type = Protobuf.convert_to_proto_type(func.to_s, @environment.package)
361
+
362
+ if proto_type && proto_type.respond_to?(:resolve)
363
+ op_value = call(args.first)
364
+ return Protobuf.convert_to_enum(op_value.value)
365
+ end
366
+
153
367
  raise EvaluateError, "#{funcall} is not supported"
154
368
  end
155
369
  end
data/lib/cel/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.2.3"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -4,6 +4,7 @@ require "bigdecimal"
4
4
  require "cel/version"
5
5
  require "cel/errors"
6
6
  require "cel/ast/types"
7
+ require "cel/ast/elements"
7
8
  require "cel/parser"
8
9
  require "cel/macro"
9
10
  require "cel/context"
@@ -12,8 +13,227 @@ require "cel/checker"
12
13
  require "cel/program"
13
14
  require "cel/environment"
14
15
 
16
+ begin
17
+ require "tzinfo"
18
+ rescue LoadError # rubocop:disable Lint/SuppressedException
19
+ end
20
+
21
+ begin
22
+ require "cel/ast/elements/protobuf"
23
+ rescue LoadError => e
24
+ puts e
25
+ module Cel
26
+ class << self
27
+ # returns a struct class containing the attributes that one would
28
+ # have expected from the protobuf stub equivalent.
29
+ def message_container(name, struct)
30
+ mod = Cel.package_to_module_name(name.to_s)
31
+
32
+ if Object.const_defined?(mod)
33
+ Object.const_get(mod)
34
+ else
35
+ struct_keys = container_struct_keys(mod, struct)
36
+ struct_class = container_struct(mod, struct_keys)
37
+
38
+ klass = Object
39
+
40
+ *namespaces, mod = mod.split("::")
41
+
42
+ namespaces.each do |nm|
43
+ klass = if klass.const_defined?(nm)
44
+ klass.const_get(nm)
45
+ else
46
+ klass.const_set(nm, Module.new)
47
+ end
48
+ end
49
+
50
+ klass.const_set(mod, struct_class)
51
+ end
52
+ end
53
+
54
+ def container_struct(mod, struct_keys)
55
+ ::Struct.new(*struct_keys, keyword_init: true) do
56
+ case mod
57
+ when "Google::Protobuf::BytesValue",
58
+ "Google::Protobuf::Int32Value",
59
+ "Google::Protobuf::Int64Value",
60
+ "Google::Protobuf::UInt32Value",
61
+ "Google::Protobuf::UInt64Value",
62
+ "Google::Protobuf::DoubleValue",
63
+ "Google::Protobuf::FloatValue"
64
+
65
+ def initialize(*, **)
66
+ super
67
+ self.value ||= 0
68
+ end
69
+ when "Google::Protobuf::StringValue"
70
+ def initialize(*, **)
71
+ super
72
+ self.value ||= ""
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def container_struct_keys(mod, struct)
79
+ keys = struct.nil? ? [] : struct.keys
80
+
81
+ # handle wrapper types
82
+ case mod
83
+ when "Google::Protobuf::Any"
84
+ keys.push(:type_url, :value)
85
+ when "Google::Protobuf::ListValue"
86
+ keys << :values
87
+ when "Google::Protobuf::Struct"
88
+ keys << :fields
89
+ when "Google::Protobuf::Value"
90
+ keys.push(:bool_value, :number_value, :int64_value, :uint64_value, :double_value, :string_value,
91
+ :bytes_value, :enum_value, :object_value, :struct_value, :map_value, :list_value, :type_value)
92
+ when "Google::Protobuf::BytesValue",
93
+ "Google::Protobuf::Int32Value",
94
+ "Google::Protobuf::Int64Value",
95
+ "Google::Protobuf::UInt32Value",
96
+ "Google::Protobuf::UInt64Value",
97
+ "Google::Protobuf::DoubleValue",
98
+ "Google::Protobuf::FloatValue",
99
+ "Google::Protobuf::BoolValue",
100
+ "Google::Protobuf::NullValue",
101
+ "Google::Protobuf::StringValue"
102
+ keys << :value
103
+ when "Google::Protobuf::Timestamp",
104
+ "Google::Protobuf::Duration"
105
+ keys.push(:seconds, :nanos)
106
+ end
107
+
108
+ keys.uniq
109
+ end
110
+ end
111
+
112
+ module Protobuf
113
+ module_function
114
+
115
+ BASE_CLASS = Class.new(Object)
116
+
117
+ def base_class
118
+ Struct
119
+ end
120
+
121
+ def enum_class
122
+ BASE_CLASS
123
+ end
124
+
125
+ def map_class
126
+ BASE_CLASS
127
+ end
128
+
129
+ def timestamp_class
130
+ BASE_CLASS
131
+ end
132
+
133
+ def duration_class
134
+ BASE_CLASS
135
+ end
136
+
137
+ def convert_to_proto(message_type, values)
138
+ message_type.new(**values)
139
+ end
140
+
141
+ def convert_to_proto_type(*); end
142
+
143
+ def try_invoke_from(var, func, args)
144
+ case var
145
+ when "NullValue", "google.protobuf.NullValue"
146
+
147
+ Cel::Null.new if func == "NULL_VALUE" && args.nil?
148
+
149
+ end
150
+ end
151
+
152
+ def try_convert_from_wrapper(struct)
153
+ case struct.class.name
154
+ when "ListValue", "Google::Protobuf::ListValue"
155
+ struct.values
156
+ when "Struct", "Google::Protobuf::Struct"
157
+ struct.fields
158
+ when "Value", "Google::Protobuf::Value"
159
+ _, value = struct.each_pair.find { |_, v| !v.nil? }
160
+
161
+ try_convert_from_wrapper(value)
162
+ when "BytesValue", "Google::Protobuf::BytesValue"
163
+ Cel::Bytes.new(struct.value.bytes)
164
+ when "Int32Value", "Google::Protobuf::Int32Value",
165
+ "Int64Value", "Google::Protobuf::Int64Value"
166
+ Cel::Number.new(:int, struct.value)
167
+ when "UInt32Value", "Google::Protobuf::UInt32Value",
168
+ "UInt64Value", "Google::Protobuf::UInt64Value"
169
+ Cel::Number.new(:uint, struct.value)
170
+ when "DoubleValue", "Google::Protobuf::DoubleValue",
171
+ "FloatValue", "Google::Protobuf::FloatValue"
172
+ Cel::Number.new(:double, struct.value)
173
+ when "BoolValue", "Google::Protobuf::BoolValue",
174
+ "NullValue", "Google::Protobuf::NullValue",
175
+ "StringValue", "Google::Protobuf::StringValue"
176
+ struct.value
177
+ when "Google::Protobuf::Timestamp"
178
+ value = (struct.seconds || 0) +
179
+ ((struct.nanos || 0) / 1_000_000)
180
+ Cel::Timestamp.new(value)
181
+ when "Google::Protobuf::Duration"
182
+ value = (struct.seconds || 0) +
183
+ ((struct.nanos || 0) / 1_000_000)
184
+ Cel::Duration.new(value)
185
+ else
186
+ struct
187
+ end
188
+ end
189
+
190
+ def try_convert_from_wrapper_type(type)
191
+ case type
192
+ when "Any", "google.protobuf.Any",
193
+ "Value", "google.protobuf.Value"
194
+ TYPES[:any]
195
+ when "ListValue", "google.protobuf.ListValue"
196
+ TYPES[:list]
197
+ when "BytesValue", "google.protobuf.BytesValue"
198
+ TYPES[:bytes]
199
+ when "Int32Value", "google.protobuf.Int32Value",
200
+ "Int64Value", "google.protobuf.Int64Value"
201
+ TYPES[:int]
202
+ when "UInt32Value", "google.protobuf.UInt32Value",
203
+ "UInt64Value", "google.protobuf.UInt64Value"
204
+ TYPES[:uint]
205
+ when "DoubleValue", "google.protobuf.DoubleValue",
206
+ "FloatValue", "google.protobuf.FloatValue"
207
+ TYPES[:double]
208
+ when "BoolValue", "google.protobuf.BoolValue"
209
+ TYPES[:bool]
210
+ when "NullValue", "google.protobuf.NullValue"
211
+ TYPES[:null]
212
+ when "StringValue", "google.protobuf.StringValue"
213
+ TYPES[:string]
214
+ when "google.protobuf.Timestamp"
215
+ TYPES[:timestamp]
216
+ when "google.protobuf.Duration"
217
+ TYPES[:duration]
218
+ else
219
+ TYPES[:map]
220
+ end
221
+ end
222
+
223
+ def method_missing(*) # rubocop:disable Style/MissingRespondToMissing
224
+ raise Cel::Error, "\"google/protobuf\" is required in order to use this feature"
225
+ end
226
+ end
227
+ end
228
+ end
229
+
15
230
  module Cel
16
- def self.to_numeric(anything)
231
+ MAX_INT = (2**63)
232
+ MAX_FLOAT = (2**63)
233
+
234
+ module_function
235
+
236
+ def to_numeric(anything)
17
237
  num = BigDecimal(anything.to_s)
18
238
  if num.frac.zero?
19
239
  num.to_i
@@ -21,4 +241,15 @@ module Cel
21
241
  num.to_f
22
242
  end
23
243
  end
244
+
245
+ def package_to_module_name(type)
246
+ namespace, type_msg = type.split("/", 2)
247
+ type_msg ||= namespace # sometimes there's no namespace
248
+
249
+ return unless /[[:upper:]]/.match?(type_msg)
250
+
251
+ type_msg.split(".").map do |typ|
252
+ /[[:upper:]]/.match?(typ[0]) ? typ : typ.capitalize
253
+ end.join("::")
254
+ end
24
255
  end