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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +38 -0
- data/lib/cel/ast/elements/invoke.rb +2 -2
- data/lib/cel/ast/elements/list.rb +1 -1
- data/lib/cel/ast/elements/protobuf.rb +2 -2
- data/lib/cel/ast/types.rb +1 -1
- data/lib/cel/checker.rb +37 -16
- data/lib/cel/context.rb +73 -5
- data/lib/cel/environment.rb +24 -10
- data/lib/cel/extensions/bind.rb +6 -3
- data/lib/cel/extensions/encoders.rb +4 -0
- data/lib/cel/extensions/math.rb +4 -0
- data/lib/cel/extensions/network.rb +220 -0
- data/lib/cel/extensions/string.rb +4 -0
- data/lib/cel/extensions.rb +3 -0
- data/lib/cel/parser.rb +1 -1
- data/lib/cel/program.rb +21 -9
- data/lib/cel/version.rb +1 -1
- data/lib/cel.rb +22 -19
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bf9b40df18d849d9a964e668c0f1aee0d82f02ccd2de536dcfa73c8d714c718
|
|
4
|
+
data.tar.gz: 0f348c6b1a4a09362d76db927aec94099fc2ac2b355ddcef963b72fa90549e07
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
41
|
+
"#{var}.#{func}#{"(#{args.join(", ")})" if args}"
|
|
42
42
|
end
|
|
43
43
|
else
|
|
44
|
-
"#{func}#{"(#{args.
|
|
44
|
+
"#{func}#{"(#{args.join(", ")})" if args}"
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -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.
|
|
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)] }
|
|
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
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) &&
|
|
232
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
433
|
+
when *@cast_allowed_types.keys
|
|
413
434
|
check_arity(func, args, 1)
|
|
414
|
-
allowed_types =
|
|
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 :
|
|
5
|
+
attr_reader :bindings
|
|
6
6
|
|
|
7
|
-
def initialize(
|
|
8
|
-
@
|
|
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
|
-
|
|
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
|
data/lib/cel/environment.rb
CHANGED
|
@@ -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
|
-
@
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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 =
|
|
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
|
-
|
|
120
|
-
context = Context.new(
|
|
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
|
data/lib/cel/extensions/bind.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/cel/extensions/math.rb
CHANGED
|
@@ -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
|
data/lib/cel/extensions.rb
CHANGED
|
@@ -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
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
|
|
61
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
BaseClass
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def map_class
|
|
128
|
-
|
|
128
|
+
BaseClass
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def timestamp_class
|
|
132
|
-
|
|
132
|
+
BaseClass
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def duration_class
|
|
136
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
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
|