cel 0.4.1 → 0.6.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: 1bf9b40df18d849d9a964e668c0f1aee0d82f02ccd2de536dcfa73c8d714c718
4
+ data.tar.gz: 0f348c6b1a4a09362d76db927aec94099fc2ac2b355ddcef963b72fa90549e07
5
5
  SHA512:
6
- metadata.gz: d39c028b26513ed27749f5bd7c5cfa57281b0433a5b5984418d657fd61441122e4cddae1942cdb7b3e3c9aeb0979d7387ca5d7444cea0afd04477615278acbfa
7
- data.tar.gz: 442da2f399685e77bee5f743c693f0ef0fe9b333dfea333f61b17f8419a6bf7752aca14e0c5211ff0a8a526bc36a9a575a9e0cac53190f47e3af0b54097c26de
6
+ metadata.gz: 936e0ea05567de1932de8a887dee32f3682cbdccc574999772283a98acef7ce47d4ad51446eb35a8bcb5195100dc0a3197ec7fb4a82f8ddc239aab47ba0f40c1
7
+ data.tar.gz: d346ef64b5f777b1e368f234a43ad69cb2759313134558ef9760e68933e61a2e1f0644b01ad46b85702017955777ceebe31bce28c410ddbbbca49639eeeb1120
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-02-28
4
+
5
+ ### Features
6
+
7
+ #### Network Extension
8
+
9
+ Support the network extension, as defined [here](https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#IP).
10
+
11
+ ### Improvements
12
+
13
+ #### support top-level lookup (ex: '.y')
14
+
15
+ also, make sure than, on lookup, keys under the passed container take priority, except in the case where the lookup is top-level
16
+ (i.e. '.y').
17
+
18
+ at the same time, #merge is refactored in order to:
19
+
20
+ * normalize nested bindings into aggregated keys ({a:{b:1}} => {:"a.b"=> 1}
21
+ * override container-prefixed variables with the local binding
22
+ * deal with conflicts by moving them to top level.
23
+
24
+ ## [0.5.0] - 2025-12-11
25
+
26
+ ### Features
27
+
28
+ #### Custom extensions
29
+
30
+ 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:
31
+
32
+ ```ruby
33
+ module Ext
34
+ # defines a random function which takes no arguments and returns 42
35
+ end
36
+
37
+ Cel::Environment.new.evaluate("ext.random()") #=> raises error
38
+ Cel::Environment.new(extensions: { ext: Ext }).evaluate("ext.random()") #=> 42
39
+ ```
40
+
41
+ ### Backwards Compatibility
42
+
43
+ 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.
44
+
45
+ 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.
46
+
47
+ ### Bugfixes
48
+
49
+ Fixed checker type inference when using nexted expressions (like when using the `bind` extensions to evaluate cel sub-expressions).
50
+
3
51
  ## [0.4.1] - 2025-11-25
4
52
 
5
53
  ### 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:
@@ -171,6 +200,7 @@ res.value #=> [1, 2]
171
200
  * bindings extensions
172
201
  * block extensions
173
202
  * encoders extensions
203
+ * network extensions
174
204
  * comprehensions V2 API
175
205
  * optionals
176
206
 
@@ -180,6 +210,14 @@ If this is something you're interested in (helping out), add a mention in the co
180
210
 
181
211
  All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
182
212
 
213
+ `cel` can be used inside ractors, but you need to freeze it first:
214
+
215
+ ```ruby
216
+ # can't be used in ractors
217
+ Cel.freeze
218
+ # can be used in ractors
219
+ ```
220
+
183
221
  ## Development
184
222
 
185
223
  Clone the repo in your local machine, where you have `ruby` installed. Then you can:
@@ -38,10 +38,10 @@ module Cel
38
38
  if indexing?
39
39
  "#{var}[#{args}]"
40
40
  else
41
- "#{var}.#{func}#{"(#{args.map(&:to_s).join(", ")})" if args}"
41
+ "#{var}.#{func}#{"(#{args.join(", ")})" if args}"
42
42
  end
43
43
  else
44
- "#{func}#{"(#{args.map(&:to_s).join(", ")})" if args}"
44
+ "#{func}#{"(#{args.join(", ")})" if args}"
45
45
  end
46
46
  end
47
47
 
@@ -51,7 +51,7 @@ module Cel
51
51
  end
52
52
 
53
53
  def to_s
54
- "[#{@value.map(&:to_s).join(", ")}]"
54
+ "[#{@value.map(&:to_s).join(", ")}]" # rubocop:disable Style/MapJoin
55
55
  end
56
56
 
57
57
  private
@@ -318,7 +318,7 @@ module Cel
318
318
  protoclass = var.split(".").last
319
319
  protoclass = Google::Protobuf.const_get(protoclass)
320
320
 
321
- value = if args.nil? && protoclass.constants.include?(func.to_sym)
321
+ value = if args.nil? && protoclass.const_defined?(func.to_sym)
322
322
  protoclass.const_get(func)
323
323
  else
324
324
  protoclass.__send__(func, *args)
@@ -340,7 +340,7 @@ module Cel
340
340
  when :repeated
341
341
  case value
342
342
  when Google::Protobuf::Map
343
- value.to_h { |k, v| [k, convert_from_proto_field_type(field, v)] } # rubocop:disable Style/HashTransformValues
343
+ value.to_h { |k, v| [k, convert_from_proto_field_type(field, v)] }
344
344
  else
345
345
  value.map { |v| convert_from_proto_field_type(field, v) }
346
346
  end
data/lib/cel/ast/types.rb CHANGED
@@ -107,7 +107,7 @@ module Cel
107
107
  when :duration
108
108
  String.new(value.to_s)
109
109
  else
110
- String.new(String(value.value))
110
+ String.new(String(value))
111
111
  end
112
112
  when :bytes
113
113
  case type
data/lib/cel/checker.rb CHANGED
@@ -11,14 +11,14 @@ module Cel
11
11
  BOOLABLE_OPERATORS = %w[&& || == != < <= >= >].freeze
12
12
 
13
13
  CAST_ALLOWED_TYPES = {
14
- int: %i[int uint double string timestamp], # TODO: enum
15
- uint: %i[int uint double string],
16
- string: %i[int uint double bytes bool bytes string timestamp duration],
17
- double: %i[double int uint string],
18
- bytes: %i[bytes string],
19
- bool: %i[bool string],
20
- duration: %i[string duration],
21
- timestamp: %i[int string timestamp],
14
+ int: %i[int uint double string timestamp].freeze, # TODO: enum
15
+ uint: %i[int uint double string].freeze,
16
+ string: %i[int uint double bytes bool bytes string timestamp duration].freeze,
17
+ double: %i[double int uint string].freeze,
18
+ bytes: %i[bytes string].freeze,
19
+ bool: %i[bool string].freeze,
20
+ duration: %i[string duration].freeze,
21
+ timestamp: %i[int string timestamp].freeze,
22
22
  }.freeze
23
23
 
24
24
  attr_reader :environment, :declarations
@@ -26,6 +26,15 @@ module Cel
26
26
  def initialize(environment, declarations = environment.declarations)
27
27
  @environment = environment
28
28
  @declarations = declarations
29
+ @cast_allowed_types = CAST_ALLOWED_TYPES.dup
30
+ @environment.extensions.each_value do |ext|
31
+ next unless ext.const_defined?(:CAST_ALLOWED_TYPES)
32
+
33
+ ext.const_get(:CAST_ALLOWED_TYPES).each do |type, allowed_types|
34
+ @cast_allowed_types[type] ||= []
35
+ @cast_allowed_types[type] += allowed_types
36
+ end
37
+ end
29
38
  end
30
39
 
31
40
  def check(ast)
@@ -66,6 +75,8 @@ module Cel
66
75
  end
67
76
 
68
77
  def check_arity(func, args, arity, op = :===)
78
+ return arity.zero? if args.nil?
79
+
69
80
  return if arity.__send__(op, args.size)
70
81
 
71
82
  raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
@@ -85,12 +96,12 @@ module Cel
85
96
  raise CheckError, "unsupported operation (#{op})"
86
97
  end
87
98
 
88
- private
89
-
90
99
  def merge(declarations)
91
100
  Checker.new(@environment, @declarations ? @declarations.merge(declarations) : declarations)
92
101
  end
93
102
 
103
+ private
104
+
94
105
  def check_literal(literal)
95
106
  case literal
96
107
  when List
@@ -228,8 +239,8 @@ module Cel
228
239
 
229
240
  return check_standard_func(funcall) unless var
230
241
 
231
- if var.is_a?(Identifier) && Cel::EXTENSIONS.include?(var.to_sym)
232
- return Cel::EXTENSIONS[var.to_sym].__check(funcall, checker: self)
242
+ if var.is_a?(Identifier) && @environment.extensions.include?(var.to_sym)
243
+ return @environment.extensions[var.to_sym].__check(funcall, checker: self)
233
244
  end
234
245
 
235
246
  var_type ||= infer_variable_type(var)
@@ -321,7 +332,9 @@ module Cel
321
332
  var_type.element_type = element_checker.check(predicate)
322
333
  var_type
323
334
  else
324
- return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
335
+ if @environment.extensions.key?(:strings)
336
+ return @environment.extensions[:strings].__check(funcall, checker: self)
337
+ end
325
338
 
326
339
  unsupported_operation(funcall)
327
340
  end
@@ -335,7 +348,9 @@ module Cel
335
348
  # TODO: verify if string can be transformed into a regex
336
349
  return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
337
350
  else
338
- return Cel::EXTENSIONS[:strings].__check(funcall, checker: self) if Cel::EXTENSIONS.key?(:strings)
351
+ if @environment.extensions.key?(:strings)
352
+ return @environment.extensions[:strings].__check(funcall, checker: self)
353
+ end
339
354
 
340
355
  unsupported_type(funcall)
341
356
  end
@@ -384,6 +399,12 @@ module Cel
384
399
  func = funcall.func
385
400
  args = funcall.args
386
401
 
402
+ @environment.implicit_extensions.each do |ext|
403
+ if (typ = ext.__check(funcall, checker: self))
404
+ return typ
405
+ end
406
+ end
407
+
387
408
  # check if protobuf conversion
388
409
  if (proto_type = Protobuf.convert_to_proto_type(func.to_s, @environment.package))
389
410
  # TODO: fixit
@@ -409,9 +430,9 @@ module Cel
409
430
 
410
431
  arg = call(args.first)
411
432
  return TYPES[:int] if find_match_all_types(%i[string bytes list map], arg)
412
- when *CAST_ALLOWED_TYPES.keys
433
+ when *@cast_allowed_types.keys
413
434
  check_arity(func, args, 1)
414
- allowed_types = CAST_ALLOWED_TYPES[func]
435
+ allowed_types = @cast_allowed_types[func]
415
436
 
416
437
  arg = call(args.first)
417
438
 
data/lib/cel/context.rb CHANGED
@@ -2,22 +2,60 @@
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
 
19
+ top_level_key = nil
15
20
  id = identifier.to_s
16
21
 
17
22
  lookup_keys = id.split(".")
18
23
 
19
24
  val = @bindings
20
25
 
26
+ if lookup_keys.first.empty?
27
+ # key starts with ".", i.e. ".y"
28
+ val = val.dup
29
+ lookup_keys.shift
30
+ top_level_key = key = lookup_keys.first
31
+
32
+ if val.respond_to?(:key?)
33
+ val = val.dup
34
+ val.keys.select { |k| k.start_with?(".#{key}") }.each do |k|
35
+ val[k.to_s[1..-1].to_sym] = val.delete(k) # rubocop:disable Style/SlicingWithRange
36
+ end
37
+ # else do nothing
38
+ end
39
+ end
40
+
41
+ if (container = @environment.container)
42
+ # ex move data from "example.com.y" to "y"
43
+ container_keys = val.keys.select { |k| k.start_with?(container) }
44
+
45
+ unless container_keys.empty?
46
+ val = val.dup
47
+ container_keys.each do |key|
48
+ skey = key.to_s.delete_prefix("#{container}.")
49
+
50
+ # forego editing if this is a top level lookup and the key being
51
+ # overwritten would be one
52
+ next if top_level_key && skey.start_with?(top_level_key)
53
+
54
+ val[skey.to_sym] = val.delete(key)
55
+ end
56
+ end
57
+ end
58
+
21
59
  loop do
22
60
  fetched = false
23
61
  (0...lookup_keys.size).reverse_each do |idx|
@@ -62,8 +100,38 @@ module Cel
62
100
  [val, lookup_keys.empty? ? nil : lookup_keys.join(".")]
63
101
  end
64
102
 
103
+ NORMALIZE_KEYS = lambda do |k, v|
104
+ if v.is_a?(Hash) || v.is_a?(Map)
105
+ v.flat_map do |k2, v2|
106
+ NORMALIZE_KEYS.call(:"#{k}.#{String(k2)}", v2)
107
+ end
108
+ else
109
+ [k, v]
110
+ end
111
+ end
112
+
65
113
  def merge(bindings)
66
- Context.new(@declarations, @bindings ? @bindings.merge(bindings) : bindings)
114
+ bindings = bindings.to_h { |k, v| NORMALIZE_KEYS[k, v] }
115
+ if (old_bindings = @bindings)
116
+ if @environment.container
117
+ old_bindings = old_bindings.dup
118
+
119
+ bindings.each_key do |key|
120
+ container_key = :"#{@environment.container}.#{key}"
121
+
122
+ old_bindings.delete(container_key) if old_bindings.key?(container_key)
123
+ end
124
+ end
125
+ conflicts = []
126
+ bindings = old_bindings.merge(bindings) do |k, old, new|
127
+ conflicts << [k, old]
128
+ new
129
+ end
130
+ conflicts.each do |k, v|
131
+ bindings[:".#{k}"] = v
132
+ end
133
+ end
134
+ Context.new(@environment, bindings)
67
135
  end
68
136
 
69
137
  private
@@ -73,7 +141,7 @@ module Cel
73
141
  end
74
142
 
75
143
  def to_cel_primitive(k, v)
76
- if @declarations && (type = @declarations[k])
144
+ if @environment.declarations && (type = @environment.declarations[k])
77
145
  type = TYPES[type] if type.is_a?(Symbol)
78
146
 
79
147
  if type.is_a?(Class) && type < Protobuf.base_class
@@ -2,20 +2,23 @@
2
2
 
3
3
  module Cel
4
4
  class Environment
5
- attr_reader :declarations, :package
5
+ attr_reader :declarations, :extensions, :package, :container
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
- @container = container
16
- @package = nil
16
+ @extensions = extensions ? EXTENSIONS.merge(extensions) : EXTENSIONS
17
+ @package = @container = nil
17
18
  @checker = Checker.new(self)
18
19
 
20
+ @container = container if container && !container.empty?
21
+
19
22
  if container && !container.empty?
20
23
 
21
24
  module_dir = container.split(".")
@@ -36,6 +39,10 @@ module Cel
36
39
  @parser = Parser.new(package, **kwargs)
37
40
  end
38
41
 
42
+ def implicit_extensions
43
+ @implicit_extensions ||= @extensions.select { |_, ext| ext.implicit? }.values
44
+ end
45
+
39
46
  def compile(expr)
40
47
  ast = @parser.parse(expr)
41
48
  @checker.check(ast)
@@ -65,10 +72,12 @@ module Cel
65
72
 
66
73
  def evaluate(expr, bindings = nil)
67
74
  declarations, bindings = process_bindings(bindings)
68
- context = Context.new(declarations, bindings)
75
+ context = Context.new(self, bindings)
69
76
  expr = @parser.parse(expr) if expr.is_a?(::String)
70
77
  @checker.check(expr) unless @disable_check
71
- Program.new(context, self).evaluate(expr)
78
+ with(declarations: declarations) do
79
+ Program.new(context, self).evaluate(expr)
80
+ end
72
81
  end
73
82
 
74
83
  def process_bindings(bindings)
@@ -84,8 +93,10 @@ module Cel
84
93
  cbinding_keys.each do |k|
85
94
  cbinding = k.to_s.delete_prefix(prefix).to_sym
86
95
 
87
- bindings[cbinding] = bindings.delete(k)
88
- declarations[cbinding] = declarations.delete(k) if declarations.key?(k)
96
+ unless bindings.key?(cbinding)
97
+ bindings[cbinding] = bindings.delete(k)
98
+ declarations[cbinding] = declarations.delete(k) if declarations.key?(k)
99
+ end
89
100
  end
90
101
 
91
102
  [declarations, bindings]
@@ -93,14 +104,17 @@ module Cel
93
104
 
94
105
  def with(declarations:, disable_check: @disable_check)
95
106
  prev_declarations = @declarations
107
+ prev_checker = @checker
96
108
  prev_disable_check = @disable_check
97
109
 
98
- @declarations = declarations.merge(declarations)
110
+ @declarations = prev_declarations ? prev_declarations.merge(declarations) : declarations
111
+ @checker = prev_checker.merge(@declarations)
99
112
  @disable_check = disable_check
100
113
 
101
114
  yield
102
115
  ensure
103
116
  @declarations = prev_declarations
117
+ @checker = prev_checker
104
118
  @disable_check = prev_disable_check
105
119
  end
106
120
 
@@ -116,8 +130,8 @@ module Cel
116
130
  end
117
131
 
118
132
  def evaluate(bindings = nil)
119
- declarations, bindings = @environment.process_bindings(bindings)
120
- context = Context.new(declarations, bindings)
133
+ _, bindings = @environment.process_bindings(bindings)
134
+ context = Context.new(@environment, bindings)
121
135
  Program.new(context, @environment).evaluate(@ast)
122
136
  end
123
137
  end
@@ -5,6 +5,10 @@ module Cel
5
5
  module Bind
6
6
  module_function
7
7
 
8
+ def implicit?
9
+ false
10
+ end
11
+
8
12
  def __check(funcall, checker:)
9
13
  func = funcall.func
10
14
  args = funcall.args
@@ -19,15 +23,14 @@ module Cel
19
23
 
20
24
  type = checker.call(type)
21
25
 
22
- checker.environment.with(declarations: { id => type }) do
26
+ checker.environment.with(declarations: { id.to_sym => type }) do
23
27
  checker.environment.check(expr)
24
28
  end
25
29
  end
26
30
 
27
31
  def bind(var, val, expr, program:)
28
32
  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)
33
+ program.with_extra_context({ var.to_sym => val }).evaluate(expr)
31
34
  end
32
35
  end
33
36
  end
@@ -6,6 +6,10 @@ module Cel
6
6
  module Base64
7
7
  module_function
8
8
 
9
+ def implicit?
10
+ false
11
+ end
12
+
9
13
  def __check(funcall, checker:)
10
14
  func = funcall.func
11
15
  args = funcall.args
@@ -5,6 +5,10 @@ module Cel
5
5
  module Math
6
6
  module_function
7
7
 
8
+ def implicit?
9
+ false
10
+ end
11
+
8
12
  def __check(funcall, checker:)
9
13
  func = funcall.func
10
14
  args = funcall.args
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "ipaddr"
5
+
6
+ module Cel
7
+ module Extensions
8
+ module Network
9
+ module_function
10
+
11
+ def implicit?
12
+ false
13
+ end
14
+
15
+ def __check(funcall, checker:)
16
+ funcall.var
17
+ func = funcall.func
18
+ args = funcall.args
19
+
20
+ case func
21
+ when :IP, :CIDR
22
+ checker.check_arity(func, args, 0)
23
+ TYPES[:type]
24
+ end
25
+ end
26
+
27
+ def IP(*) # rubocop:disable Naming/MethodName
28
+ TYPES[:ip]
29
+ end
30
+
31
+ def CIDR(*) # rubocop:disable Naming/MethodName
32
+ TYPES[:cidr]
33
+ end
34
+
35
+ module IP
36
+ module_function
37
+
38
+ CAST_ALLOWED_TYPES = {
39
+ string: %i[ip cidr].freeze,
40
+ }.freeze
41
+
42
+ def implicit?
43
+ true
44
+ end
45
+
46
+ def __check(funcall, checker:)
47
+ funcall.var
48
+ func = funcall.func
49
+ args = funcall.args
50
+
51
+ case func
52
+ when :ip, :cidr
53
+ checker.check_arity(func, args, 1)
54
+ arg = checker.call(args.first)
55
+ TYPES[func] if checker.find_match_all_types(%i[string], arg)
56
+ when :isIP, :isCanonical
57
+ checker.check_arity(func, args, 1)
58
+ arg = checker.call(args.first)
59
+ TYPES[:bool] if checker.find_match_all_types(%i[string], arg)
60
+ end
61
+ end
62
+
63
+ def __ip(value)
64
+ Cel::IP.new(value)
65
+ rescue IPAddr::InvalidAddressError
66
+ raise EvaluateError, "IP Address '#{value}' parse error during conversion from string"
67
+ end
68
+
69
+ def __cidr(value)
70
+ Cel::CIDR.new(value)
71
+ rescue IPAddr::InvalidAddressError
72
+ raise EvaluateError, "IP Address '#{value}' parse error during conversion from string"
73
+ end
74
+
75
+ def ip(str, program:)
76
+ value = program.call(str).value
77
+
78
+ __ip(value)
79
+ end
80
+
81
+ def cidr(str, program:)
82
+ value = program.call(str).value
83
+
84
+ __cidr(value)
85
+ end
86
+
87
+ def isIP(str, program:) # rubocop:disable Naming/MethodName
88
+ value = program.call(str).value
89
+
90
+ begin
91
+ __ip(value)
92
+ Bool.new(true)
93
+ rescue EvaluateError
94
+ Bool.new(false)
95
+ end
96
+ end
97
+
98
+ def isCanonical(str, program:) # rubocop:disable Naming/MethodName
99
+ value = program.call(str).value
100
+
101
+ Bool.new(value == __ip(value).to_s)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#IP
108
+ class IP < Literal
109
+ IPV4_LL_UC_CIDR = IPAddr.new("169.254.0.0/16")
110
+ IPV4_LL_MC_CIDR = IPAddr.new("224.0.0.0/24")
111
+ IPV6_LL_UC_CIDR = IPAddr.new("fe80::/10")
112
+ IPV6_LL_MC_CIDR = IPAddr.new("ff00::/8")
113
+
114
+ def initialize(val)
115
+ value = IPAddr.new(val)
116
+
117
+ if value.ipv6?
118
+ raise EvaluateError, "IP Address with zone value is not allowed" if value.zone_id
119
+
120
+ if Resolv::IPv4::Regex.match?(val.split(":").last)
121
+ raise EvaluateError, "IPv4-mapped IPv6 address is not allowed"
122
+ end
123
+ end
124
+
125
+ super(:ip, value)
126
+ end
127
+
128
+ define_cel_method(:==) do |other|
129
+ case other
130
+ when IP
131
+ if @value.ipv4? && other.value.ipv6? && other.value.ipv4_mapped?
132
+ # convert ipv4 to ipv4-mapped ipv6 addr
133
+ @value.ipv4_mapped == other.value
134
+ elsif @value.ipv6? && other.value.ipv4? && @value.ipv4_mapped?
135
+ # convert ipv4 to ipv4-mapped ipv6 addr
136
+ @value == other.value.ipv4_mapped
137
+ else
138
+ # same family?
139
+ @value == other.value
140
+ end
141
+ else
142
+ super
143
+ end
144
+ end
145
+
146
+ define_cel_method(:family) do
147
+ Number.new(:int, @value.ipv6? ? 6 : 4)
148
+ end
149
+
150
+ define_cel_method(:isUnspecified) do
151
+ Bool.cast(@value == (@value.ipv6? ? "::" : "0.0.0.0"))
152
+ end
153
+
154
+ define_cel_method(:isLoopback) do
155
+ Bool.cast(@value.loopback?)
156
+ end
157
+
158
+ define_cel_method(:isGlobalUnicast) do
159
+ if @value.ipv6?
160
+ # not a link-local unicast, loopback or multicast address.
161
+ Bool.cast(!@value.loopback? && !IPV6_LL_UC_CIDR.include?(@value) && !IPV6_LL_MC_CIDR.include?(@value))
162
+ else
163
+ # not zero or 255.255.255.255
164
+ Bool.cast(@value != "255.255.255.255" && @value != "0.0.0.0")
165
+ end
166
+ end
167
+
168
+ define_cel_method(:isLinkLocalMulticast) do
169
+ Bool.cast(@value.ipv6? ? IPV6_LL_MC_CIDR.include?(@value) : IPV4_LL_MC_CIDR.include?(@value))
170
+ end
171
+
172
+ define_cel_method(:isLinkLocalUnicast) do
173
+ Bool.cast(@value.ipv6? ? IPV6_LL_UC_CIDR.include?(@value) : IPV4_LL_UC_CIDR.include?(@value))
174
+ end
175
+ end
176
+
177
+ class CIDR < Literal
178
+ def initialize(val)
179
+ value = IPAddr.new(val)
180
+
181
+ if value.ipv6?
182
+ raise EvaluateError, "IP Address with zone value is not allowed" if value.zone_id
183
+
184
+ val, = val.split("/", 2)
185
+ if Resolv::IPv4::Regex.match?(val.split(":").last)
186
+ raise EvaluateError, "IPv4-mapped IPv6 address is not allowed"
187
+ end
188
+ end
189
+
190
+ super(:cidr, value)
191
+ end
192
+
193
+ define_cel_method(:containsIP) do |other|
194
+ Bool.cast(@value.include?(other.value))
195
+ end
196
+
197
+ define_cel_method(:containsCIDR) do |other|
198
+ Bool.cast(@value.include?(other.value))
199
+ end
200
+
201
+ define_cel_method(:ip) do
202
+ IP.new(@value.to_s)
203
+ end
204
+
205
+ define_cel_method(:masked) do
206
+ self
207
+ end
208
+
209
+ define_cel_method(:prefixLength) do
210
+ Number.new(:int, @value.prefix)
211
+ end
212
+
213
+ def to_str
214
+ @value.cidr
215
+ end
216
+ end
217
+
218
+ TYPES[:ip] = Type.new(:ip)
219
+ TYPES[:cidr] = Type.new(:cidr)
220
+ end
@@ -5,6 +5,10 @@ module Cel
5
5
  module String
6
6
  module_function
7
7
 
8
+ def implicit?
9
+ false
10
+ end
11
+
8
12
  def __check(funcall, checker:)
9
13
  var = funcall.var
10
14
  func = funcall.func
@@ -4,6 +4,7 @@ require_relative "extensions/math"
4
4
  require_relative "extensions/string"
5
5
  require_relative "extensions/bind"
6
6
  require_relative "extensions/encoders"
7
+ require_relative "extensions/network"
7
8
 
8
9
  module Cel
9
10
  EXTENSIONS = {
@@ -11,5 +12,7 @@ module Cel
11
12
  strings: Extensions::String,
12
13
  base64: Extensions::Encoders::Base64,
13
14
  cel: Extensions::Bind,
15
+ net: Extensions::Network,
16
+ ip: Extensions::Network::IP,
14
17
  }.freeze
15
18
  end
data/lib/cel/parser.rb CHANGED
@@ -995,7 +995,7 @@ module_eval(<<'.,.,', 'parser.ry', 58)
995
995
 
996
996
  module_eval(<<'.,.,', 'parser.ry', 62)
997
997
  def _reduce_36(val, _values, result)
998
- result = val[1]
998
+ result = ".#{val[1]}"
999
999
  result
1000
1000
  end
1001
1001
  .,.,
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
@@ -208,10 +208,14 @@ module Cel
208
208
  return evaluate_invoke(invoke, proto_type)
209
209
  end
210
210
 
211
- # try stuff like a.b.c
212
- return evaluate_identifier(invoke.to_s) if args.nil?
213
-
214
- evaluate_identifier(var)
211
+ if @environment.extensions.key?(var.to_sym)
212
+ var = @environment.extensions[var.to_sym]
213
+ elsif args.nil?
214
+ # try stuff like a.b.c
215
+ return evaluate_identifier(invoke.to_s)
216
+ else
217
+ evaluate_identifier(var)
218
+ end
215
219
 
216
220
  when Invoke
217
221
  if args.nil?
@@ -265,7 +269,7 @@ module Cel
265
269
  raise EvaluateError, "#{invoke} is not supported" unless var.class.method_defined?(func, false)
266
270
 
267
271
  # eliminate protobuf API from the lookup
268
- raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
272
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.method_defined?(func)
269
273
 
270
274
  # If e evaluates to a message and f is not declared in this message, the
271
275
  # runtime error no_such_field is raised.
@@ -274,16 +278,18 @@ module Cel
274
278
  var.cel_send(func, *args)
275
279
  when Literal
276
280
  # eliminate ruby API from the lookup
277
- raise NoMatchingOverloadError.new(var, func) if Literal.instance_methods.include?(func)
281
+ raise NoMatchingOverloadError.new(var, func) if Literal.method_defined?(func)
278
282
 
279
283
  # If e evaluates to a message and f is not declared in this message, the
280
284
  # runtime error no_such_field is raised.
281
285
  raise NoSuchFieldError.new(var, func) unless var.respond_to?(func)
282
286
 
287
+ args ?
288
+ var.cel_send(func, *Array(args).map(&method(:call))) :
283
289
  var.cel_send(func)
284
290
  when Protobuf.base_class
285
291
  # eliminate protobuf API from the lookup
286
- raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.instance_methods.include?(func)
292
+ raise NoMatchingOverloadError.new(var, func) if Protobuf.base_class.method_defined?(func)
287
293
 
288
294
  # If e evaluates to a message and f is not declared in this message, the
289
295
  # runtime error no_such_field is raised.
@@ -344,6 +350,12 @@ module Cel
344
350
  func = funcall.func
345
351
  args = funcall.args
346
352
 
353
+ unless Module.respond_to?(func)
354
+ @environment.implicit_extensions.each do |ext|
355
+ return ext.__send__(func, *args, program: self) if ext.respond_to?(func)
356
+ end
357
+ end
358
+
347
359
  case func
348
360
  when :type
349
361
  operand = args.first
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.6.0"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -22,8 +22,7 @@ end
22
22
 
23
23
  begin
24
24
  require_relative "cel/ast/elements/protobuf"
25
- rescue LoadError => e
26
- puts e
25
+ rescue LoadError
27
26
  module Cel
28
27
  class << self
29
28
  # returns a struct class containing the attributes that one would
@@ -114,26 +113,27 @@ rescue LoadError => e
114
113
  module Protobuf
115
114
  module_function
116
115
 
117
- BASE_CLASS = Class.new(Object)
116
+ class BaseClass < Object
117
+ end
118
118
 
119
119
  def base_class
120
120
  Struct
121
121
  end
122
122
 
123
123
  def enum_class
124
- BASE_CLASS
124
+ BaseClass
125
125
  end
126
126
 
127
127
  def map_class
128
- BASE_CLASS
128
+ BaseClass
129
129
  end
130
130
 
131
131
  def timestamp_class
132
- BASE_CLASS
132
+ BaseClass
133
133
  end
134
134
 
135
135
  def duration_class
136
- BASE_CLASS
136
+ BaseClass
137
137
  end
138
138
 
139
139
  def convert_to_proto(message_type, values)
@@ -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.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -109,6 +109,7 @@ files:
109
109
  - lib/cel/extensions/bind.rb
110
110
  - lib/cel/extensions/encoders.rb
111
111
  - lib/cel/extensions/math.rb
112
+ - lib/cel/extensions/network.rb
112
113
  - lib/cel/extensions/string.rb
113
114
  - lib/cel/macro.rb
114
115
  - lib/cel/parser.rb