cel 0.3.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcb8e069501a6c9cfdf6af67f83701111d67130d04a9208a05482d14ac1c519a
4
- data.tar.gz: 0af115f5924dd63f926227fd916c2fe3f2b25bb6a68e682df1c9f5be36b1be6f
3
+ metadata.gz: 82fcb37e45c0553a7c073b58e4c6b7203cc50bd186419724d72f5f1b0217e3f5
4
+ data.tar.gz: 44170fc7001634abc19860db8dcbc8c05ae8974716ad80dd28c4316dc0cec7f3
5
5
  SHA512:
6
- metadata.gz: d78064a96c11a026f1b6f16a3ebdfed6f3f528170975ab9c26b1e7438c7805419830418f32e7b6557bafeace936c35f1dc18f695a1294862780c2aff9e0bc335
7
- data.tar.gz: 9897a6ed3a7708cefaf1b6bd24f86802b1c50709cbbb9bab1fd843e31e3dc1f1f049f4770afa0c34ed89c91b30442a95738b30eb52d91e64d99f1dbc880e0de9
6
+ metadata.gz: 593ec20b7001fe593d6df79928ca8411327251fff2405491eb33642299cf6bc09d255d0088fdb6a1deda77eabccf8bcce229ad5038f68ee932f9c10ef162cdc0
7
+ data.tar.gz: 55eb21528a3ff994f5fc013ca49ded2b7cb0aaf9d5d434c671fc0cd534ed0119269c1e2bf8b5fe9e44ba1f1e05f51d30f5fd84abd1f44d740898dd971b341ece
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2025-08-01
4
+
5
+ ### Improvements
6
+
7
+ * Support for depth protection, using the defaults from the spec (ex: recursion depth of 32, nesting depth of 32), and supporting API to override them (ex: `Cel::Environment.new(max_recursion_depth: 42))`).
8
+ * lazy-evaluating bindings to avoid upfront costly transformations of ruby objects into cel objects which are ultimately not used in expressions.
9
+
10
+ ### Bugfixes
11
+
12
+ * `has()` macro correctly works for mapsl
13
+
3
14
  ## [0.3.0] - 2025-06-12
4
15
 
5
16
  ### Features
data/README.md CHANGED
@@ -32,7 +32,7 @@ The usage pattern follows the pattern defined by [cel-go](https://github.com/goo
32
32
  require "cel"
33
33
 
34
34
  # set the environment
35
- env = Cel::Environment.new(name: :string, group: :string)
35
+ env = Cel::Environment.new(declarations: { name: :string, group: :string })
36
36
 
37
37
  # 1.1 parse
38
38
  begin
@@ -67,20 +67,52 @@ end
67
67
  puts return_value #=> true
68
68
  ```
69
69
 
70
- ### types
70
+ ### Environment
71
71
 
72
- `cel-ruby` supports declaring the types of variables in the environment, which allows for expression checking:
72
+ `Cel::Environment` is the entrypoint for parsing, checking and evaluating CEL expressions, as well as customizing/constraining any of these functions. For that matter, it can be initialized with the following keyword arguments:
73
+
74
+ * `:declarations`: a hash of the expected variables names-to-cel-types, which are used to validate the expression and data bindings. When variables aren't declared, they assume the `any` type.
75
+ * `:container`: used to declare the package of protobuf messages only declared by name used in the expression (i.e. `cel.expr.conformance.proto3`).
76
+ * `:disable_check`: (defaults to `false`) enables/disables expression check phase (except when `env.check(expr)` is explicitly called).
77
+ * `max_recursion_depth`: (defaults to `32`) max number of parsable recursive/repeating rules, as per what the spec states.
78
+ * `max_nesting_depth`: (defaults to `12`) max number of parsable nested rules, as per what the spec states.
79
+
80
+ #### declarations
81
+
82
+ `cel-ruby` supports declaring the types of variables in the environment, which enhances expression type checking:
73
83
 
74
84
  ```ruby
85
+ # without declarations
86
+ env = Cel::Environment.new
87
+ env.check("[first_name] + middle_names + [last_name]") #=> any
88
+
89
+ # with declarations
75
90
  env = Cel::Environment.new(
76
- first_name: :string, # shortcut for Cel::Types[:string]
77
- middle_names: Cel::Types[:list, :string], # list of strings
78
- last_name: :string
91
+ declarations: {
92
+ first_name: :string, # shortcut for Cel::Types[:string]
93
+ middle_names: Cel::TYPES[:list, :string], # list of strings
94
+ last_name: :string
95
+ }
79
96
  )
97
+ env.check("[first_name] + middle_names + [last_name]") #=> list(string)
98
+
99
+ # you can use Cel::TYPES to access any type of primitive type, i.e. Cel::TYPES[:bytes]
100
+ ```
101
+
102
+ #### :container
103
+
104
+ This can be used to simplify writing CEL expressions with long protobuf package declarations:
80
105
 
81
- # you can use Cel::Types to access any type of primitive type, i.e. Cel::Types[:bytes]
106
+ ```ruby
107
+ env = Cel::Environment.new
108
+ env.evaluate("my.company.private.protobufs.Account{id: 2}.id)")
109
+ # or
110
+ env = Cel::Environment.new(container: "my.company.private.protobufs.")
111
+ env.evaluate("Account{id: 2}.id)")
82
112
  ```
83
113
 
114
+ **Note**: the `google.protobuf` packaged is already supported OOTB.
115
+
84
116
  ### protobuf
85
117
 
86
118
  If `google/protobuf` is available in the environment, `cel-ruby` will also be able to integrate with protobuf declarations in CEL expressions.
@@ -8,6 +8,25 @@ require "google/protobuf/well_known_types"
8
8
 
9
9
  module Cel
10
10
  module Protobuf
11
+ PROTOBUF_VALUES = Set.new(
12
+ ["Any", "google.protobuf.Any",
13
+ "ListValue", "google.protobuf.ListValue",
14
+ "Struct", "google.protobuf.Struct",
15
+ "Value", "google.protobuf.Value",
16
+ "BoolValue", "google.protobuf.BoolValue",
17
+ "BytesValue", "google.protobuf.BytesValue",
18
+ "DoubleValue", "google.protobuf.DoubleValue",
19
+ "FloatValue", "google.protobuf.FloatValue",
20
+ "Int32Value", "google.protobuf.Int32Value",
21
+ "Int64Value", "google.protobuf.Int64Value",
22
+ "Uint32Value", "google.protobuf.Uint32Value",
23
+ "Uint64Value", "google.protobuf.Uint64Value",
24
+ "NullValue", "google.protobuf.NullValue",
25
+ "StringValue", "google.protobuf.StringValue",
26
+ "Timestamp", "google.protobuf.Timestamp",
27
+ "Duration", "google.protobuf.Duration"]
28
+ )
29
+
11
30
  module_function
12
31
 
13
32
  def base_class
@@ -294,34 +313,18 @@ module Cel
294
313
  end
295
314
 
296
315
  def try_invoke_from(var, func, args)
297
- case var
298
- when "Any", "google.protobuf.Any",
299
- "ListValue", "google.protobuf.ListValue",
300
- "Struct", "google.protobuf.Struct",
301
- "Value", "google.protobuf.Value",
302
- "BoolValue", "google.protobuf.BoolValue",
303
- "BytesValue", "google.protobuf.BytesValue",
304
- "DoubleValue", "google.protobuf.DoubleValue",
305
- "FloatValue", "google.protobuf.FloatValue",
306
- "Int32Value", "google.protobuf.Int32Value",
307
- "Int64Value", "google.protobuf.Int64Value",
308
- "Uint32Value", "google.protobuf.Uint32Value",
309
- "Uint64Value", "google.protobuf.Uint64Value",
310
- "NullValue", "google.protobuf.NullValue",
311
- "StringValue", "google.protobuf.StringValue",
312
- "Timestamp", "google.protobuf.Timestamp",
313
- "Duration", "google.protobuf.Duration"
314
- protoclass = var.split(".").last
315
- protoclass = Google::Protobuf.const_get(protoclass)
316
-
317
- value = if args.nil? && protoclass.constants.include?(func.to_sym)
318
- protoclass.const_get(func)
319
- else
320
- protoclass.__send__(func, *args)
321
- end
316
+ return unless var.respond_to?(:to_str) && PROTOBUF_VALUES.include?(var.to_str)
322
317
 
323
- Literal.to_cel_type(value)
318
+ protoclass = var.split(".").last
319
+ protoclass = Google::Protobuf.const_get(protoclass)
320
+
321
+ value = if args.nil? && protoclass.constants.include?(func.to_sym)
322
+ protoclass.const_get(func)
323
+ else
324
+ protoclass.__send__(func, *args)
324
325
  end
326
+
327
+ Literal.to_cel_type(value)
325
328
  end
326
329
 
327
330
  # finds value for +attribute+ declared in the +proto+.
@@ -37,12 +37,13 @@ module Cel
37
37
  class Message
38
38
  EMPTY_STRUCT = {}.freeze
39
39
 
40
- attr_reader :name, :struct, :package
40
+ attr_reader :name, :struct, :package, :depth
41
41
 
42
- def initialize(name, struct, package: nil)
42
+ def initialize(name, struct, package: nil, depth: 1)
43
43
  @struct = struct ? struct.transform_keys(&:to_sym) : EMPTY_STRUCT
44
44
  @name = name
45
45
  @package = package
46
+ @depth = depth
46
47
  end
47
48
 
48
49
  def type
@@ -83,7 +84,7 @@ module Cel
83
84
  class Invoke
84
85
  attr_accessor :var
85
86
 
86
- attr_reader :func, :args
87
+ attr_reader :func, :args, :depth
87
88
 
88
89
  def self.new(func:, var: nil, args: nil, **rest)
89
90
  Protobuf.try_invoke_from(var, func, args) || super
@@ -93,11 +94,12 @@ module Cel
93
94
  TYPES[:any]
94
95
  end
95
96
 
96
- def initialize(func:, var: nil, args: nil, package: nil)
97
+ def initialize(func:, var: nil, args: nil, depth: 1, package: nil)
97
98
  @var = var
98
99
  @func = func.to_sym
99
100
  @args = args
100
101
  @package = package
102
+ @depth = depth
101
103
  end
102
104
 
103
105
  def ==(other)
@@ -113,7 +115,7 @@ module Cel
113
115
 
114
116
  def to_s
115
117
  if var
116
- if func == :[]
118
+ if indexing?
117
119
  "#{var}[#{args}]"
118
120
  else
119
121
  "#{var}.#{func}#{"(#{args.map(&:to_s).join(", ")})" if args}"
@@ -134,6 +136,10 @@ module Cel
134
136
 
135
137
  proto_type.const_get(@func)
136
138
  end
139
+
140
+ def indexing?
141
+ @func == :[]
142
+ end
137
143
  end
138
144
 
139
145
  class Function
@@ -392,11 +398,14 @@ module Cel
392
398
  end
393
399
 
394
400
  class List < Literal
395
- def initialize(value)
401
+ attr_reader :depth
402
+
403
+ def initialize(value, depth: 1)
396
404
  value = value.map do |v|
397
405
  Literal.to_cel_type(v)
398
406
  end
399
407
  super(TYPES[:list], value)
408
+ @depth = depth
400
409
  end
401
410
 
402
411
  def [](key)
@@ -429,22 +438,22 @@ module Cel
429
438
  end
430
439
 
431
440
  def by_max_depth
432
- max = @value.max { |a1, a2| depth(a1, 0) <=> depth(a2, 0) }
441
+ max = @value.max { |a1, a2| calc_depth(a1, 0) <=> calc_depth(a2, 0) }
433
442
 
434
443
  # return the last value if all options have the same depth
435
- return @value.last if (max == @value.first) && (depth(max, 0) == depth(@value.last, 0))
444
+ return @value.last if (max == @value.first) && (calc_depth(max, 0) == calc_depth(@value.last, 0))
436
445
 
437
446
  max
438
447
  end
439
448
 
440
449
  private
441
450
 
442
- def depth(element, acc)
451
+ def calc_depth(element, acc)
443
452
  case element
444
453
  when List
445
- element.value.map { |el| depth(el, acc + 1) }.max || (acc + 1)
454
+ element.value.map { |el| calc_depth(el, acc + 1) }.max || (acc + 1)
446
455
  when Map
447
- element.value.map { |(_, el)| depth(el, acc + 1) }.max || (acc + 1)
456
+ element.value.map { |(_, el)| calc_depth(el, acc + 1) }.max || (acc + 1)
448
457
  else
449
458
  acc
450
459
  end
@@ -453,13 +462,16 @@ module Cel
453
462
 
454
463
  class Map < Literal
455
464
  ALLOWED_TYPES = %i[int uint bool string].map { |typ| TYPES[typ] }.freeze
456
- def initialize(value)
465
+
466
+ attr_reader :depth
467
+
468
+ def initialize(value, depth: 1)
457
469
  # store array internally to support repeated keys
458
470
  value = value.map do |k, v|
459
471
  [Literal.to_cel_type(k), Literal.to_cel_type(v)]
460
472
  end
461
473
  super(TYPES[:map], value)
462
- check
474
+ @depth = depth
463
475
  end
464
476
 
465
477
  def ==(other)
@@ -507,13 +519,19 @@ module Cel
507
519
  end
508
520
 
509
521
  def respond_to_missing?(meth, *args)
510
- super || (@value && @value.any? { |k, _| k.to_s == meth.to_s })
522
+ return true if super
523
+ return false unless @value
524
+
525
+ meth_s = meth.to_s
526
+
527
+ @value.any? { |k, _| k == meth_s }
511
528
  end
512
529
 
513
530
  def method_missing(meth, *args)
514
531
  return super unless @value
515
532
 
516
- values = @value.filter_map { |k, v| v if k.to_s == meth.to_s }
533
+ meth_s = meth.to_s
534
+ values = @value.filter_map { |k, v| v if k == meth_s }
517
535
 
518
536
  return super if values.empty?
519
537
 
@@ -760,14 +778,15 @@ module Cel
760
778
  end
761
779
 
762
780
  class Operation
763
- attr_reader :op, :operands
781
+ attr_reader :op, :operands, :depth
764
782
 
765
783
  attr_accessor :type
766
784
 
767
- def initialize(op, operands)
785
+ def initialize(op, operands, depth: 1)
768
786
  @op = op
769
787
  @operands = operands
770
788
  @type = TYPES[:any]
789
+ @depth = depth
771
790
  end
772
791
 
773
792
  def ==(other)
@@ -795,12 +814,13 @@ module Cel
795
814
  end
796
815
 
797
816
  class Condition
798
- attr_reader :if, :then, :else
817
+ attr_reader :if, :then, :else, :depth
799
818
 
800
- def initialize(if_, then_, else_)
819
+ def initialize(if_, then_, else_, depth: 1)
801
820
  @if = if_
802
821
  @then = then_
803
822
  @else = else_
823
+ @depth = depth
804
824
  end
805
825
 
806
826
  def type
data/lib/cel/ast/types.rb CHANGED
@@ -176,7 +176,7 @@ module Cel
176
176
  end
177
177
 
178
178
  def cast(value)
179
- List.new(value)
179
+ value.is_a?(List) ? value : List.new(value)
180
180
  end
181
181
  end
182
182
 
@@ -203,7 +203,7 @@ module Cel
203
203
  end
204
204
 
205
205
  def cast(value)
206
- Map.new(value)
206
+ value.is_a?(Map) ? value : Map.new(value)
207
207
  end
208
208
  end
209
209
 
data/lib/cel/context.rb CHANGED
@@ -7,23 +7,6 @@ module Cel
7
7
  def initialize(declarations, bindings)
8
8
  @declarations = declarations
9
9
  @bindings = bindings.dup
10
-
11
- return unless @bindings
12
-
13
- @bindings.each do |k, v|
14
- if @declarations && (type = @declarations[k])
15
- type = TYPES[type] if type.is_a?(Symbol)
16
-
17
- val = if type.is_a?(Class) && type < Protobuf.base_class
18
- Protobuf.convert_to_proto(type, v)
19
- else
20
- type.cast(to_cel_type(v))
21
- end
22
- else
23
- val = to_cel_type(v)
24
- end
25
- @bindings[k] = val
26
- end
27
10
  end
28
11
 
29
12
  def lookup(identifier)
@@ -39,6 +22,7 @@ module Cel
39
22
  fetched = false
40
23
  (0...lookup_keys.size).reverse_each do |idx|
41
24
  key = lookup_keys[0..idx].join(".").to_sym
25
+ is_binding = val == @bindings
42
26
 
43
27
  val = if val.respond_to?(:key?)
44
28
  # hash
@@ -56,7 +40,8 @@ module Cel
56
40
  end
57
41
  end
58
42
 
59
- lookup_keys = lookup_keys[idx + 1..]
43
+ val = @bindings[key] = to_cel_primitive(key, val) if is_binding
44
+ lookup_keys = lookup_keys[(idx + 1)..]
60
45
  fetched = true
61
46
  break
62
47
  end
@@ -86,5 +71,19 @@ module Cel
86
71
  def to_cel_type(v)
87
72
  Literal.to_cel_type(v)
88
73
  end
74
+
75
+ def to_cel_primitive(k, v)
76
+ if @declarations && (type = @declarations[k])
77
+ type = TYPES[type] if type.is_a?(Symbol)
78
+
79
+ if type.is_a?(Class) && type < Protobuf.base_class
80
+ Protobuf.convert_to_proto(type, v)
81
+ else
82
+ type.cast(to_cel_type(v))
83
+ end
84
+ else
85
+ to_cel_type(v)
86
+ end
87
+ end
89
88
  end
90
89
  end
@@ -4,7 +4,12 @@ module Cel
4
4
  class Environment
5
5
  attr_reader :declarations, :package
6
6
 
7
- def initialize(declarations: nil, container: nil, disable_check: false)
7
+ def initialize(
8
+ declarations: nil,
9
+ container: nil,
10
+ disable_check: false,
11
+ **kwargs
12
+ )
8
13
  @disable_check = disable_check
9
14
  @declarations = declarations
10
15
  @container = container
@@ -28,7 +33,7 @@ module Cel
28
33
  @package = Object.const_get(proto_module_name) if Object.const_defined?(proto_module_name)
29
34
  end
30
35
 
31
- @parser = Parser.new(package)
36
+ @parser = Parser.new(package, **kwargs)
32
37
  end
33
38
 
34
39
  def compile(expr)
data/lib/cel/errors.rb CHANGED
@@ -5,6 +5,9 @@ module Cel
5
5
 
6
6
  class ParseError < Error; end
7
7
 
8
+ class MaxRecursionDepthExceededError < ParseError; end
9
+ class MaxNestingDepthExceededError < ParseError; end
10
+
8
11
  class CheckError < Error; end
9
12
 
10
13
  class EvaluateError < Error; end
data/lib/cel/macro.rb CHANGED
@@ -13,12 +13,13 @@ module Cel
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
15
  def has(invoke, program:)
16
- var = invoke.var
17
16
  func = invoke.func
17
+ var = program.disable_cel_conversion do
18
+ program.evaluate(invoke.var)
19
+ end
18
20
 
19
21
  case var
20
- when Message
21
- var = program.evaluate(var)
22
+ when Protobuf.base_class
22
23
  # If e evaluates to a message and f is not a declared field for the message,
23
24
  # has(e.f) raises a no_such_field error.
24
25
  raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
data/lib/cel/parser.rb CHANGED
@@ -89,7 +89,14 @@ var void while
89
89
 
90
90
  IDENTIFIER_REGEX = /[_a-zA-Z][_a-zA-Z0-9]*/
91
91
 
92
- def initialize(package = nil)
92
+ def initialize(
93
+ package = nil,
94
+ max_recursion_depth: 32,
95
+ max_nesting_depth: 12,
96
+ expression_size_limit: 2048
97
+ )
98
+ @max_recursion_depth = max_recursion_depth
99
+ @max_nesting_depth = max_nesting_depth
93
100
  @package = package
94
101
  end
95
102
 
@@ -243,12 +250,49 @@ def literal_null
243
250
  Cel::Null::INSTANCE
244
251
  end
245
252
 
246
- def new_map(val)
247
- Cel::Map.new(val)
253
+ def new_map(elements)
254
+ depth = 1
255
+
256
+ if elements.size > @max_recursion_depth
257
+ raise MaxRecursionDepthExceededError, "max number of map elements exceeded"
258
+ end
259
+
260
+ depth = elements.filter_map do |_, elem|
261
+ next unless elem.is_a?(Cel::Map)
262
+
263
+ d = depth + elem.depth
264
+
265
+ if d > @max_nesting_depth
266
+ raise MaxNestingDepthExceededError, "max nesting of map literals exceeded"
267
+ end
268
+
269
+ d
270
+ end.max || depth
271
+
272
+ Cel::Map.new(elements, depth: depth)
248
273
  end
249
274
 
250
275
  def new_list(val)
251
- Cel::List.new(Array(val))
276
+ elements = Array(val)
277
+ depth = 1
278
+
279
+ if elements.size > @max_recursion_depth
280
+ raise MaxRecursionDepthExceededError, "max number of elements for list literals exceeded"
281
+ end
282
+
283
+ depth = elements.filter_map do |elem|
284
+ next unless elem.is_a?(Cel::List)
285
+
286
+ d = depth + elem.depth
287
+
288
+ if d > @max_nesting_depth
289
+ raise MaxNestingDepthExceededError, "max nesting of list literals exceeded"
290
+ end
291
+
292
+ d
293
+ end.max || depth
294
+
295
+ Cel::List.new(elements, depth: depth)
252
296
  end
253
297
 
254
298
  def new_identifier(id)
@@ -256,19 +300,159 @@ def new_identifier(id)
256
300
  end
257
301
 
258
302
  def new_message(type, struct)
259
- Cel::Message.new(type, struct, package: @package)
303
+ depth = 1
304
+ if struct
305
+ if struct.size > @max_recursion_depth
306
+ raise MaxRecursionDepthExceededError, "max number of message elements exceeded"
307
+ end
308
+
309
+ depth = struct.filter_map do |_, elem|
310
+ next unless elem.is_a?(Cel::Message)
311
+
312
+ d = depth + elem.depth
313
+
314
+ if d > @max_nesting_depth
315
+ raise MaxNestingDepthExceededError, "max nesting of map literals exceeded"
316
+ end
317
+
318
+ d
319
+ end.max || depth
320
+ end
321
+
322
+ Cel::Message.new(type, struct, package: @package, depth: depth)
260
323
  end
261
324
 
262
325
  def global_call(function, args = nil)
263
- Cel::Invoke.new(func: function, var: nil, args: args, package: @package)
326
+ depth = 1
327
+
328
+ if args && args.size > @max_recursion_depth
329
+ raise MaxRecursionDepthExceededError, "max number of function call arguments exceeded"
330
+ end
331
+
332
+ depth = args.filter_map do |arg|
333
+ next unless arg.is_a?(Cel::Invoke)
334
+
335
+ d = depth + arg.depth
336
+
337
+ if d > @max_nesting_depth
338
+ raise MaxNestingDepthExceededError, "max nesting of function calls exceeded"
339
+ end
340
+
341
+ d
342
+ end.max || depth
343
+
344
+ Cel::Invoke.new(func: function, var: nil, args: args, package: @package, depth: depth)
264
345
  end
265
346
 
266
- def receiver_call(function, target, args = nil)
267
- Cel::Invoke.new(func: function, var: target, args: args, package: @package)
347
+ def indexing_call(target, args)
348
+ depth = 1
349
+
350
+ if target.is_a?(Cel::Invoke)
351
+ depth += target.depth if target.indexing?
352
+
353
+ if depth > @max_nesting_depth
354
+ raise MaxNestingDepthExceededError, "max nesting of indexing operators exceeded"
355
+ end
356
+ end
357
+
358
+ receiver_call(:[], target, args, package: @package, depth: depth)
268
359
  end
269
360
 
270
- def operation(op, operands)
271
- Cel::Operation.new(op, operands)
361
+ def selector_call(function, target)
362
+ depth = 1
363
+
364
+ # if selector operator
365
+ if target.is_a?(Cel::Invoke) && !target.var.nil?
366
+ depth += target.depth
367
+
368
+ if depth > @max_nesting_depth
369
+ raise MaxNestingDepthExceededError, "max nesting of selection operators exceeded"
370
+ end
371
+ end
372
+
373
+ receiver_call(function, target, package: @package, depth: depth)
374
+ end
375
+
376
+ def receiver_call(function, target, args = nil, **kwargs)
377
+ Cel::Invoke.new(func: function, var: target, args: args, package: @package, **kwargs)
378
+ end
379
+
380
+ def logical_operation(op, operands)
381
+ lhs, rhs = operands
382
+
383
+ if lhs.is_a?(Cel::Operation) && lhs.op == op
384
+ # concat them
385
+ operands = [*lhs.operands, rhs]
386
+ end
387
+
388
+ if operands.size - 1 > @max_recursion_depth
389
+ raise Cel::MaxRecursionDepthExceededError, "max recursion depth exceeded for #{op} operation"
390
+ end
391
+
392
+ operation(op, operands)
393
+ end
394
+
395
+ def math_operation(op, operands)
396
+ # 32 binary arithmetic operators of the same precedence in a row
397
+ lhs, _ = operands
398
+ depth = 1
399
+
400
+ if lhs.is_a?(Cel::Operation)
401
+ case op
402
+ when "*", "/", "%"
403
+ case lhs.op
404
+ when "*", "/", "%"
405
+ depth = lhs.depth + 1
406
+ end
407
+ when "+", "+"
408
+ case lhs.op
409
+ when "+", "+"
410
+ depth = lhs.depth + 1
411
+ end
412
+ end
413
+ end
414
+
415
+ if depth > @max_recursion_depth
416
+ raise MaxRecursionDepthExceededError, "max number of arithmetic operators with the same precedence exceeded"
417
+ end
418
+
419
+ operation(op, operands, depth: depth)
420
+ end
421
+
422
+ def unary_operation(op, operand)
423
+ depth = 1
424
+
425
+ if operand.is_a?(Cel::Operation) && operand.op == op && operand.unary?
426
+ depth = depth + operand.depth
427
+
428
+ if depth > @max_recursion_depth
429
+ raise MaxRecursionDepthExceededError, "max number of an unary operator exceeded"
430
+ end
431
+ end
432
+
433
+ operation(op, [operand], depth: depth)
434
+ end
435
+
436
+ def operation(op, operands, **kwargs)
437
+ Cel::Operation.new(op, operands, **kwargs)
438
+ end
439
+
440
+ def ternary_condition(if_op, *ops)
441
+ depth = 1
442
+
443
+ depth = ops.filter_map do |op|
444
+ next unless op.is_a?(Cel::Condition)
445
+
446
+ d = depth + op.depth
447
+
448
+ if d > @max_recursion_depth
449
+ raise MaxRecursionDepthExceededError, "max number of consecutive ternary operators exceeded"
450
+ end
451
+
452
+ d
453
+ end.max || depth
454
+
455
+ Cel::Condition.new(if_op, *ops, depth: depth)
272
456
  end
273
457
 
274
458
  # Checks whether the given identifier token is a reserved word or not. Throws
@@ -276,7 +460,7 @@ end
276
460
  def validated_id!(identifier)
277
461
  return identifier unless RESERVED.include?(identifier)
278
462
 
279
- raise Cel::ParseError.new("invalid usage of the reserved word \"#{identifier}\"")
463
+ raise Cel::ParseError, "invalid usage of the reserved word \"#{identifier}\""
280
464
  end
281
465
 
282
466
  ...end parser.ry/module_eval...
@@ -646,7 +830,7 @@ Racc_debug_parser = false
646
830
 
647
831
  module_eval(<<'.,.,', 'parser.ry', 17)
648
832
  def _reduce_4(val, _values, result)
649
- result = Cel::Condition.new(val[0], val[2], val[4])
833
+ result = ternary_condition(val[0], val[2], val[4])
650
834
  result
651
835
  end
652
836
  .,.,
@@ -655,7 +839,7 @@ module_eval(<<'.,.,', 'parser.ry', 17)
655
839
 
656
840
  module_eval(<<'.,.,', 'parser.ry', 20)
657
841
  def _reduce_6(val, _values, result)
658
- result = operation(val[1], [val[0], val[2]])
842
+ result = logical_operation(val[1], [val[0], val[2]])
659
843
  result
660
844
  end
661
845
  .,.,
@@ -664,7 +848,7 @@ module_eval(<<'.,.,', 'parser.ry', 20)
664
848
 
665
849
  module_eval(<<'.,.,', 'parser.ry', 23)
666
850
  def _reduce_8(val, _values, result)
667
- result = operation(val[1], [val[0], val[2]])
851
+ result = logical_operation(val[1], [val[0], val[2]])
668
852
  result
669
853
  end
670
854
  .,.,
@@ -682,14 +866,14 @@ module_eval(<<'.,.,', 'parser.ry', 26)
682
866
 
683
867
  module_eval(<<'.,.,', 'parser.ry', 29)
684
868
  def _reduce_12(val, _values, result)
685
- result = operation(val[1], [val[0], val[2]])
869
+ result = math_operation(val[1], [val[0], val[2]])
686
870
  result
687
871
  end
688
872
  .,.,
689
873
 
690
874
  module_eval(<<'.,.,', 'parser.ry', 30)
691
875
  def _reduce_13(val, _values, result)
692
- result = operation(val[1], [val[0], val[2]])
876
+ result = math_operation(val[1], [val[0], val[2]])
693
877
  result
694
878
  end
695
879
  .,.,
@@ -698,7 +882,7 @@ module_eval(<<'.,.,', 'parser.ry', 30)
698
882
 
699
883
  module_eval(<<'.,.,', 'parser.ry', 33)
700
884
  def _reduce_15(val, _values, result)
701
- result = operation(val[1], [val[0], val[2]])
885
+ result = math_operation(val[1], [val[0], val[2]])
702
886
  result
703
887
  end
704
888
  .,.,
@@ -709,21 +893,21 @@ module_eval(<<'.,.,', 'parser.ry', 33)
709
893
 
710
894
  module_eval(<<'.,.,', 'parser.ry', 38)
711
895
  def _reduce_18(val, _values, result)
712
- result = operation("!", [val[1]])
896
+ result = unary_operation("!", val[1])
713
897
  result
714
898
  end
715
899
  .,.,
716
900
 
717
901
  module_eval(<<'.,.,', 'parser.ry', 39)
718
902
  def _reduce_19(val, _values, result)
719
- result = operation("-", [val[1]])
903
+ result = unary_operation("-", val[1])
720
904
  result
721
905
  end
722
906
  .,.,
723
907
 
724
908
  module_eval(<<'.,.,', 'parser.ry', 41)
725
909
  def _reduce_20(val, _values, result)
726
- result = operation("!", [val[1]])
910
+ result = unary_operation("!", val[1])
727
911
  result
728
912
  end
729
913
  .,.,
@@ -732,7 +916,7 @@ module_eval(<<'.,.,', 'parser.ry', 41)
732
916
 
733
917
  module_eval(<<'.,.,', 'parser.ry', 44)
734
918
  def _reduce_22(val, _values, result)
735
- result = operation("-", [val[1]])
919
+ result = unary_operation("-", val[1])
736
920
  result
737
921
  end
738
922
  .,.,
@@ -743,7 +927,7 @@ module_eval(<<'.,.,', 'parser.ry', 44)
743
927
 
744
928
  module_eval(<<'.,.,', 'parser.ry', 48)
745
929
  def _reduce_25(val, _values, result)
746
- result = receiver_call("[]", val[0], val[2])
930
+ result = indexing_call(val[0], val[2])
747
931
  result
748
932
  end
749
933
  .,.,
@@ -757,7 +941,7 @@ module_eval(<<'.,.,', 'parser.ry', 49)
757
941
 
758
942
  module_eval(<<'.,.,', 'parser.ry', 50)
759
943
  def _reduce_27(val, _values, result)
760
- result = receiver_call(val[2], val[0])
944
+ result = selector_call(val[2], val[0])
761
945
  result
762
946
  end
763
947
  .,.,
data/lib/cel/program.rb CHANGED
@@ -35,6 +35,16 @@ module Cel
35
35
 
36
36
  alias_method :call, :evaluate
37
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
+
38
48
  private
39
49
 
40
50
  def try_evaluate_lookup(key)
@@ -68,21 +78,18 @@ module Cel
68
78
  end
69
79
 
70
80
  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)
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)
80
88
 
81
- return value unless convert_to_cel
89
+ return value unless should_have_converted
82
90
 
83
- Literal.to_cel_type(value)
84
- ensure
85
- @convert_to_cel = convert_to_cel
91
+ Literal.to_cel_type(value)
92
+ end
86
93
  end
87
94
 
88
95
  def evaluate_operation(operation)
data/lib/cel/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -115,7 +115,7 @@ rescue LoadError => e
115
115
  BASE_CLASS = Class.new(Object)
116
116
 
117
117
  def base_class
118
- BASE_CLASS
118
+ Struct
119
119
  end
120
120
 
121
121
  def enum_class
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-06-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -57,8 +57,8 @@ email:
57
57
  executables: []
58
58
  extensions: []
59
59
  extra_rdoc_files:
60
- - LICENSE.txt
61
60
  - CHANGELOG.md
61
+ - LICENSE.txt
62
62
  - README.md
63
63
  files:
64
64
  - CHANGELOG.md
@@ -99,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
99
  - !ruby/object:Gem::Version
100
100
  version: '0'
101
101
  requirements: []
102
- rubygems_version: 3.6.2
102
+ rubygems_version: 3.6.7
103
103
  specification_version: 4
104
104
  summary: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
105
105
  test_files: []