cel 0.3.1 → 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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +2 -2
- data/lib/cel/ast/elements.rb +13 -1
- data/lib/cel/checker.rb +47 -31
- data/lib/cel/context.rb +1 -1
- data/lib/cel/environment.rb +15 -2
- data/lib/cel/errors.rb +9 -0
- data/lib/cel/extensions/bind.rb +35 -0
- data/lib/cel/extensions/encoders.rb +41 -0
- data/lib/cel/extensions/math.rb +211 -0
- data/lib/cel/extensions/string.rb +228 -0
- data/lib/cel/extensions.rb +15 -0
- data/lib/cel/parser.rb +2 -2
- data/lib/cel/program.rb +12 -4
- data/lib/cel/version.rb +1 -1
- data/lib/cel.rb +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12cebd55174d05db4f154e185872921949f3931817503fccb5dec9f516c3128d
|
|
4
|
+
data.tar.gz: 8fa9c386f343ecfdda97ce99c37296e15378763ab0308270fdc0e789cb4c9ab0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a46166c14750ef4fc245d1aca362f103d4340ddce4155c51e2f49e03ea64c5d812dccaf61ac073f9f6207995a2f8090a748fe06bf984159542385416a2e9e83a
|
|
7
|
+
data.tar.gz: cfa9d65dd070389fca1ed80817a83baee1efe193cab6830bfd0a3278051a8593a673a4de8e19c19db1a450312cad69ccb91ac49e5db2ee02d18d0efb301319c1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
3
17
|
## [0.3.1] - 2025-08-01
|
|
4
18
|
|
|
5
19
|
### Improvements
|
data/README.md
CHANGED
|
@@ -131,11 +131,11 @@ env.evaluate("google.protobuf.Duration{seconds: 123}.seconds == 123") #=> true
|
|
|
131
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`:
|
|
132
132
|
|
|
133
133
|
```ruby
|
|
134
|
-
env =
|
|
134
|
+
env = Cel::Environment.new(declarations: {foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b }})
|
|
135
135
|
env.evaluate("foo(2, 2)") #=> 4
|
|
136
136
|
|
|
137
137
|
# this is also possible, just not as type-safe
|
|
138
|
-
env2 =
|
|
138
|
+
env2 = Cel::Environment.new(declarations: {foo: ->(a, b) { a + b }})
|
|
139
139
|
env2.evaluate("foo(2, 2)") #=> 4
|
|
140
140
|
```
|
|
141
141
|
|
data/lib/cel/ast/elements.rb
CHANGED
|
@@ -272,7 +272,7 @@ module Cel
|
|
|
272
272
|
%i[+ -].each do |op|
|
|
273
273
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
274
274
|
def #{op}@
|
|
275
|
-
raise
|
|
275
|
+
raise NoOverloadError if @type == TYPES[:uint]
|
|
276
276
|
value = super
|
|
277
277
|
Number.new(@type, value)
|
|
278
278
|
end
|
|
@@ -364,6 +364,10 @@ module Cel
|
|
|
364
364
|
end
|
|
365
365
|
OUT
|
|
366
366
|
end
|
|
367
|
+
|
|
368
|
+
def to_s
|
|
369
|
+
inspect
|
|
370
|
+
end
|
|
367
371
|
end
|
|
368
372
|
|
|
369
373
|
class Bytes < Literal
|
|
@@ -446,6 +450,10 @@ module Cel
|
|
|
446
450
|
max
|
|
447
451
|
end
|
|
448
452
|
|
|
453
|
+
def to_s
|
|
454
|
+
"[#{@value.map(&:to_s).join(", ")}]"
|
|
455
|
+
end
|
|
456
|
+
|
|
449
457
|
private
|
|
450
458
|
|
|
451
459
|
def calc_depth(element, acc)
|
|
@@ -633,6 +641,10 @@ module Cel
|
|
|
633
641
|
Protobuf.timestamp_class.from_time(@value)
|
|
634
642
|
end
|
|
635
643
|
|
|
644
|
+
def to_s
|
|
645
|
+
@value.utc.iso8601
|
|
646
|
+
end
|
|
647
|
+
|
|
636
648
|
private
|
|
637
649
|
|
|
638
650
|
def to_local_time(tz = nil)
|
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
|
-
|
|
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
data/lib/cel/environment.rb
CHANGED
|
@@ -53,8 +53,8 @@ module Cel
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def check(expr)
|
|
56
|
-
|
|
57
|
-
@checker.check(
|
|
56
|
+
expr = @parser.parse(expr) if expr.is_a?(::String)
|
|
57
|
+
@checker.check(expr)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def program(expr)
|
|
@@ -91,6 +91,19 @@ module Cel
|
|
|
91
91
|
[declarations, bindings]
|
|
92
92
|
end
|
|
93
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
|
+
|
|
94
107
|
private
|
|
95
108
|
|
|
96
109
|
def validate(ast, structs); end
|
data/lib/cel/errors.rb
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
module Extensions
|
|
5
|
+
module Bind
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def __check(funcall, checker:)
|
|
9
|
+
func = funcall.func
|
|
10
|
+
args = funcall.args
|
|
11
|
+
|
|
12
|
+
return checker.unsupported_operation(funcall) unless func == :bind
|
|
13
|
+
|
|
14
|
+
checker.check_arity(func, args, 3)
|
|
15
|
+
|
|
16
|
+
id, type, expr = args
|
|
17
|
+
|
|
18
|
+
return checker.unsupported_operation(funcall) unless id.is_a?(Identifier)
|
|
19
|
+
|
|
20
|
+
type = checker.call(type)
|
|
21
|
+
|
|
22
|
+
checker.environment.with(declarations: { id => type }) do
|
|
23
|
+
checker.environment.check(expr)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def bind(var, val, expr, program:)
|
|
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)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
module Extensions
|
|
5
|
+
module Encoders
|
|
6
|
+
module Base64
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def __check(funcall, checker:)
|
|
10
|
+
func = funcall.func
|
|
11
|
+
args = funcall.args
|
|
12
|
+
|
|
13
|
+
case func
|
|
14
|
+
when :encode
|
|
15
|
+
checker.check_arity(func, args, 1)
|
|
16
|
+
arg = checker.call(args.first)
|
|
17
|
+
return TYPES[:string] if checker.find_match_all_types(%i[bytes], arg)
|
|
18
|
+
when :decode
|
|
19
|
+
checker.check_arity(func, args, 1)
|
|
20
|
+
arg = checker.call(args.first)
|
|
21
|
+
return TYPES[:bytes] if checker.find_match_all_types(%i[string], arg)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
checker.unsupported_operation(funcall)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def encode(str, program:)
|
|
28
|
+
value = program.call(str).value
|
|
29
|
+
|
|
30
|
+
Cel::String.new([value.pack("C*")].pack("m0"))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def decode(str, program:)
|
|
34
|
+
value = program.call(str).value
|
|
35
|
+
|
|
36
|
+
Cel::Bytes.new(value.unpack1("m").unpack("C*"))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
module Extensions
|
|
5
|
+
module Math
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def __check(funcall, checker:)
|
|
9
|
+
func = funcall.func
|
|
10
|
+
args = funcall.args
|
|
11
|
+
|
|
12
|
+
case func
|
|
13
|
+
when :round, :trunc, :floor, :ceil
|
|
14
|
+
checker.check_arity(func, args, 1)
|
|
15
|
+
arg = checker.call(args.first)
|
|
16
|
+
return TYPES[:double] if checker.find_match_all_types(%i[double], arg)
|
|
17
|
+
when :isInf, :isNaN, :isFinite
|
|
18
|
+
checker.check_arity(func, args, 1)
|
|
19
|
+
arg = checker.call(args.first)
|
|
20
|
+
return TYPES[:bool] if checker.find_match_all_types(%i[double], arg)
|
|
21
|
+
when :bitNot
|
|
22
|
+
checker.check_arity(func, args, 1)
|
|
23
|
+
arg = checker.call(args.first)
|
|
24
|
+
return TYPES[:int] if checker.find_match_all_types(%i[int uint], arg)
|
|
25
|
+
when :bitOr, :bitAnd, :bitXor
|
|
26
|
+
checker.check_arity(func, args, 2)
|
|
27
|
+
args = args.map(&checker.method(:call))
|
|
28
|
+
|
|
29
|
+
type = checker.find_match_all_types(%i[int uint], args)
|
|
30
|
+
return type if type
|
|
31
|
+
when :sign, :abs
|
|
32
|
+
checker.check_arity(func, args, 1)
|
|
33
|
+
arg = checker.call(args.first)
|
|
34
|
+
type = checker.find_match_all_types(%i[double int uint], arg)
|
|
35
|
+
return if type
|
|
36
|
+
when :least, :greatest
|
|
37
|
+
checker.check_arity_any(func, args)
|
|
38
|
+
args = args.map(&checker.method(:call))
|
|
39
|
+
|
|
40
|
+
return TYPES[:any] if args.all? do |arg|
|
|
41
|
+
case arg
|
|
42
|
+
when TYPES[:any], TYPES[:int], TYPES[:uint], TYPES[:double],
|
|
43
|
+
TYPES[:list, :int], TYPES[:list, :uint], TYPES[:list, :double], TYPES[:list, :any]
|
|
44
|
+
true
|
|
45
|
+
else
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
when :bitShiftRight, :bitShiftLeft
|
|
50
|
+
checker.check_arity(func, args, 2)
|
|
51
|
+
num, amount_of_bits = args.map(&checker.method(:call))
|
|
52
|
+
|
|
53
|
+
return num if checker.find_match_all_types(%i[int uint],
|
|
54
|
+
num) && checker.find_match_all_types(%i[int], amount_of_bits)
|
|
55
|
+
else
|
|
56
|
+
checker.unsupported_operation(funcall)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
checker.unsupported_operation(funcall)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def abs(num, program:)
|
|
63
|
+
num = program.call(num)
|
|
64
|
+
|
|
65
|
+
raise EvaluateError, "out of range" unless num.value.between?(-MAX_INT + 1, MAX_INT - 1)
|
|
66
|
+
|
|
67
|
+
Number.new(num.type, num.value.abs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def round(num, program:)
|
|
71
|
+
num = program.call(num)
|
|
72
|
+
|
|
73
|
+
Number.new(:double, num.value.round)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def trunc(num, program:)
|
|
77
|
+
num = program.call(num)
|
|
78
|
+
|
|
79
|
+
Number.new(:double, num.value.truncate)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def floor(num, program:)
|
|
83
|
+
num = program.call(num)
|
|
84
|
+
|
|
85
|
+
Number.new(:double, num.value.floor)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ceil(num, program:)
|
|
89
|
+
num = program.call(num)
|
|
90
|
+
|
|
91
|
+
Number.new(:double, num.value.ceil)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def bitNot(num, program:)
|
|
95
|
+
val = program.call(num).value
|
|
96
|
+
|
|
97
|
+
case num.type
|
|
98
|
+
when TYPES[:int]
|
|
99
|
+
Number.new(:int, ~val)
|
|
100
|
+
when TYPES[:uint]
|
|
101
|
+
Number.new(:uint, ((2**64) - 1) - val)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def bitOr(lhs, rhs, program:)
|
|
106
|
+
lhs = program.call(lhs)
|
|
107
|
+
rhs = program.call(rhs)
|
|
108
|
+
Number.new(lhs.type, lhs | rhs)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bitAnd(lhs, rhs, program:)
|
|
112
|
+
lhs = program.call(lhs)
|
|
113
|
+
rhs = program.call(rhs)
|
|
114
|
+
Number.new(lhs.type, lhs & rhs)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def bitXor(lhs, rhs, program:)
|
|
118
|
+
lhs = program.call(lhs)
|
|
119
|
+
rhs = program.call(rhs)
|
|
120
|
+
Number.new(lhs.type, lhs ^ rhs)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sign(num, program:)
|
|
124
|
+
num = program.call(num)
|
|
125
|
+
value = num.value
|
|
126
|
+
|
|
127
|
+
Number.new(num.type, if value.negative?
|
|
128
|
+
-1
|
|
129
|
+
else
|
|
130
|
+
value.positive? ? 1 : 0
|
|
131
|
+
end)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def least(*args, program:)
|
|
135
|
+
args = args.map(&program.method(:call))
|
|
136
|
+
|
|
137
|
+
return args.min if args.size > 1
|
|
138
|
+
|
|
139
|
+
arg = args.first
|
|
140
|
+
|
|
141
|
+
case arg
|
|
142
|
+
when List
|
|
143
|
+
least(*arg.value, program: program)
|
|
144
|
+
else
|
|
145
|
+
arg
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def greatest(*args, program:)
|
|
150
|
+
args = args.map(&program.method(:call))
|
|
151
|
+
|
|
152
|
+
return args.max if args.size > 1
|
|
153
|
+
|
|
154
|
+
arg = args.first
|
|
155
|
+
|
|
156
|
+
case arg
|
|
157
|
+
when List
|
|
158
|
+
greatest(*arg.value, program: program)
|
|
159
|
+
else
|
|
160
|
+
arg
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def bitShiftRight(num, amount_of_bits, program:)
|
|
165
|
+
num = program.call(num)
|
|
166
|
+
amount_of_bits = program.call(amount_of_bits).value
|
|
167
|
+
|
|
168
|
+
raise EvaluateError, "math.#{__method__}() negative offset: #{amount_of_bits}" if amount_of_bits.negative?
|
|
169
|
+
|
|
170
|
+
# When the second parameter is 64 or greater, 0 will always be returned
|
|
171
|
+
return Number.new(num.type, 0) if amount_of_bits >= 64
|
|
172
|
+
|
|
173
|
+
value = num.value.negative? ? ((2**64) - 1) & num.value : num.value
|
|
174
|
+
|
|
175
|
+
Number.new(num.type, value >> amount_of_bits)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def bitShiftLeft(num, amount_of_bits, program:)
|
|
179
|
+
num = program.call(num)
|
|
180
|
+
amount_of_bits = program.call(amount_of_bits).value
|
|
181
|
+
|
|
182
|
+
raise EvaluateError, "math.#{__method__}() negative offset: #{amount_of_bits}" if amount_of_bits.negative?
|
|
183
|
+
|
|
184
|
+
# When the second parameter is 64 or greater, 0 will always be returned
|
|
185
|
+
return Number.new(num.type, 0) if amount_of_bits >= 64
|
|
186
|
+
|
|
187
|
+
Number.new(num.type, num.value << amount_of_bits)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def isInf(num, program:)
|
|
191
|
+
val = program.call(num).value
|
|
192
|
+
|
|
193
|
+
Bool.cast(val.infinite?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def isNaN(num, program:)
|
|
197
|
+
val = program.call(num).value
|
|
198
|
+
|
|
199
|
+
Bool.cast(val.nan?)
|
|
200
|
+
rescue FloatDomainError
|
|
201
|
+
Bool.cast(true)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def isFinite(num, program:)
|
|
205
|
+
val = program.call(num).value
|
|
206
|
+
|
|
207
|
+
Bool.cast(!val.infinite? && !val.nan?)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
module Extensions
|
|
5
|
+
module String
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def __check(funcall, checker:)
|
|
9
|
+
var = funcall.var
|
|
10
|
+
func = funcall.func
|
|
11
|
+
args = funcall.args
|
|
12
|
+
|
|
13
|
+
case func
|
|
14
|
+
when :quote
|
|
15
|
+
checker.check_arity(func, args, 1)
|
|
16
|
+
arg = checker.call(args.first)
|
|
17
|
+
return TYPES[:string] if checker.find_match_all_types(%i[string], arg)
|
|
18
|
+
when :indexOf, :lastIndexOf
|
|
19
|
+
if checker.call(var) == TYPES[:string]
|
|
20
|
+
checker.check_arity(func, args, 2, :>=)
|
|
21
|
+
arg, pos = args
|
|
22
|
+
arg = checker.call(arg)
|
|
23
|
+
if checker.find_match_all_types(%i[string], arg)
|
|
24
|
+
return TYPES[:int] if pos.nil?
|
|
25
|
+
|
|
26
|
+
pos = checker.call(pos)
|
|
27
|
+
return TYPES[:int] if checker.find_match_all_types(%i[int], pos)
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
when :split
|
|
32
|
+
if checker.call(var) == TYPES[:string]
|
|
33
|
+
checker.check_arity(func, args, 2, :>=)
|
|
34
|
+
arg, pos = args
|
|
35
|
+
arg = checker.call(arg)
|
|
36
|
+
if checker.find_match_all_types(%i[string], arg)
|
|
37
|
+
return TYPES[:list, :string] if pos.nil?
|
|
38
|
+
|
|
39
|
+
pos = checker.call(pos)
|
|
40
|
+
return TYPES[:list, :string] if checker.find_match_all_types(%i[int], pos)
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
when :substring
|
|
45
|
+
if checker.call(var) == TYPES[:string]
|
|
46
|
+
checker.check_arity(func, args, 2, :>=)
|
|
47
|
+
arg, pos = args
|
|
48
|
+
arg = checker.call(arg)
|
|
49
|
+
if checker.find_match_all_types(%i[int], arg)
|
|
50
|
+
return TYPES[:string] if pos.nil?
|
|
51
|
+
|
|
52
|
+
pos = checker.call(pos)
|
|
53
|
+
return TYPES[:string] if checker.find_match_all_types(%i[int], pos)
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
when :trim, :lowerAscii, :upperAscii, :reverse
|
|
58
|
+
if checker.call(var) == TYPES[:string]
|
|
59
|
+
checker.check_arity(func, args, 0)
|
|
60
|
+
return TYPES[:string]
|
|
61
|
+
end
|
|
62
|
+
when :format
|
|
63
|
+
if checker.call(var) == TYPES[:string]
|
|
64
|
+
checker.check_arity(func, args, 1)
|
|
65
|
+
arg = checker.call(args.first)
|
|
66
|
+
return TYPES[:string] if checker.find_match_all_types(%i[list], arg)
|
|
67
|
+
end
|
|
68
|
+
when :charAt
|
|
69
|
+
if checker.call(var) == TYPES[:string]
|
|
70
|
+
checker.check_arity(func, args, 1)
|
|
71
|
+
arg = checker.call(args.first)
|
|
72
|
+
return TYPES[:string] if checker.find_match_all_types(%i[int], arg)
|
|
73
|
+
end
|
|
74
|
+
when :replace
|
|
75
|
+
if checker.call(var) == TYPES[:string]
|
|
76
|
+
checker.check_arity(func, args, 2..3, :include?)
|
|
77
|
+
p, r, limit = args.map(&checker.method(:call))
|
|
78
|
+
if limit
|
|
79
|
+
return TYPES[:string] if checker.find_match_all_types(%i[string], [p, r]) &&
|
|
80
|
+
checker.find_match_all_types(%i[int], limit)
|
|
81
|
+
elsif checker.find_match_all_types(%i[string], [p, r])
|
|
82
|
+
return TYPES[:string]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
when :join
|
|
86
|
+
if checker.call(var) == TYPES[:list, :string] || checker.call(var) == TYPES[:list, :any]
|
|
87
|
+
checker.check_arity(func, args, 1, :>=)
|
|
88
|
+
arg = checker.call(args.first)
|
|
89
|
+
return TYPES[:string] unless arg
|
|
90
|
+
return TYPES[:string] if checker.find_match_all_types(%i[string], arg)
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
checker.unsupported_operation(funcall)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def quote(str, program:)
|
|
99
|
+
value = program.call(str).value
|
|
100
|
+
|
|
101
|
+
# this should use String#dump, but this also replaces printable UTF8 characters
|
|
102
|
+
|
|
103
|
+
# replace quotes
|
|
104
|
+
value = value.gsub(/[\\'"\t\r\a\b\v\f\n]/) { |m| m.dump[1..-2] }
|
|
105
|
+
|
|
106
|
+
# replace invalid utf-8 chars
|
|
107
|
+
value = value.encode(Encoding::UTF_8, invalid: :replace)
|
|
108
|
+
|
|
109
|
+
# adding leading quotes if not quoted already
|
|
110
|
+
value = "\"#{value}\"" unless value.start_with?("\"") && value.end_with?("\"")
|
|
111
|
+
|
|
112
|
+
Cel::String.new(value)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class String
|
|
118
|
+
def charAt(idx)
|
|
119
|
+
raise EvaluateError, "index out of range: #{idx}" unless idx.between?(0, @value.size)
|
|
120
|
+
|
|
121
|
+
String.new(@value[idx] || "")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def indexOf(substr, *args)
|
|
125
|
+
idx, = args
|
|
126
|
+
|
|
127
|
+
raise EvaluateError, "index out of range: #{idx}" if idx && !idx.between?(0, @value.size)
|
|
128
|
+
|
|
129
|
+
Number.new(:int, @value.index(substr, *args) || -1)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def lastIndexOf(substr, *args)
|
|
133
|
+
idx, = args
|
|
134
|
+
|
|
135
|
+
raise EvaluateError, "index out of range: #{idx}" if idx && !idx.between?(0, @value.size)
|
|
136
|
+
|
|
137
|
+
Number.new(:int, @value.rindex(substr, *args) || -1)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def lowerAscii
|
|
141
|
+
String.new(@value.downcase(:ascii))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def upperAscii
|
|
145
|
+
String.new(@value.upcase(:ascii))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
UNICODE_WHITESPACE = /[ \t\r\n\u000C\u000D\u0020\u0085\u00A0\u1680\u2000-\u200A\u2028-\u2029\u202F\u205F\u3000]+/
|
|
149
|
+
|
|
150
|
+
def trim
|
|
151
|
+
# why this? because String#split only takes null, horizontal tab, line feed, vertical tab, form feed,
|
|
152
|
+
# carriage return, space into account. We need the unicode definition of whitespace.
|
|
153
|
+
value = @value.gsub(/\A#{UNICODE_WHITESPACE}/o, "")
|
|
154
|
+
.gsub(/#{UNICODE_WHITESPACE}\z/o, "")
|
|
155
|
+
|
|
156
|
+
String.new(value.strip)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def reverse
|
|
160
|
+
String.new(@value.reverse)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def split(pattern, *args)
|
|
164
|
+
raise NoOverloadError unless pattern.is_a?(String) && args.size.between?(0, 1)
|
|
165
|
+
|
|
166
|
+
limit, = args
|
|
167
|
+
|
|
168
|
+
raise NoOverloadError if limit && !limit.is_a?(Number)
|
|
169
|
+
|
|
170
|
+
return List.new([]) if limit && limit.zero?
|
|
171
|
+
|
|
172
|
+
List.new(@value.split(pattern, *args))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def substring(*args)
|
|
176
|
+
raise NoOverloadError unless args.size.between?(1, 2)
|
|
177
|
+
|
|
178
|
+
start_idx, end_idx = *args
|
|
179
|
+
end_idx ||= @value.size
|
|
180
|
+
|
|
181
|
+
raise EvaluateError, "index out of range: #{start_idx}" unless start_idx.between?(0, @value.size)
|
|
182
|
+
|
|
183
|
+
raise EvaluateError, "index out of range: #{end_idx}" if end_idx >= @value.size + 1
|
|
184
|
+
|
|
185
|
+
raise EvaluateError, "end index can't be higher than start index" if end_idx < start_idx
|
|
186
|
+
|
|
187
|
+
String.new(@value[start_idx..(end_idx - 1)])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def replace(pattern, replacement, *args)
|
|
191
|
+
raise NoOverloadError unless args.size.between?(0, 1)
|
|
192
|
+
|
|
193
|
+
limit, = args
|
|
194
|
+
|
|
195
|
+
raise NoOverloadError unless pattern.is_a?(String) && replacement.is_a?(String)
|
|
196
|
+
|
|
197
|
+
pattern = pattern.value
|
|
198
|
+
replacement = replacement.value
|
|
199
|
+
|
|
200
|
+
value = if limit
|
|
201
|
+
raise NoOverloadError unless limit.is_a?(Number)
|
|
202
|
+
|
|
203
|
+
@value.gsub(pattern) do |m|
|
|
204
|
+
if limit.zero?
|
|
205
|
+
m
|
|
206
|
+
else
|
|
207
|
+
limit -= 1
|
|
208
|
+
replacement
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
else
|
|
212
|
+
@value.gsub(pattern, replacement)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
String.new(value)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def format(mappings)
|
|
219
|
+
String.new(format(@value, *mappings))
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
class List
|
|
224
|
+
def join(*)
|
|
225
|
+
String.new(super)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cel/extensions/math"
|
|
4
|
+
require "cel/extensions/string"
|
|
5
|
+
require "cel/extensions/bind"
|
|
6
|
+
require "cel/extensions/encoders"
|
|
7
|
+
|
|
8
|
+
module Cel
|
|
9
|
+
EXTENSIONS = {
|
|
10
|
+
math: Extensions::Math,
|
|
11
|
+
strings: Extensions::String,
|
|
12
|
+
base64: Extensions::Encoders::Base64,
|
|
13
|
+
cel: Extensions::Bind,
|
|
14
|
+
}.freeze
|
|
15
|
+
end
|
data/lib/cel/parser.rb
CHANGED
data/lib/cel/program.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Cel
|
|
4
4
|
class Program
|
|
5
|
-
attr_reader :context
|
|
5
|
+
attr_reader :environment, :context
|
|
6
6
|
|
|
7
7
|
def initialize(context, environment)
|
|
8
8
|
@context = context
|
|
@@ -55,7 +55,11 @@ module Cel
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def evaluate_identifier(identifier)
|
|
58
|
-
|
|
58
|
+
id_sym = identifier.to_sym
|
|
59
|
+
|
|
60
|
+
if Cel::EXTENSIONS.key?(id_sym)
|
|
61
|
+
Cel::EXTENSIONS[id_sym]
|
|
62
|
+
elsif Cel::PRIMITIVE_TYPES.include?(id_sym)
|
|
59
63
|
TYPES[identifier.to_sym]
|
|
60
64
|
else
|
|
61
65
|
val, func = @context.lookup(identifier)
|
|
@@ -283,8 +287,10 @@ module Cel
|
|
|
283
287
|
value = Protobuf.lookup(var, func)
|
|
284
288
|
|
|
285
289
|
Literal.to_cel_type(value)
|
|
286
|
-
|
|
287
|
-
if var.
|
|
290
|
+
when Module
|
|
291
|
+
if var.respond_to?(func) && !Module.respond_to?(func)
|
|
292
|
+
return var.__send__(func, *args, program: self)
|
|
293
|
+
elsif var.const_defined?(func) && !Module.const_defined?(func)
|
|
288
294
|
# this block assumes a message based call on a protobuf message, either to a
|
|
289
295
|
# subclass/namespace (Foo.Bar), or an enum (Foo.BAR)
|
|
290
296
|
# protobuf accessing enum module
|
|
@@ -313,6 +319,8 @@ module Cel
|
|
|
313
319
|
return Literal.to_cel_type(value)
|
|
314
320
|
end
|
|
315
321
|
|
|
322
|
+
raise EvaluateError, "#{invoke} is not supported"
|
|
323
|
+
else
|
|
316
324
|
raise EvaluateError, "#{invoke} is not supported"
|
|
317
325
|
end
|
|
318
326
|
end
|
data/lib/cel/version.rb
CHANGED
data/lib/cel.rb
CHANGED
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tiago Cardoso
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: tzinfo-data
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
description: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
|
|
55
69
|
email:
|
|
56
70
|
- cardoso_tiago@hotmail.com
|
|
@@ -73,6 +87,11 @@ files:
|
|
|
73
87
|
- lib/cel/encoder.rb
|
|
74
88
|
- lib/cel/environment.rb
|
|
75
89
|
- lib/cel/errors.rb
|
|
90
|
+
- lib/cel/extensions.rb
|
|
91
|
+
- lib/cel/extensions/bind.rb
|
|
92
|
+
- lib/cel/extensions/encoders.rb
|
|
93
|
+
- lib/cel/extensions/math.rb
|
|
94
|
+
- lib/cel/extensions/string.rb
|
|
76
95
|
- lib/cel/macro.rb
|
|
77
96
|
- lib/cel/parser.rb
|
|
78
97
|
- lib/cel/program.rb
|
|
@@ -99,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
99
118
|
- !ruby/object:Gem::Version
|
|
100
119
|
version: '0'
|
|
101
120
|
requirements: []
|
|
102
|
-
rubygems_version: 3.6.
|
|
121
|
+
rubygems_version: 3.6.9
|
|
103
122
|
specification_version: 4
|
|
104
123
|
summary: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
|
|
105
124
|
test_files: []
|