cel 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcb8e069501a6c9cfdf6af67f83701111d67130d04a9208a05482d14ac1c519a
4
- data.tar.gz: 0af115f5924dd63f926227fd916c2fe3f2b25bb6a68e682df1c9f5be36b1be6f
3
+ metadata.gz: 12cebd55174d05db4f154e185872921949f3931817503fccb5dec9f516c3128d
4
+ data.tar.gz: 8fa9c386f343ecfdda97ce99c37296e15378763ab0308270fdc0e789cb4c9ab0
5
5
  SHA512:
6
- metadata.gz: d78064a96c11a026f1b6f16a3ebdfed6f3f528170975ab9c26b1e7438c7805419830418f32e7b6557bafeace936c35f1dc18f695a1294862780c2aff9e0bc335
7
- data.tar.gz: 9897a6ed3a7708cefaf1b6bd24f86802b1c50709cbbb9bab1fd843e31e3dc1f1f049f4770afa0c34ed89c91b30442a95738b30eb52d91e64d99f1dbc880e0de9
6
+ metadata.gz: a46166c14750ef4fc245d1aca362f103d4340ddce4155c51e2f49e03ea64c5d812dccaf61ac073f9f6207995a2f8090a748fe06bf984159542385416a2e9e83a
7
+ data.tar.gz: cfa9d65dd070389fca1ed80817a83baee1efe193cab6830bfd0a3278051a8593a673a4de8e19c19db1a450312cad69ccb91ac49e5db2ee02d18d0efb301319c1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-11-18
4
+
5
+ ### Features
6
+
7
+ `cel-ruby` supports the defined set of [CEL extensions](https://github.com/google/cel-go/blob/master/ext/README.md): math, string, bind and encoders.
8
+
9
+ ```ruby
10
+ Cel::Environment.new.evaluate("math.bitShiftLeft(1, 2) ") #=> returns 4
11
+ ```
12
+
13
+ ### Improvements
14
+
15
+ When the `tzinfo-data` gem is loaded, expressions will support human-friendly timezones, such as "US/Central", for timestamps.
16
+
17
+ ## [0.3.1] - 2025-08-01
18
+
19
+ ### Improvements
20
+
21
+ * 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))`).
22
+ * lazy-evaluating bindings to avoid upfront costly transformations of ruby objects into cel objects which are ultimately not used in expressions.
23
+
24
+ ### Bugfixes
25
+
26
+ * `has()` macro correctly works for mapsl
27
+
3
28
  ## [0.3.0] - 2025-06-12
4
29
 
5
30
  ### 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.
@@ -99,11 +131,11 @@ env.evaluate("google.protobuf.Duration{seconds: 123}.seconds == 123") #=> true
99
131
  `cel-ruby` allows you to define custom functions to be used insde CEL expressions. While we **strongly** recommend usage of `Cel::Function` for defining them (due to the ability of them being used for checking), the only requirement is that the function object responds to `.call`:
100
132
 
101
133
  ```ruby
102
- env = environment(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b})
134
+ env = Cel::Environment.new(declarations: {foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b }})
103
135
  env.evaluate("foo(2, 2)") #=> 4
104
136
 
105
137
  # this is also possible, just not as type-safe
106
- env2 = environment(foo: -> (a, b) { a + b})
138
+ env2 = Cel::Environment.new(declarations: {foo: ->(a, b) { a + b }})
107
139
  env2.evaluate("foo(2, 2)") #=> 4
108
140
  ```
109
141
 
@@ -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
@@ -266,7 +272,7 @@ module Cel
266
272
  %i[+ -].each do |op|
267
273
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
268
274
  def #{op}@
269
- raise EvaluateError, "no such overload" if @type == TYPES[:uint]
275
+ raise NoOverloadError if @type == TYPES[:uint]
270
276
  value = super
271
277
  Number.new(@type, value)
272
278
  end
@@ -358,6 +364,10 @@ module Cel
358
364
  end
359
365
  OUT
360
366
  end
367
+
368
+ def to_s
369
+ inspect
370
+ end
361
371
  end
362
372
 
363
373
  class Bytes < Literal
@@ -392,11 +402,14 @@ module Cel
392
402
  end
393
403
 
394
404
  class List < Literal
395
- def initialize(value)
405
+ attr_reader :depth
406
+
407
+ def initialize(value, depth: 1)
396
408
  value = value.map do |v|
397
409
  Literal.to_cel_type(v)
398
410
  end
399
411
  super(TYPES[:list], value)
412
+ @depth = depth
400
413
  end
401
414
 
402
415
  def [](key)
@@ -429,22 +442,26 @@ module Cel
429
442
  end
430
443
 
431
444
  def by_max_depth
432
- max = @value.max { |a1, a2| depth(a1, 0) <=> depth(a2, 0) }
445
+ max = @value.max { |a1, a2| calc_depth(a1, 0) <=> calc_depth(a2, 0) }
433
446
 
434
447
  # 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))
448
+ return @value.last if (max == @value.first) && (calc_depth(max, 0) == calc_depth(@value.last, 0))
436
449
 
437
450
  max
438
451
  end
439
452
 
453
+ def to_s
454
+ "[#{@value.map(&:to_s).join(", ")}]"
455
+ end
456
+
440
457
  private
441
458
 
442
- def depth(element, acc)
459
+ def calc_depth(element, acc)
443
460
  case element
444
461
  when List
445
- element.value.map { |el| depth(el, acc + 1) }.max || (acc + 1)
462
+ element.value.map { |el| calc_depth(el, acc + 1) }.max || (acc + 1)
446
463
  when Map
447
- element.value.map { |(_, el)| depth(el, acc + 1) }.max || (acc + 1)
464
+ element.value.map { |(_, el)| calc_depth(el, acc + 1) }.max || (acc + 1)
448
465
  else
449
466
  acc
450
467
  end
@@ -453,13 +470,16 @@ module Cel
453
470
 
454
471
  class Map < Literal
455
472
  ALLOWED_TYPES = %i[int uint bool string].map { |typ| TYPES[typ] }.freeze
456
- def initialize(value)
473
+
474
+ attr_reader :depth
475
+
476
+ def initialize(value, depth: 1)
457
477
  # store array internally to support repeated keys
458
478
  value = value.map do |k, v|
459
479
  [Literal.to_cel_type(k), Literal.to_cel_type(v)]
460
480
  end
461
481
  super(TYPES[:map], value)
462
- check
482
+ @depth = depth
463
483
  end
464
484
 
465
485
  def ==(other)
@@ -507,13 +527,19 @@ module Cel
507
527
  end
508
528
 
509
529
  def respond_to_missing?(meth, *args)
510
- super || (@value && @value.any? { |k, _| k.to_s == meth.to_s })
530
+ return true if super
531
+ return false unless @value
532
+
533
+ meth_s = meth.to_s
534
+
535
+ @value.any? { |k, _| k == meth_s }
511
536
  end
512
537
 
513
538
  def method_missing(meth, *args)
514
539
  return super unless @value
515
540
 
516
- values = @value.filter_map { |k, v| v if k.to_s == meth.to_s }
541
+ meth_s = meth.to_s
542
+ values = @value.filter_map { |k, v| v if k == meth_s }
517
543
 
518
544
  return super if values.empty?
519
545
 
@@ -615,6 +641,10 @@ module Cel
615
641
  Protobuf.timestamp_class.from_time(@value)
616
642
  end
617
643
 
644
+ def to_s
645
+ @value.utc.iso8601
646
+ end
647
+
618
648
  private
619
649
 
620
650
  def to_local_time(tz = nil)
@@ -760,14 +790,15 @@ module Cel
760
790
  end
761
791
 
762
792
  class Operation
763
- attr_reader :op, :operands
793
+ attr_reader :op, :operands, :depth
764
794
 
765
795
  attr_accessor :type
766
796
 
767
- def initialize(op, operands)
797
+ def initialize(op, operands, depth: 1)
768
798
  @op = op
769
799
  @operands = operands
770
800
  @type = TYPES[:any]
801
+ @depth = depth
771
802
  end
772
803
 
773
804
  def ==(other)
@@ -795,12 +826,13 @@ module Cel
795
826
  end
796
827
 
797
828
  class Condition
798
- attr_reader :if, :then, :else
829
+ attr_reader :if, :then, :else, :depth
799
830
 
800
- def initialize(if_, then_, else_)
831
+ def initialize(if_, then_, else_, depth: 1)
801
832
  @if = if_
802
833
  @then = then_
803
834
  @else = else_
835
+ @depth = depth
804
836
  end
805
837
 
806
838
  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/checker.rb CHANGED
@@ -21,7 +21,7 @@ module Cel
21
21
  timestamp: %i[int string timestamp],
22
22
  }.freeze
23
23
 
24
- attr_reader :declarations
24
+ attr_reader :environment, :declarations
25
25
 
26
26
  def initialize(environment, declarations = environment.declarations)
27
27
  @environment = environment
@@ -51,6 +51,40 @@ module Cel
51
51
 
52
52
  alias_method :call, :check
53
53
 
54
+ def find_match_all_types(expected, types)
55
+ # at least an expected type must match all values
56
+ type = expected.find do |expected_type|
57
+ case types
58
+ when Array
59
+ types.all? { |typ| typ == expected_type }
60
+ else
61
+ types == expected_type
62
+ end
63
+ end
64
+
65
+ type && types.is_a?(Type) ? types : TYPES[type]
66
+ end
67
+
68
+ def check_arity(func, args, arity, op = :===)
69
+ return if arity.__send__(op, args.size)
70
+
71
+ raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
72
+ end
73
+
74
+ def check_arity_any(func, args)
75
+ return if args.size.positive?
76
+
77
+ raise CheckError, "`#{func}` invoked with no arguments"
78
+ end
79
+
80
+ def unsupported_type(op)
81
+ raise CheckError, "no matching overload: #{op}"
82
+ end
83
+
84
+ def unsupported_operation(op)
85
+ raise CheckError, "unsupported operation (#{op})"
86
+ end
87
+
54
88
  private
55
89
 
56
90
  def merge(declarations)
@@ -194,6 +228,10 @@ module Cel
194
228
 
195
229
  return check_standard_func(funcall) unless var
196
230
 
231
+ if var.is_a?(Identifier) && Cel::EXTENSIONS.include?(var.to_sym)
232
+ return Cel::EXTENSIONS[var.to_sym].__check(funcall, checker: self)
233
+ end
234
+
197
235
  var_type ||= infer_variable_type(var)
198
236
 
199
237
  case var_type
@@ -283,6 +321,8 @@ module Cel
283
321
  var_type.element_type = element_checker.check(predicate)
284
322
  var_type
285
323
  else
324
+ return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
325
+
286
326
  unsupported_operation(funcall)
287
327
  end
288
328
  when TYPES[:string]
@@ -295,6 +335,8 @@ module Cel
295
335
  # TODO: verify if string can be transformed into a regex
296
336
  return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
297
337
  else
338
+ return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
339
+
298
340
  unsupported_type(funcall)
299
341
  end
300
342
  unsupported_operation(funcall)
@@ -373,7 +415,7 @@ module Cel
373
415
 
374
416
  arg = call(args.first)
375
417
 
376
- return TYPES[func] if find_match_all_types(allowed_types, arg)
418
+ return TYPES[func] if find_match_all_types(allowed_types, arg) || arg == TYPES[:any]
377
419
  when :matches
378
420
  check_arity(func, args, 2)
379
421
  return TYPES[:bool] if find_match_all_types(%i[string], args.map(&method(:call)))
@@ -424,7 +466,9 @@ module Cel
424
466
  def check_identifier(identifier)
425
467
  return identifier.type unless identifier.type == :any
426
468
 
427
- return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
469
+ id_sym = identifier.to_sym
470
+
471
+ return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(id_sym)
428
472
 
429
473
  proto_type = identifier.try_convert_to_proto_type
430
474
 
@@ -472,33 +516,5 @@ module Cel
472
516
  typ
473
517
  end
474
518
  end
475
-
476
- def find_match_all_types(expected, types)
477
- # at least an expected type must match all values
478
- type = expected.find do |expected_type|
479
- case types
480
- when Array
481
- types.all? { |typ| typ == expected_type }
482
- else
483
- types == expected_type
484
- end
485
- end
486
-
487
- type && types.is_a?(Type) ? types : TYPES[type]
488
- end
489
-
490
- def check_arity(func, args, arity)
491
- return if arity === args.size # rubocop:disable Style/CaseEquality
492
-
493
- raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
494
- end
495
-
496
- def unsupported_type(op)
497
- raise CheckError, "no matching overload: #{op}"
498
- end
499
-
500
- def unsupported_operation(op)
501
- raise CheckError, "unsupported operation (#{op})"
502
- end
503
519
  end
504
520
  end
data/lib/cel/context.rb CHANGED
@@ -2,28 +2,11 @@
2
2
 
3
3
  module Cel
4
4
  class Context
5
- attr_reader :declarations
5
+ attr_reader :declarations, :bindings
6
6
 
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)
@@ -48,8 +53,8 @@ module Cel
48
53
  end
49
54
 
50
55
  def check(expr)
51
- ast = @parser.parse(expr)
52
- @checker.check(ast)
56
+ expr = @parser.parse(expr) if expr.is_a?(::String)
57
+ @checker.check(expr)
53
58
  end
54
59
 
55
60
  def program(expr)
@@ -86,6 +91,19 @@ module Cel
86
91
  [declarations, bindings]
87
92
  end
88
93
 
94
+ def with(declarations:, disable_check: @disable_check)
95
+ prev_declarations = @declarations
96
+ prev_disable_check = @disable_check
97
+
98
+ @declarations = declarations.merge(declarations)
99
+ @disable_check = disable_check
100
+
101
+ yield
102
+ ensure
103
+ @declarations = prev_declarations
104
+ @disable_check = prev_disable_check
105
+ end
106
+
89
107
  private
90
108
 
91
109
  def validate(ast, structs); end