cel 0.4.1 → 0.5.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: e4d35d9012196d61497f40e35c8e9dd9b1a9d7e1e922078a414cd574eacc439a
4
- data.tar.gz: f2a0e48bc07e0c0b5eb23c7ea6d709a1b56fff3400b51204d26c3031f11db696
3
+ metadata.gz: 5e83a98ac5e076442ae06b53d13f43581a3982196f57582bbac02a08eef46ca9
4
+ data.tar.gz: 1fdb06029d13f3a47037688df7d4d4a77f65e237aa73d0e00fb6989a78600000
5
5
  SHA512:
6
- metadata.gz: d39c028b26513ed27749f5bd7c5cfa57281b0433a5b5984418d657fd61441122e4cddae1942cdb7b3e3c9aeb0979d7387ca5d7444cea0afd04477615278acbfa
7
- data.tar.gz: 442da2f399685e77bee5f743c693f0ef0fe9b333dfea333f61b17f8419a6bf7752aca14e0c5211ff0a8a526bc36a9a575a9e0cac53190f47e3af0b54097c26de
6
+ metadata.gz: 59eb650749f3b0fcef9a8ee66b6850831db66f116d851f35894d375191e280cac465e48c1f39103d6fcb21f7ebcf6c695f2cd286c65bcfb6c12ef08ddcb07400
7
+ data.tar.gz: b6dada73b82d27a17a6d9f23a672f36ec7c9af6f89eebef788227aba7b22cdc88720100ffcf5f466db70e923db689ff4df903942abb7ab2f8adeef70de4604df
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2025-12-11
4
+
5
+ ### Features
6
+
7
+ #### Custom extensions
8
+
9
+ A new `:extensions` kwarg is added to `Cel::Environment.new` which allows adding custom extensions, in a similar manner as what the standard extensions (like `math` or `string`) are done:
10
+
11
+ ```ruby
12
+ module Ext
13
+ # defines a random function which takes no arguments and returns 42
14
+ end
15
+
16
+ Cel::Environment.new.evaluate("ext.random()") #=> raises error
17
+ Cel::Environment.new(extensions: { ext: Ext }).evaluate("ext.random()") #=> 42
18
+ ```
19
+
20
+ ### Backwards Compatibility
21
+
22
+ The ractor safety introduced in 0.4.1 has been relaxed in order to allow extensions of core classes by custom extensions, And you'll need to explicitly call `Cel.freeze` before using `cel` inside ractors. This is a direct consequence of how extensions patch `cel` core classes.
23
+
24
+ ATTENTION: Changes may be introduced in the way core classes are patched by extensions, towards making `cel` ractor-safe by default. If you rely on custom extensions, do follow the migration instructions in subsequent releases.
25
+
26
+ ### Bugfixes
27
+
28
+ Fixed checker type inference when using nexted expressions (like when using the `bind` extensions to evaluate cel sub-expressions).
29
+
3
30
  ## [0.4.1] - 2025-11-25
4
31
 
5
32
  ### Improvements
data/README.md CHANGED
@@ -162,6 +162,35 @@ res = env.evaluate('tuple(1, 2)') #=> Tuple instance
162
162
  res.value #=> [1, 2]
163
163
  ```
164
164
 
165
+ ### Custom Extensions
166
+
167
+ `cel` already supports the [conformance spec extensions packages](https://pkg.go.dev/github.com/google/cel-go/ext#section-readme). However, if you need to add your own, you can do so:
168
+
169
+ ```ruby
170
+ module Ext
171
+ def __check(funcall, checker:)
172
+ func = funcall.func
173
+ args = funcall.args
174
+
175
+ case func
176
+ when :random
177
+ checker.check_arity(func, args, 0)
178
+ return TYPES[:int]
179
+ else
180
+ checker.unsupported_operation(funcall)
181
+ end
182
+ end
183
+
184
+ # extensions will always receive the program instance as a kwarg
185
+ def random(program:)
186
+ 42
187
+ end
188
+ end
189
+
190
+ env = Cel::Environment.new(extensions: { ext: Ext})
191
+ env.evaluate("ext.random()") #=> 42
192
+ ```
193
+
165
194
  ## Spec Coverage
166
195
 
167
196
  `cel` is tested against the conformance suite from the [cel-spec repository](https://github.com/google/cel-spec/tree/master/conformance), and supports all features from the language except:
@@ -180,6 +209,14 @@ If this is something you're interested in (helping out), add a mention in the co
180
209
 
181
210
  All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
182
211
 
212
+ `cel` can be used inside ractors, but you need to freeze it first:
213
+
214
+ ```ruby
215
+ # can't be used in ractors
216
+ Cel.freeze
217
+ # can be used in ractors
218
+ ```
219
+
183
220
  ## Development
184
221
 
185
222
  Clone the repo in your local machine, where you have `ruby` installed. Then you can:
data/lib/cel/checker.rb CHANGED
@@ -85,12 +85,12 @@ module Cel
85
85
  raise CheckError, "unsupported operation (#{op})"
86
86
  end
87
87
 
88
- private
89
-
90
88
  def merge(declarations)
91
89
  Checker.new(@environment, @declarations ? @declarations.merge(declarations) : declarations)
92
90
  end
93
91
 
92
+ private
93
+
94
94
  def check_literal(literal)
95
95
  case literal
96
96
  when List
@@ -228,8 +228,8 @@ module Cel
228
228
 
229
229
  return check_standard_func(funcall) unless var
230
230
 
231
- if var.is_a?(Identifier) && Cel::EXTENSIONS.include?(var.to_sym)
232
- return Cel::EXTENSIONS[var.to_sym].__check(funcall, checker: self)
231
+ if var.is_a?(Identifier) && @environment.extensions.include?(var.to_sym)
232
+ return @environment.extensions[var.to_sym].__check(funcall, checker: self)
233
233
  end
234
234
 
235
235
  var_type ||= infer_variable_type(var)
@@ -321,7 +321,9 @@ module Cel
321
321
  var_type.element_type = element_checker.check(predicate)
322
322
  var_type
323
323
  else
324
- return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
324
+ if @environment.extensions.key?(:strings)
325
+ return @environment.extensions[:strings].__check(funcall, checker: self)
326
+ end
325
327
 
326
328
  unsupported_operation(funcall)
327
329
  end
@@ -335,7 +337,9 @@ module Cel
335
337
  # TODO: verify if string can be transformed into a regex
336
338
  return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
337
339
  else
338
- return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
340
+ if @environment.extensions.key?(:strings)
341
+ return @environment.extensions[:strings].__check(funcall, checker: self)
342
+ end
339
343
 
340
344
  unsupported_type(funcall)
341
345
  end
data/lib/cel/context.rb CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  module Cel
4
4
  class Context
5
- attr_reader :declarations, :bindings
5
+ attr_reader :bindings
6
6
 
7
- def initialize(declarations, bindings)
8
- @declarations = declarations
7
+ def initialize(environment, bindings)
8
+ @environment = environment
9
9
  @bindings = bindings.dup
10
10
  end
11
11
 
12
+ def declarations
13
+ @environment.declarations
14
+ end
15
+
12
16
  def lookup(identifier)
13
17
  raise EvaluateError, "no value in context for #{identifier}" unless @bindings
14
18
 
@@ -63,7 +67,7 @@ module Cel
63
67
  end
64
68
 
65
69
  def merge(bindings)
66
- Context.new(@declarations, @bindings ? @bindings.merge(bindings) : bindings)
70
+ Context.new(@environment, @bindings ? @bindings.merge(bindings) : bindings)
67
71
  end
68
72
 
69
73
  private
@@ -73,7 +77,7 @@ module Cel
73
77
  end
74
78
 
75
79
  def to_cel_primitive(k, v)
76
- if @declarations && (type = @declarations[k])
80
+ if @environment.declarations && (type = @environment.declarations[k])
77
81
  type = TYPES[type] if type.is_a?(Symbol)
78
82
 
79
83
  if type.is_a?(Class) && type < Protobuf.base_class
@@ -2,17 +2,19 @@
2
2
 
3
3
  module Cel
4
4
  class Environment
5
- attr_reader :declarations, :package
5
+ attr_reader :declarations, :extensions, :package
6
6
 
7
7
  def initialize(
8
8
  declarations: nil,
9
9
  container: nil,
10
+ extensions: nil,
10
11
  disable_check: false,
11
12
  **kwargs
12
13
  )
13
14
  @disable_check = disable_check
14
15
  @declarations = declarations
15
16
  @container = container
17
+ @extensions = extensions ? EXTENSIONS.merge(extensions) : EXTENSIONS
16
18
  @package = nil
17
19
  @checker = Checker.new(self)
18
20
 
@@ -65,10 +67,12 @@ module Cel
65
67
 
66
68
  def evaluate(expr, bindings = nil)
67
69
  declarations, bindings = process_bindings(bindings)
68
- context = Context.new(declarations, bindings)
70
+ context = Context.new(self, bindings)
69
71
  expr = @parser.parse(expr) if expr.is_a?(::String)
70
72
  @checker.check(expr) unless @disable_check
71
- Program.new(context, self).evaluate(expr)
73
+ with(declarations: declarations) do
74
+ Program.new(context, self).evaluate(expr)
75
+ end
72
76
  end
73
77
 
74
78
  def process_bindings(bindings)
@@ -93,14 +97,17 @@ module Cel
93
97
 
94
98
  def with(declarations:, disable_check: @disable_check)
95
99
  prev_declarations = @declarations
100
+ prev_checker = @checker
96
101
  prev_disable_check = @disable_check
97
102
 
98
- @declarations = declarations.merge(declarations)
103
+ @declarations = prev_declarations ? prev_declarations.merge(declarations) : declarations
104
+ @checker = prev_checker.merge(@declarations)
99
105
  @disable_check = disable_check
100
106
 
101
107
  yield
102
108
  ensure
103
109
  @declarations = prev_declarations
110
+ @checker = prev_checker
104
111
  @disable_check = prev_disable_check
105
112
  end
106
113
 
@@ -116,8 +123,8 @@ module Cel
116
123
  end
117
124
 
118
125
  def evaluate(bindings = nil)
119
- declarations, bindings = @environment.process_bindings(bindings)
120
- context = Context.new(declarations, bindings)
126
+ _, bindings = @environment.process_bindings(bindings)
127
+ context = Context.new(@environment, bindings)
121
128
  Program.new(context, @environment).evaluate(@ast)
122
129
  end
123
130
  end
@@ -19,15 +19,14 @@ module Cel
19
19
 
20
20
  type = checker.call(type)
21
21
 
22
- checker.environment.with(declarations: { id => type }) do
22
+ checker.environment.with(declarations: { id.to_sym => type }) do
23
23
  checker.environment.check(expr)
24
24
  end
25
25
  end
26
26
 
27
27
  def bind(var, val, expr, program:)
28
28
  program.environment.with(declarations: { var.to_sym => val.type }, disable_check: false) do
29
- bindings = program.context.bindings.merge({ var.to_sym => val })
30
- program.environment.evaluate(expr, bindings)
29
+ program.with_extra_context({ var.to_sym => val }).evaluate(expr)
31
30
  end
32
31
  end
33
32
  end
data/lib/cel/program.rb CHANGED
@@ -57,8 +57,8 @@ module Cel
57
57
  def evaluate_identifier(identifier)
58
58
  id_sym = identifier.to_sym
59
59
 
60
- if Cel::EXTENSIONS.key?(id_sym)
61
- Cel::EXTENSIONS[id_sym]
60
+ if @environment.extensions.key?(id_sym)
61
+ @environment.extensions[id_sym]
62
62
  elsif Cel::PRIMITIVE_TYPES.include?(id_sym)
63
63
  TYPES[identifier.to_sym]
64
64
  else
data/lib/cel/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -255,16 +255,19 @@ module Cel
255
255
  end.join("::")
256
256
  end
257
257
 
258
- # freeze constants to make cel ractor-friendly
259
- Number.freeze
260
- Bool.freeze
261
- Null.freeze
262
- String.freeze
263
- Bytes.freeze
264
- List.freeze
265
- Map.freeze
266
- Timestamp.freeze
267
- Duration.freeze
268
- Type.freeze
269
- TYPES.each_value(&:freeze).freeze
258
+ def freeze
259
+ # freeze constants to make cel ractor-friendly
260
+ Number.freeze
261
+ Bool.freeze
262
+ Null.freeze
263
+ String.freeze
264
+ Bytes.freeze
265
+ List.freeze
266
+ Map.freeze
267
+ Timestamp.freeze
268
+ Duration.freeze
269
+ Type.freeze
270
+ TYPES.each_value(&:freeze).freeze
271
+ super
272
+ end
270
273
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso