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.
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
@@ -29,11 +37,24 @@ module Cel
29
37
 
30
38
  private
31
39
 
40
+ def try_evaluate_lookup(key)
41
+ Protobuf.convert_to_proto_type(key, @package) || begin
42
+ Literal.to_cel_type(evaluate_identifier(key))
43
+ rescue EvaluateError # rubocop:disable Lint/SuppressedException
44
+ end
45
+ end
46
+
32
47
  def evaluate_identifier(identifier)
33
48
  if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
34
49
  TYPES[identifier.to_sym]
35
50
  else
36
- @context.lookup(identifier)
51
+ val, func = @context.lookup(identifier)
52
+
53
+ if func
54
+ evaluate_invoke(Cel::Invoke.new(var: val, func: func))
55
+ else
56
+ Literal.to_cel_type(val)
57
+ end
37
58
  end
38
59
  end
39
60
 
@@ -46,30 +67,116 @@ module Cel
46
67
  end
47
68
  end
48
69
 
49
- def evaluate_operation(operation)
50
- op = operation.op
70
+ def evaluate_message(val)
71
+ convert_to_cel = @convert_to_cel
72
+
73
+ @convert_to_cel = false
74
+ values = val.struct.transform_values do |value|
75
+ v = call(value)
76
+ v = v.to_ruby_type if v.respond_to?(:to_ruby_type)
77
+ v
78
+ end
79
+ value = Protobuf.convert_to_proto(val.message_type, values)
51
80
 
52
- values = operation.operands.map do |operand|
53
- ev_operand = call(operand)
81
+ return value unless convert_to_cel
54
82
 
55
- # return ev_operand if op == "||" && ev_operand == true
83
+ Literal.to_cel_type(value)
84
+ ensure
85
+ @convert_to_cel = convert_to_cel
86
+ end
56
87
 
57
- ev_operand
58
- end
88
+ def evaluate_operation(operation)
89
+ op = operation.op
90
+
91
+ operands = operation.operands
59
92
 
60
93
  if operation.unary? &&
61
94
  op != "!" # https://bugs.ruby-lang.org/issues/18246
62
95
  # 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))
96
+ operand = operands.first
97
+ op_value = call(operand)
98
+ value = Literal.to_cel_type(op_value.__send__(:"#{op}@"))
99
+ value.check_overflow unless operand.is_a?(Literal)
100
+ return value
101
+ end
102
+
103
+ case op
104
+ when "&&"
105
+ error = nil
106
+ last_op = nil
107
+
108
+ operands.each do |x|
109
+ op_value = call(x)
110
+
111
+ raise NoMatchingOverloadError, op unless op_value.type == TYPES[:bool]
112
+
113
+ result = true == op_value.value # rubocop:disable Style/YodaCondition
114
+
115
+ if error
116
+ # save for later processing
117
+ last_op = result
118
+ next
119
+ end
120
+
121
+ return Bool.cast(false) unless result
122
+ rescue StandardError => e
123
+ error = e
124
+ end
125
+
126
+ # if an error happened, but it was succeeded by an operation evaluating to true,
127
+ # then the latter wins
128
+ raise error if error && (last_op.nil? || last_op == true)
129
+
130
+ Bool.cast(last_op.nil? || last_op)
131
+ when "||"
132
+ error = nil
133
+ last_op = nil
134
+
135
+ operands.each do |x|
136
+ op_value = call(x)
137
+
138
+ raise NoMatchingOverloadError, op unless op_value.type == TYPES[:bool]
139
+
140
+ result = true == op_value.value # rubocop:disable Style/YodaCondition
141
+
142
+ if error
143
+ # save for later processing
144
+ last_op = result
145
+ next
146
+ end
147
+
148
+ return Bool.cast(true) if result
149
+ rescue StandardError => e
150
+ error = e
151
+ end
152
+
153
+ # if an error happened, but it was succeeded by an operation evaluating to true,
154
+ # then the latter wins
155
+ raise error if error && (last_op.nil? || last_op == false)
156
+
157
+ Bool.cast(last_op.nil? ? false : last_op)
158
+ when "in"
159
+ collection, element = operands.reverse_each.map(&method(:call))
160
+ Bool.cast(collection.include?(element))
161
+ when "!"
162
+ op_value, = operands.map(&method(:call))
163
+
164
+ raise InvalidArgumentError, op_value unless op_value.type == TYPES[:bool]
165
+
166
+ Bool.cast(!op_value.value)
167
+ when "=="
168
+ operands = operands.map(&method(:call))
169
+
170
+ # ensure that the op is called on the cel-side override
171
+ lhs, rhs = operands.sort_by do |t|
172
+ t.is_a?(Class) && t < Protobuf.base_class ? 1 : 0
173
+ end
174
+
175
+ val = lhs.public_send(op, rhs)
176
+
177
+ Literal.to_cel_type(val)
71
178
  else
72
- op_value, *values = values
179
+ op_value, *values = operands.map(&method(:call))
73
180
  val = op_value.public_send(op, *values)
74
181
 
75
182
  Literal.to_cel_type(val)
@@ -77,52 +184,138 @@ module Cel
77
184
  end
78
185
 
79
186
  def evaluate_invoke(invoke, var = invoke.var)
187
+ return evaluate_standard_func(invoke) unless var
188
+
80
189
  func = invoke.func
81
190
  args = invoke.args
82
191
 
83
- return evaluate_standard_func(invoke) unless var
192
+ var =
193
+ case var
194
+ when Identifier
195
+ if (proto_type = var.try_convert_to_proto_type)
196
+ return evaluate_invoke(invoke, proto_type)
197
+ end
84
198
 
85
- var = case var
86
- when Identifier
87
- evaluate_identifier(var)
88
- when Invoke
89
- evaluate_invoke(var)
90
- else
91
- var
92
- end
199
+ # try stuff like a.b.c
200
+ return evaluate_identifier(invoke.to_s) if args.nil?
201
+
202
+ evaluate_identifier(var)
203
+
204
+ when Invoke
205
+ if args.nil?
206
+ # try stuff like a.b.c before trying a.b then c
207
+ val = try_evaluate_lookup(invoke.to_s)
208
+
209
+ return val if val
210
+ end
211
+
212
+ evaluate_invoke(var)
213
+ else
214
+ call(var)
215
+ end || var
93
216
 
94
217
  case var
95
218
  when String
96
219
  raise EvaluateError, "#{invoke} is not supported" unless String.method_defined?(func, false)
97
220
 
98
221
  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)
222
+ when Map
223
+ return Macro.__send__(func, var, *args, program: self) if !Module.respond_to?(func) && Macro.respond_to?(func)
103
224
 
104
- var.public_send(func)
105
- when Map, List
106
- return Macro.__send__(func, var, *args, context: @context) if Macro.respond_to?(func)
225
+ # If e evaluates to a map, then e.f is equivalent to e['f'] (where f is
226
+ # still being used as a meta-variable, e.g. the expression x.foo is equivalent
227
+ # to the expression x['foo'] when x evaluates to a map).
228
+
229
+ if args
230
+ var.public_send(func, *args)
231
+ else
232
+ begin
233
+ var.public_send(func)
234
+ rescue NoMethodError
235
+ raise NoSuchKeyError.new(var, func)
236
+ end
237
+ end
238
+ when List
239
+ return Macro.__send__(func, var, *args, program: self) if !Module.respond_to?(func) && Macro.respond_to?(func)
107
240
 
108
241
  # If e evaluates to a map, then e.f is equivalent to e['f'] (where f is
109
242
  # still being used as a meta-variable, e.g. the expression x.foo is equivalent
110
243
  # to the expression x['foo'] when x evaluates to a map).
111
244
 
112
245
  args ?
113
- var.public_send(func, *args) :
246
+ var.public_send(func, *Array(args).map(&method(:call))) :
114
247
  var.public_send(func)
115
248
  when Timestamp, Duration
116
249
  raise EvaluateError, "#{invoke} is not supported" unless var.class.method_defined?(func, false)
117
250
 
251
+ # eliminate protobuf API from the lookup
252
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
253
+
254
+ # If e evaluates to a message and f is not declared in this message, the
255
+ # runtime error no_such_field is raised.
256
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
257
+
118
258
  var.public_send(func, *args)
259
+ when Literal
260
+ # eliminate ruby API from the lookup
261
+ raise NoMatchingOverloadError.new(var, func) if Literal.instance_methods.include?(func)
262
+
263
+ # If e evaluates to a message and f is not declared in this message, the
264
+ # runtime error no_such_field is raised.
265
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
266
+
267
+ var.public_send(func)
268
+ when Protobuf.base_class
269
+ # eliminate protobuf API from the lookup
270
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
271
+
272
+ # If e evaluates to a message and f is not declared in this message, the
273
+ # runtime error no_such_field is raised.
274
+ raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
275
+
276
+ value = Protobuf.lookup(var, func)
277
+
278
+ Literal.to_cel_type(value)
119
279
  else
280
+ if var.is_a?(Module) && var.const_defined?(func)
281
+ # this block assumes a message based call on a protobuf message, either to a
282
+ # subclass/namespace (Foo.Bar), or an enum (Foo.BAR)
283
+ # protobuf accessing enum module
284
+ enum = var.const_get(func)
285
+
286
+ if args.nil?
287
+ case enum
288
+ when Integer
289
+ # enum lookup
290
+ return Protobuf.convert_to_enum(enum, var)
291
+ else
292
+ return enum
293
+ end
294
+ else
295
+ arg = call(args.first)
296
+ value = case arg
297
+ when Number
298
+ Protobuf.convert_to_enum(arg.value, enum)
299
+ when String
300
+ resolved_arg = enum.resolve(arg.to_sym)
301
+ raise EvaluateError, "#{arg} is invalid" unless resolved_arg
302
+
303
+ Protobuf.convert_to_enum(resolved_arg, enum)
304
+ end
305
+ end
306
+ return Literal.to_cel_type(value)
307
+ end
308
+
120
309
  raise EvaluateError, "#{invoke} is not supported"
121
310
  end
122
311
  end
123
312
 
124
313
  def evaluate_condition(condition)
125
- call(condition.if).value ? call(condition.then) : call(condition.else)
314
+ if_value = call(condition.if)
315
+
316
+ raise InvalidArgument, condition.if unless if_value.type == TYPES[:bool]
317
+
318
+ if_value.value ? call(condition.then) : call(condition.else)
126
319
  end
127
320
 
128
321
  def evaluate_standard_func(funcall)
@@ -131,25 +324,39 @@ module Cel
131
324
 
132
325
  case func
133
326
  when :type
134
- val = call(args.first)
135
- return val.type if val.respond_to?(:type)
327
+ operand = args.first
328
+ val = call(operand)
329
+ case val
330
+ when Protobuf.enum_class
331
+ val.enum_type
332
+ else
333
+ if val.respond_to?(:type)
334
+ val.type
335
+ else
336
+ val.class
337
+ end
338
+ end
136
339
 
137
- val.class
138
340
  # 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
341
+ when :has, :matches, :size
342
+ Macro.__send__(func, *args, program: self)
343
+ when :int, :uint, :string, :double, :bytes, :duration, :timestamp, :bool
146
344
  type = TYPES[func]
147
- type.cast(call(args.first))
345
+ op_value = call(args.first)
346
+ type.cast(op_value)
148
347
  when :dyn
149
348
  call(args.first)
150
349
  else
151
350
  return evaluate_custom_func(@context.declarations[func], funcall) if @context.declarations.key?(func)
152
351
 
352
+ # enum conversion
353
+ proto_type = Protobuf.convert_to_proto_type(func.to_s, @environment.package)
354
+
355
+ if proto_type && proto_type.respond_to?(:resolve)
356
+ op_value = call(args.first)
357
+ return Protobuf.convert_to_enum(op_value.value)
358
+ end
359
+
153
360
  raise EvaluateError, "#{funcall} is not supported"
154
361
  end
155
362
  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.0"
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
+ BASE_CLASS
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