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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82fcb37e45c0553a7c073b58e4c6b7203cc50bd186419724d72f5f1b0217e3f5
4
- data.tar.gz: 44170fc7001634abc19860db8dcbc8c05ae8974716ad80dd28c4316dc0cec7f3
3
+ metadata.gz: 12cebd55174d05db4f154e185872921949f3931817503fccb5dec9f516c3128d
4
+ data.tar.gz: 8fa9c386f343ecfdda97ce99c37296e15378763ab0308270fdc0e789cb4c9ab0
5
5
  SHA512:
6
- metadata.gz: 593ec20b7001fe593d6df79928ca8411327251fff2405491eb33642299cf6bc09d255d0088fdb6a1deda77eabccf8bcce229ad5038f68ee932f9c10ef162cdc0
7
- data.tar.gz: 55eb21528a3ff994f5fc013ca49ded2b7cb0aaf9d5d434c671fc0cd534ed0119269c1e2bf8b5fe9e44ba1f1e05f51d30f5fd84abd1f44d740898dd971b341ece
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 = environment(foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b})
134
+ env = Cel::Environment.new(declarations: {foo: Cel::Function(:int, :int, return_type: :int) { |a, b| a + b }})
135
135
  env.evaluate("foo(2, 2)") #=> 4
136
136
 
137
137
  # this is also possible, just not as type-safe
138
- env2 = environment(foo: -> (a, b) { a + b})
138
+ env2 = Cel::Environment.new(declarations: {foo: ->(a, b) { a + b }})
139
139
  env2.evaluate("foo(2, 2)") #=> 4
140
140
  ```
141
141
 
@@ -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 EvaluateError, "no such overload" if @type == TYPES[:uint]
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
- return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
469
+ id_sym = identifier.to_sym
470
+
471
+ return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(id_sym)
428
472
 
429
473
  proto_type = identifier.try_convert_to_proto_type
430
474
 
@@ -472,33 +516,5 @@ module Cel
472
516
  typ
473
517
  end
474
518
  end
475
-
476
- def find_match_all_types(expected, types)
477
- # at least an expected type must match all values
478
- type = expected.find do |expected_type|
479
- case types
480
- when Array
481
- types.all? { |typ| typ == expected_type }
482
- else
483
- types == expected_type
484
- end
485
- end
486
-
487
- type && types.is_a?(Type) ? types : TYPES[type]
488
- end
489
-
490
- def check_arity(func, args, arity)
491
- return if arity === args.size # rubocop:disable Style/CaseEquality
492
-
493
- raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
494
- end
495
-
496
- def unsupported_type(op)
497
- raise CheckError, "no matching overload: #{op}"
498
- end
499
-
500
- def unsupported_operation(op)
501
- raise CheckError, "unsupported operation (#{op})"
502
- end
503
519
  end
504
520
  end
data/lib/cel/context.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Cel
4
4
  class Context
5
- attr_reader :declarations
5
+ attr_reader :declarations, :bindings
6
6
 
7
7
  def initialize(declarations, bindings)
8
8
  @declarations = declarations
@@ -53,8 +53,8 @@ module Cel
53
53
  end
54
54
 
55
55
  def check(expr)
56
- ast = @parser.parse(expr)
57
- @checker.check(ast)
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
@@ -48,5 +48,14 @@ module Cel
48
48
  end
49
49
  end
50
50
 
51
+ class NoOverloadError < EvaluateError
52
+ attr_reader :code
53
+
54
+ def initialize
55
+ super("No such overload")
56
+ @code = :no_overload
57
+ end
58
+ end
59
+
51
60
  class BindingError < EvaluateError; end
52
61
  end
@@ -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
@@ -404,9 +404,9 @@ def math_operation(op, operands)
404
404
  when "*", "/", "%"
405
405
  depth = lhs.depth + 1
406
406
  end
407
- when "+", "+"
407
+ when "+"
408
408
  case lhs.op
409
- when "+", "+"
409
+ when "+"
410
410
  depth = lhs.depth + 1
411
411
  end
412
412
  end
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
- if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
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
- else
287
- if var.is_a?(Module) && var.const_defined?(func)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cel
4
4
  module Ruby
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/cel.rb CHANGED
@@ -5,6 +5,7 @@ require "cel/version"
5
5
  require "cel/errors"
6
6
  require "cel/ast/types"
7
7
  require "cel/ast/elements"
8
+ require "cel/extensions"
8
9
  require "cel/parser"
9
10
  require "cel/macro"
10
11
  require "cel/context"
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.3.1
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.7
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: []