cel 0.6.1 → 0.6.2
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 +18 -0
- data/README.md +1 -0
- data/lib/cel/ast/elements/timestamp.rb +35 -1
- data/lib/cel/ast/types.rb +56 -3
- data/lib/cel/checker.rb +64 -25
- data/lib/cel/context.rb +4 -1
- data/lib/cel/extensions/bind.rb +2 -2
- data/lib/cel/extensions/encoders.rb +2 -4
- data/lib/cel/extensions/list.rb +188 -0
- data/lib/cel/extensions/math.rb +10 -16
- data/lib/cel/extensions/string.rb +12 -13
- data/lib/cel/extensions.rb +2 -0
- data/lib/cel/macro.rb +7 -2
- data/lib/cel/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cba7782a9640b53263c12796cdcbf6b65d67265f6017f706cf8ae90c248e77fd
|
|
4
|
+
data.tar.gz: e7174cae9eab76f1d1fd9bbad8feaa9246dce94e2005b3262b1ba5df283b3d24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3900c8e0bc26da2a9687f8b25411d308ddc9a4bdecad3aebc3ef751e2645d2fd6027a837eff52863bcdf2751e740fbe2035a4ff55ba5b71acc7c4ef6d7227cc9
|
|
7
|
+
data.tar.gz: faec09b8bc335bf26bb110a6ba1e36c12e4b60623d6faf3e4546e4fd91a0881d20011e003c4cc2fb41301f42a8658521d436412d66536e568f159ee23b45a850
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.6.2] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Support the lists extension functions, as listed [here](https://deepwiki.com/google/cel-go/9.2-standard-extensions):
|
|
8
|
+
* slice, flatten, sort, sortBy, distinct, reverse, range
|
|
9
|
+
|
|
10
|
+
### Improvements
|
|
11
|
+
|
|
12
|
+
* Expression checker can now type-check Map expressions.
|
|
13
|
+
* Improve type expression for maps, where instead of the element type being the first matched, it'll infer whether all elements of the map are of the same type, and if not, fallback to any.
|
|
14
|
+
|
|
15
|
+
### Bugfixes
|
|
16
|
+
|
|
17
|
+
* Fix issue when using map bindings via dot keys in macro expressions.
|
|
18
|
+
* Fix macro evaluation when variable used in expression was an accessor (instead of an identifier).
|
|
19
|
+
* timestamps: require RFC 3339 compliant strings on initialization (instead of all that ruby `Time.parse` accepts).
|
|
20
|
+
|
|
3
21
|
## [0.6.1] - 2026-07-27
|
|
4
22
|
|
|
5
23
|
* parser: enabled support for declaring CEL messages inside a CEL list.
|
data/README.md
CHANGED
|
@@ -4,15 +4,30 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module Cel
|
|
6
6
|
class Timestamp < Literal
|
|
7
|
+
# Ruby 4.1 grows a core Time.rfc3339; below that DateTime.rfc3339 is the only stdlib RFC 3339
|
|
8
|
+
# parser. Gate on the version rather than respond_to?(:rfc3339) alone: ActiveSupport defines
|
|
9
|
+
# its own, laxer Time.rfc3339 on every ruby - it rolls invalid dates over, turning
|
|
10
|
+
# "2022-02-30T00:00:00Z" into March 2nd - so probing for the method would silently hand
|
|
11
|
+
# parsing to ActiveSupport, and only when it happened to be loaded first.
|
|
12
|
+
CORE_RFC3339 = RUBY_VERSION >= "4.1.0" && Time.respond_to?(:rfc3339)
|
|
13
|
+
private_constant :CORE_RFC3339
|
|
14
|
+
|
|
15
|
+
require "date" unless CORE_RFC3339
|
|
16
|
+
|
|
7
17
|
TIMESTAMP_RANGE = (Time.parse("0001-01-01T00:00:00Z")..Time.parse("9999-12-31T23:59:59.999999999Z"))
|
|
8
18
|
|
|
9
19
|
def initialize(value)
|
|
10
20
|
value = case value
|
|
11
|
-
when ::String then
|
|
21
|
+
when ::String then parse_rfc3339(value)
|
|
12
22
|
when Numeric then Time.at(value)
|
|
13
23
|
else value
|
|
14
24
|
end
|
|
15
25
|
|
|
26
|
+
# CEL timestamps are absolute instants carrying no timezone, so normalize to UTC: that is
|
|
27
|
+
# what the string conversion and the offset-less accessors (getHours & co.) assume. Anything
|
|
28
|
+
# that is not a time falls through to the range check below, which rejects it.
|
|
29
|
+
value = value.getutc if value.is_a?(::Time)
|
|
30
|
+
|
|
16
31
|
raise EvaluateError, "out of range" unless TIMESTAMP_RANGE.include?(value)
|
|
17
32
|
|
|
18
33
|
super(:timestamp, value.round(9))
|
|
@@ -92,6 +107,25 @@ module Cel
|
|
|
92
107
|
|
|
93
108
|
private
|
|
94
109
|
|
|
110
|
+
# The CEL spec defines timestamp() over RFC 3339 date-times, which always carry an explicit
|
|
111
|
+
# offset. Time.parse would instead accept anything remotely date-like and silently resolve
|
|
112
|
+
# offset-less input in the server-local timezone.
|
|
113
|
+
if CORE_RFC3339
|
|
114
|
+
def parse_rfc3339(value)
|
|
115
|
+
Time.rfc3339(value)
|
|
116
|
+
rescue ArgumentError
|
|
117
|
+
raise EvaluateError, "invalid timestamp #{value.inspect} (must be an RFC 3339 date-time)"
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
# Date::ITALY, the default calendar reform, reads pre-1582 date-times as Julian ones,
|
|
121
|
+
# shifting them off the proleptic Gregorian calendar Time uses.
|
|
122
|
+
def parse_rfc3339(value)
|
|
123
|
+
DateTime.rfc3339(value, Date::GREGORIAN).to_time
|
|
124
|
+
rescue ArgumentError # Date::Error
|
|
125
|
+
raise EvaluateError, "invalid timestamp #{value.inspect} (must be an RFC 3339 date-time)"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
95
129
|
def to_local_time(tz = nil)
|
|
96
130
|
time = @value
|
|
97
131
|
if tz
|
data/lib/cel/ast/types.rb
CHANGED
|
@@ -214,25 +214,78 @@ module Cel
|
|
|
214
214
|
end
|
|
215
215
|
|
|
216
216
|
class MapType < Type
|
|
217
|
+
# The map's value type (`V` in `map(K, V)`). Named `element_type` for
|
|
218
|
+
# symmetry with ListType and because `TYPES[:map, V]` has always set the
|
|
219
|
+
# declared value type here.
|
|
217
220
|
attr_accessor :element_type
|
|
218
221
|
|
|
222
|
+
# The map's key type (`K` in `map(K, V)`).
|
|
223
|
+
attr_accessor :key_type
|
|
224
|
+
|
|
225
|
+
# For a struct-like map literal — every key a constant — the per-key value
|
|
226
|
+
# types, as `{ <bare key> => Cel::Type }`. `nil` when at least one key is
|
|
227
|
+
# not a constant, so a caller can tell "no fixed key set" apart from "an
|
|
228
|
+
# empty one". Purely additional information: `==` compares `map(K, V)`, so
|
|
229
|
+
# two maps with the same K/V stay equal whatever their fields are.
|
|
230
|
+
attr_accessor :field_types
|
|
231
|
+
|
|
219
232
|
attr_reader :type_map
|
|
220
233
|
protected :type_map
|
|
221
234
|
|
|
222
235
|
def initialize(type_map)
|
|
223
236
|
super(:map)
|
|
224
237
|
@type_map = type_map
|
|
225
|
-
@
|
|
238
|
+
@key_type = MapType.uniform_type(@type_map.map { |k, _| MapType.node_type(k) })
|
|
239
|
+
@element_type = MapType.uniform_type(@type_map.map { |_, v| MapType.node_type(v) })
|
|
240
|
+
@field_types = nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# The single type shared by `types`, or `any` when they disagree (or when
|
|
244
|
+
# there are none) — the same widening ListType applies to a heterogeneous
|
|
245
|
+
# list literal.
|
|
246
|
+
def self.uniform_type(types)
|
|
247
|
+
types = types.compact
|
|
248
|
+
return TYPES[:any] if types.empty?
|
|
249
|
+
|
|
250
|
+
first_type = types.first
|
|
251
|
+
types.all? { |type| type == first_type } ? first_type : TYPES[:any]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A map-literal key is a constant or a bare identifier (see `Map#check`);
|
|
255
|
+
# an identifier key is a field name, i.e. a string.
|
|
256
|
+
#
|
|
257
|
+
# TODO: correct this in the parser instead. A bare identifier key is a
|
|
258
|
+
# field name, so the parser should emit a string node for it rather than an
|
|
259
|
+
# `Identifier` typed `any` that has to be patched up here.
|
|
260
|
+
def self.node_type(node)
|
|
261
|
+
return TYPES[:string] if node.is_a?(Identifier) && node.type == :any
|
|
262
|
+
|
|
263
|
+
node.respond_to?(:type) ? node.type : TYPES[:any]
|
|
226
264
|
end
|
|
227
265
|
|
|
228
266
|
def get(attrib)
|
|
267
|
+
attrib = attrib.to_s if attrib.is_a?(::Symbol)
|
|
229
268
|
_, value = @type_map.find { |k, _| k == attrib }
|
|
230
269
|
value
|
|
231
270
|
end
|
|
232
271
|
|
|
233
272
|
def ==(other)
|
|
234
|
-
|
|
235
|
-
|
|
273
|
+
return true if other.is_a?(::Symbol) && other == :map
|
|
274
|
+
return super unless other.is_a?(MapType)
|
|
275
|
+
|
|
276
|
+
MapType.compatible?(@key_type, other.key_type) &&
|
|
277
|
+
MapType.compatible?(@element_type, other.element_type)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# `any` is CEL's `dyn`: it stands in for an unknown type, so it is
|
|
281
|
+
# compatible with every other one. A map whose keys or values could not be
|
|
282
|
+
# narrowed (an empty literal, a heterogeneous one, `TYPES[:map]`) therefore
|
|
283
|
+
# matches any map, and only two *narrowed* parameters can disagree.
|
|
284
|
+
def self.compatible?(left, right)
|
|
285
|
+
return true if left.nil? || right.nil?
|
|
286
|
+
return true if left.to_sym == :any || right.to_sym == :any
|
|
287
|
+
|
|
288
|
+
left == right
|
|
236
289
|
end
|
|
237
290
|
|
|
238
291
|
def cast(value)
|
data/lib/cel/checker.rb
CHANGED
|
@@ -100,6 +100,23 @@ module Cel
|
|
|
100
100
|
Checker.new(@environment, @declarations ? @declarations.merge(declarations) : declarations)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
def infer_variable_type(var)
|
|
104
|
+
case var
|
|
105
|
+
when Identifier
|
|
106
|
+
check_identifier(var)
|
|
107
|
+
when Invoke
|
|
108
|
+
check_invoke(var)
|
|
109
|
+
when Literal
|
|
110
|
+
check_literal(var)
|
|
111
|
+
when Group
|
|
112
|
+
check(var.value)
|
|
113
|
+
when Protobuf.base_class
|
|
114
|
+
var.class
|
|
115
|
+
else
|
|
116
|
+
var.type
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
103
120
|
private
|
|
104
121
|
|
|
105
122
|
def check_literal(literal)
|
|
@@ -116,12 +133,47 @@ module Cel
|
|
|
116
133
|
end
|
|
117
134
|
lit
|
|
118
135
|
when Map
|
|
119
|
-
|
|
136
|
+
# Check each value expression, so that a type error inside the map literal raises here
|
|
137
|
+
# instead of surfacing at evaluation time. Keys are not checked: map-literal keys are
|
|
138
|
+
# constants or identifiers fixed at parse time, not expressions resolved against
|
|
139
|
+
# declarations (see Map#check).
|
|
140
|
+
# literal.value is an array of [key, value] pairs here, not a Hash
|
|
141
|
+
value_types = literal.value.map { |_key, value| call(value) }
|
|
142
|
+
|
|
143
|
+
lit = MapType.new(literal.value)
|
|
144
|
+
# MapType's constructor can only read the types the AST nodes already
|
|
145
|
+
# carry, and an Invoke/Condition value node carries none until it is
|
|
146
|
+
# checked. Now that every value expression has been, replace the
|
|
147
|
+
# constructor's guess with the checked types — the same refinement
|
|
148
|
+
# the List branch above does with `element_type`.
|
|
149
|
+
lit.element_type = MapType.uniform_type(value_types)
|
|
150
|
+
lit.field_types = field_types_for(literal.value, value_types)
|
|
151
|
+
lit
|
|
120
152
|
else
|
|
121
153
|
literal.type
|
|
122
154
|
end
|
|
123
155
|
end
|
|
124
156
|
|
|
157
|
+
# The per-key value types of a struct-like map literal, or nil when at
|
|
158
|
+
# least one key is not a constant (`{someVar: 1}`), in which case the map
|
|
159
|
+
# has no statically known key set.
|
|
160
|
+
def field_types_for(pairs, value_types)
|
|
161
|
+
fields = {}
|
|
162
|
+
|
|
163
|
+
pairs.each_with_index do |(key, _value), index|
|
|
164
|
+
bare_key =
|
|
165
|
+
case key
|
|
166
|
+
when Identifier then key.to_s
|
|
167
|
+
when Literal then key.value
|
|
168
|
+
else return nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
fields[bare_key] = value_types[index]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
fields
|
|
175
|
+
end
|
|
176
|
+
|
|
125
177
|
def check_message(literal)
|
|
126
178
|
literal.type
|
|
127
179
|
end
|
|
@@ -215,23 +267,6 @@ module Cel
|
|
|
215
267
|
unsupported_type(operation)
|
|
216
268
|
end
|
|
217
269
|
|
|
218
|
-
def infer_variable_type(var)
|
|
219
|
-
case var
|
|
220
|
-
when Identifier
|
|
221
|
-
check_identifier(var)
|
|
222
|
-
when Invoke
|
|
223
|
-
check_invoke(var)
|
|
224
|
-
when Literal
|
|
225
|
-
check_literal(var)
|
|
226
|
-
when Group
|
|
227
|
-
check(var.value)
|
|
228
|
-
when Protobuf.base_class
|
|
229
|
-
var.class
|
|
230
|
-
else
|
|
231
|
-
var.type
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
270
|
def check_invoke(funcall, var_type = nil)
|
|
236
271
|
var = funcall.var
|
|
237
272
|
func = funcall.func
|
|
@@ -240,7 +275,9 @@ module Cel
|
|
|
240
275
|
return check_standard_func(funcall) unless var
|
|
241
276
|
|
|
242
277
|
if var.is_a?(Identifier) && @environment.extensions.include?(var.to_sym)
|
|
243
|
-
|
|
278
|
+
type = @environment.extensions[var.to_sym].__check(funcall, checker: self)
|
|
279
|
+
unsupported_operation(funcall) unless type
|
|
280
|
+
return type
|
|
244
281
|
end
|
|
245
282
|
|
|
246
283
|
var_type ||= infer_variable_type(var)
|
|
@@ -265,7 +302,8 @@ module Cel
|
|
|
265
302
|
|
|
266
303
|
unsupported_type(funcall) unless identifier.is_a?(Identifier)
|
|
267
304
|
|
|
268
|
-
|
|
305
|
+
# A comprehension over a map iterates its KEYS.
|
|
306
|
+
element_checker = merge(identifier.to_sym => var_type.key_type)
|
|
269
307
|
|
|
270
308
|
unsupported_type(funcall) if element_checker.check(predicate) != :bool
|
|
271
309
|
|
|
@@ -332,11 +370,10 @@ module Cel
|
|
|
332
370
|
var_type.element_type = element_checker.check(predicate)
|
|
333
371
|
var_type
|
|
334
372
|
else
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
end
|
|
373
|
+
type = @environment.extensions[:strings]&.__check(funcall, checker: self) ||
|
|
374
|
+
@environment.extensions[:lists]&.__check(funcall, checker: self)
|
|
338
375
|
|
|
339
|
-
unsupported_operation(funcall)
|
|
376
|
+
type || unsupported_operation(funcall)
|
|
340
377
|
end
|
|
341
378
|
when TYPES[:string]
|
|
342
379
|
case func
|
|
@@ -349,7 +386,9 @@ module Cel
|
|
|
349
386
|
return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
|
|
350
387
|
else
|
|
351
388
|
if @environment.extensions.key?(:strings)
|
|
352
|
-
|
|
389
|
+
type = @environment.extensions[:strings].__check(funcall, checker: self)
|
|
390
|
+
unsupported_operation(funcall) unless type
|
|
391
|
+
return type
|
|
353
392
|
end
|
|
354
393
|
|
|
355
394
|
unsupported_type(funcall)
|
data/lib/cel/context.rb
CHANGED
|
@@ -111,7 +111,10 @@ module Cel
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def merge(bindings)
|
|
114
|
-
|
|
114
|
+
# NORMALIZE_KEYS returns a flat array of keys and values ([k1, v1, k2, v2, ...]),
|
|
115
|
+
# which must be regrouped into pairs: a map value with more than one key expands
|
|
116
|
+
# into more than one binding.
|
|
117
|
+
bindings = bindings.flat_map { |k, v| NORMALIZE_KEYS[k, v].each_slice(2).to_a }.to_h
|
|
115
118
|
if (old_bindings = @bindings)
|
|
116
119
|
if @environment.container
|
|
117
120
|
old_bindings = old_bindings.dup
|
data/lib/cel/extensions/bind.rb
CHANGED
|
@@ -13,13 +13,13 @@ module Cel
|
|
|
13
13
|
func = funcall.func
|
|
14
14
|
args = funcall.args
|
|
15
15
|
|
|
16
|
-
return
|
|
16
|
+
return unless func == :bind
|
|
17
17
|
|
|
18
18
|
checker.check_arity(func, args, 3)
|
|
19
19
|
|
|
20
20
|
id, type, expr = args
|
|
21
21
|
|
|
22
|
-
return
|
|
22
|
+
return unless id.is_a?(Identifier)
|
|
23
23
|
|
|
24
24
|
type = checker.call(type)
|
|
25
25
|
|
|
@@ -18,14 +18,12 @@ module Cel
|
|
|
18
18
|
when :encode
|
|
19
19
|
checker.check_arity(func, args, 1)
|
|
20
20
|
arg = checker.call(args.first)
|
|
21
|
-
|
|
21
|
+
TYPES[:string] if checker.find_match_all_types(%i[bytes], arg)
|
|
22
22
|
when :decode
|
|
23
23
|
checker.check_arity(func, args, 1)
|
|
24
24
|
arg = checker.call(args.first)
|
|
25
|
-
|
|
25
|
+
TYPES[:bytes] if checker.find_match_all_types(%i[string], arg)
|
|
26
26
|
end
|
|
27
|
-
|
|
28
|
-
checker.unsupported_operation(funcall)
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
def encode(str, program:)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cel
|
|
4
|
+
module Extensions
|
|
5
|
+
module List
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def implicit?
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def __check(funcall, checker:, var_type: nil)
|
|
13
|
+
var = funcall.var
|
|
14
|
+
func = funcall.func
|
|
15
|
+
args = funcall.args
|
|
16
|
+
|
|
17
|
+
var_type ||= checker.infer_variable_type(var)
|
|
18
|
+
|
|
19
|
+
case func
|
|
20
|
+
when :distinct, :reverse
|
|
21
|
+
checker.check_arity(funcall, args, 0)
|
|
22
|
+
var_type
|
|
23
|
+
when :flatten
|
|
24
|
+
checker.check_arity(funcall, args, 1, :>=)
|
|
25
|
+
if (arg = args.first) && arg.type != TYPES[:int]
|
|
26
|
+
checker.unsupported_type(funcall)
|
|
27
|
+
end
|
|
28
|
+
TYPES[:list]
|
|
29
|
+
when :range
|
|
30
|
+
checker.check_arity(funcall, args, 1)
|
|
31
|
+
if (arg = args.first) && arg.type != TYPES[:int]
|
|
32
|
+
checker.unsupported_type(funcall)
|
|
33
|
+
end
|
|
34
|
+
var_type
|
|
35
|
+
when :sort
|
|
36
|
+
checker.check_arity(funcall, args, 0)
|
|
37
|
+
checker.unsupported_type(funcall) if %i[list].include?(var_type.element_type)
|
|
38
|
+
var_type
|
|
39
|
+
when :sortBy
|
|
40
|
+
checker.check_arity(funcall, args, 2)
|
|
41
|
+
identifier, predicate = args
|
|
42
|
+
|
|
43
|
+
checker.nsupported_type(funcall) unless identifier.is_a?(Identifier)
|
|
44
|
+
|
|
45
|
+
identifier.type = var_type.element_type
|
|
46
|
+
|
|
47
|
+
element_checker = checker.merge(identifier.to_sym => var_type.element_type)
|
|
48
|
+
|
|
49
|
+
checker.unsupported_type(funcall) if element_checker.check(predicate) != var_type.element_type
|
|
50
|
+
var_type
|
|
51
|
+
when :slice
|
|
52
|
+
checker.check_arity(funcall, args, 2)
|
|
53
|
+
var_type if checker.find_match_all_types(%i[int], args.map(&checker.method(:call)))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def quote(str, program:)
|
|
58
|
+
value = program.call(str).value
|
|
59
|
+
|
|
60
|
+
# this should use String#dump, but this also replaces printable UTF8 characters
|
|
61
|
+
|
|
62
|
+
# replace quotes
|
|
63
|
+
value = value.gsub(/[\\'"\t\r\a\b\v\f\n]/) { |m| m.dump[1..-2] }
|
|
64
|
+
|
|
65
|
+
# replace invalid utf-8 chars
|
|
66
|
+
value = value.encode(Encoding::UTF_8, invalid: :replace)
|
|
67
|
+
|
|
68
|
+
# adding leading quotes if not quoted already
|
|
69
|
+
value = "\"#{value}\"" unless value.start_with?("\"") && value.end_with?("\"")
|
|
70
|
+
|
|
71
|
+
Cel::String.new(value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def range(num, program:)
|
|
75
|
+
num = program.call(num)
|
|
76
|
+
|
|
77
|
+
raise InvalidArgumentError, num unless num.between?(0, 1_000_000)
|
|
78
|
+
|
|
79
|
+
Cel::List.new(num.times)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class List
|
|
85
|
+
define_cel_method(:distinct) do
|
|
86
|
+
List.new(@value.uniq)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
define_cel_method(:flatten) do |*args|
|
|
90
|
+
depth = args.first || 1
|
|
91
|
+
|
|
92
|
+
raise InvalidArgumentError, depth if depth.negative?
|
|
93
|
+
|
|
94
|
+
flatten_proc = proc do |dth, el|
|
|
95
|
+
case el
|
|
96
|
+
when List
|
|
97
|
+
case dth
|
|
98
|
+
when 0
|
|
99
|
+
el
|
|
100
|
+
when nil
|
|
101
|
+
el.value.map do |el2|
|
|
102
|
+
flatten_proc.call(dth, el2)
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
el.value.map do |el2|
|
|
106
|
+
flatten_proc.call(dth - 1, el2)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
el
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Array#flatten does not know how to flatten lists; we'll need to turn
|
|
115
|
+
# lists into arrays so we can call Array#flatten.
|
|
116
|
+
flattenable_value = @value.map(&flatten_proc.curry.call(depth))
|
|
117
|
+
|
|
118
|
+
List.new(flattenable_value.flatten(depth))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
define_cel_method(:reverse) do
|
|
122
|
+
List.new(@value.reverse)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
define_cel_method(:sort) do
|
|
126
|
+
if @value.size == 1
|
|
127
|
+
# #sort call below won't enter the block otherwise, but cel requires
|
|
128
|
+
# validation of the unique element
|
|
129
|
+
el1 = @value.first
|
|
130
|
+
raise InvalidArgumentError, el1 if el1.is_a?(List)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sorted_value = @value.sort do |el1, el2|
|
|
134
|
+
raise InvalidArgumentError, el1 if el1.is_a?(List)
|
|
135
|
+
raise InvalidArgumentError, el2 if el2.is_a?(List)
|
|
136
|
+
|
|
137
|
+
el1 <=> el2
|
|
138
|
+
end
|
|
139
|
+
List.new(sorted_value)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
define_cel_method(:slice) do |a, b|
|
|
143
|
+
raise InvalidArgumentError, a if a.negative?
|
|
144
|
+
raise InvalidArgumentError, b if b > @value.size
|
|
145
|
+
|
|
146
|
+
List.new(@value.slice(a, b - a))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class Bool
|
|
151
|
+
def <=>(other)
|
|
152
|
+
return 0 unless other.is_a?(Bool) && @value != other.value
|
|
153
|
+
|
|
154
|
+
case @value
|
|
155
|
+
when false
|
|
156
|
+
# then other is true
|
|
157
|
+
-1
|
|
158
|
+
when true
|
|
159
|
+
# then other is false
|
|
160
|
+
1
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class Bytes
|
|
166
|
+
def <=>(other)
|
|
167
|
+
return 0 unless other.is_a?(Bytes)
|
|
168
|
+
|
|
169
|
+
@value <=> other.value
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
module Macro
|
|
174
|
+
def sortBy(collection, *identifiers, predicate, program:) # rubocop:disable Naming/MethodName
|
|
175
|
+
identifiers = identifiers.map(&:to_sym)
|
|
176
|
+
|
|
177
|
+
sorted_collection = collection.zip(
|
|
178
|
+
with_context(collection, identifiers).map do |context|
|
|
179
|
+
program.with_extra_context(context).evaluate(predicate)
|
|
180
|
+
end
|
|
181
|
+
).sort do |(_, pred1), (_, pred2)|
|
|
182
|
+
pred1 <=> pred2
|
|
183
|
+
end.map(&:first)
|
|
184
|
+
|
|
185
|
+
List.new(sorted_collection)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
data/lib/cel/extensions/math.rb
CHANGED
|
@@ -17,31 +17,29 @@ module Cel
|
|
|
17
17
|
when :round, :trunc, :floor, :ceil
|
|
18
18
|
checker.check_arity(func, args, 1)
|
|
19
19
|
arg = checker.call(args.first)
|
|
20
|
-
|
|
20
|
+
TYPES[:double] if checker.find_match_all_types(%i[double], arg)
|
|
21
21
|
when :isInf, :isNaN, :isFinite
|
|
22
22
|
checker.check_arity(func, args, 1)
|
|
23
23
|
arg = checker.call(args.first)
|
|
24
|
-
|
|
24
|
+
TYPES[:bool] if checker.find_match_all_types(%i[double], arg)
|
|
25
25
|
when :bitNot
|
|
26
26
|
checker.check_arity(func, args, 1)
|
|
27
27
|
arg = checker.call(args.first)
|
|
28
|
-
|
|
28
|
+
TYPES[:int] if checker.find_match_all_types(%i[int uint], arg)
|
|
29
29
|
when :bitOr, :bitAnd, :bitXor
|
|
30
30
|
checker.check_arity(func, args, 2)
|
|
31
31
|
args = args.map(&checker.method(:call))
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
return type if type
|
|
33
|
+
checker.find_match_all_types(%i[int uint], args)
|
|
35
34
|
when :sign, :abs
|
|
36
35
|
checker.check_arity(func, args, 1)
|
|
37
36
|
arg = checker.call(args.first)
|
|
38
|
-
|
|
39
|
-
return if type
|
|
37
|
+
checker.find_match_all_types(%i[double int uint], arg)
|
|
40
38
|
when :least, :greatest
|
|
41
39
|
checker.check_arity_any(func, args)
|
|
42
40
|
args = args.map(&checker.method(:call))
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
TYPES[:any] if args.all? do |arg|
|
|
45
43
|
case arg
|
|
46
44
|
when TYPES[:any], TYPES[:int], TYPES[:uint], TYPES[:double],
|
|
47
45
|
TYPES[:list, :int], TYPES[:list, :uint], TYPES[:list, :double], TYPES[:list, :any]
|
|
@@ -54,13 +52,9 @@ module Cel
|
|
|
54
52
|
checker.check_arity(func, args, 2)
|
|
55
53
|
num, amount_of_bits = args.map(&checker.method(:call))
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else
|
|
60
|
-
checker.unsupported_operation(funcall)
|
|
55
|
+
num if checker.find_match_all_types(%i[int uint], num) &&
|
|
56
|
+
checker.find_match_all_types(%i[int], amount_of_bits)
|
|
61
57
|
end
|
|
62
|
-
|
|
63
|
-
checker.unsupported_operation(funcall)
|
|
64
58
|
end
|
|
65
59
|
|
|
66
60
|
def abs(num, program:)
|
|
@@ -143,7 +137,7 @@ module Cel
|
|
|
143
137
|
arg = args.first
|
|
144
138
|
|
|
145
139
|
case arg
|
|
146
|
-
when List
|
|
140
|
+
when Cel::List
|
|
147
141
|
least(*arg.value, program: program)
|
|
148
142
|
else
|
|
149
143
|
arg
|
|
@@ -158,7 +152,7 @@ module Cel
|
|
|
158
152
|
arg = args.first
|
|
159
153
|
|
|
160
154
|
case arg
|
|
161
|
-
when List
|
|
155
|
+
when Cel::List
|
|
162
156
|
greatest(*arg.value, program: program)
|
|
163
157
|
else
|
|
164
158
|
arg
|
|
@@ -18,7 +18,7 @@ module Cel
|
|
|
18
18
|
when :quote
|
|
19
19
|
checker.check_arity(func, args, 1)
|
|
20
20
|
arg = checker.call(args.first)
|
|
21
|
-
|
|
21
|
+
TYPES[:string] if checker.find_match_all_types(%i[string], arg)
|
|
22
22
|
when :indexOf, :lastIndexOf
|
|
23
23
|
if checker.call(var) == TYPES[:string]
|
|
24
24
|
checker.check_arity(func, args, 2, :>=)
|
|
@@ -28,7 +28,7 @@ module Cel
|
|
|
28
28
|
return TYPES[:int] if pos.nil?
|
|
29
29
|
|
|
30
30
|
pos = checker.call(pos)
|
|
31
|
-
|
|
31
|
+
TYPES[:int] if checker.find_match_all_types(%i[int], pos)
|
|
32
32
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -41,7 +41,7 @@ module Cel
|
|
|
41
41
|
return TYPES[:list, :string] if pos.nil?
|
|
42
42
|
|
|
43
43
|
pos = checker.call(pos)
|
|
44
|
-
|
|
44
|
+
TYPES[:list, :string] if checker.find_match_all_types(%i[int], pos)
|
|
45
45
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -54,36 +54,36 @@ module Cel
|
|
|
54
54
|
return TYPES[:string] if pos.nil?
|
|
55
55
|
|
|
56
56
|
pos = checker.call(pos)
|
|
57
|
-
|
|
57
|
+
TYPES[:string] if checker.find_match_all_types(%i[int], pos)
|
|
58
58
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
when :trim, :lowerAscii, :upperAscii, :reverse
|
|
62
62
|
if checker.call(var) == TYPES[:string]
|
|
63
63
|
checker.check_arity(func, args, 0)
|
|
64
|
-
|
|
64
|
+
TYPES[:string]
|
|
65
65
|
end
|
|
66
66
|
when :format
|
|
67
67
|
if checker.call(var) == TYPES[:string]
|
|
68
68
|
checker.check_arity(func, args, 1)
|
|
69
69
|
arg = checker.call(args.first)
|
|
70
|
-
|
|
70
|
+
TYPES[:string] if checker.find_match_all_types(%i[list], arg)
|
|
71
71
|
end
|
|
72
72
|
when :charAt
|
|
73
73
|
if checker.call(var) == TYPES[:string]
|
|
74
74
|
checker.check_arity(func, args, 1)
|
|
75
75
|
arg = checker.call(args.first)
|
|
76
|
-
|
|
76
|
+
TYPES[:string] if checker.find_match_all_types(%i[int], arg)
|
|
77
77
|
end
|
|
78
78
|
when :replace
|
|
79
79
|
if checker.call(var) == TYPES[:string]
|
|
80
80
|
checker.check_arity(func, args, 2..3, :include?)
|
|
81
81
|
p, r, limit = args.map(&checker.method(:call))
|
|
82
82
|
if limit
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
TYPES[:string] if checker.find_match_all_types(%i[string], [p, r]) &&
|
|
84
|
+
checker.find_match_all_types(%i[int], limit)
|
|
85
85
|
elsif checker.find_match_all_types(%i[string], [p, r])
|
|
86
|
-
|
|
86
|
+
TYPES[:string]
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
when :join
|
|
@@ -91,12 +91,11 @@ module Cel
|
|
|
91
91
|
checker.check_arity(func, args, 1, :>=)
|
|
92
92
|
arg = checker.call(args.first)
|
|
93
93
|
return TYPES[:string] unless arg
|
|
94
|
-
|
|
94
|
+
|
|
95
|
+
TYPES[:string] if checker.find_match_all_types(%i[string], arg)
|
|
95
96
|
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
|
-
|
|
99
|
-
checker.unsupported_operation(funcall)
|
|
100
99
|
end
|
|
101
100
|
|
|
102
101
|
def quote(str, program:)
|
data/lib/cel/extensions.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "extensions/string"
|
|
|
5
5
|
require_relative "extensions/bind"
|
|
6
6
|
require_relative "extensions/encoders"
|
|
7
7
|
require_relative "extensions/network"
|
|
8
|
+
require_relative "extensions/list"
|
|
8
9
|
|
|
9
10
|
module Cel
|
|
10
11
|
EXTENSIONS = {
|
|
@@ -14,5 +15,6 @@ module Cel
|
|
|
14
15
|
cel: Extensions::Bind,
|
|
15
16
|
net: Extensions::Network,
|
|
16
17
|
ip: Extensions::Network::IP,
|
|
18
|
+
lists: Extensions::List,
|
|
17
19
|
}.freeze
|
|
18
20
|
end
|
data/lib/cel/macro.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Cel
|
|
4
4
|
module Macro
|
|
5
|
-
|
|
5
|
+
extend self
|
|
6
6
|
|
|
7
7
|
# If e evaluates to a protocol buffers version 2 message and f is a defined field:
|
|
8
8
|
# If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
|
|
@@ -64,7 +64,10 @@ module Cel
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def matches(string, pattern, program: nil)
|
|
67
|
-
|
|
67
|
+
if program
|
|
68
|
+
string = program.evaluate(string)
|
|
69
|
+
pattern = program.evaluate(pattern)
|
|
70
|
+
end
|
|
68
71
|
pattern = Regexp.new(pattern)
|
|
69
72
|
Bool.cast(pattern.match?(string))
|
|
70
73
|
end
|
|
@@ -128,6 +131,8 @@ module Cel
|
|
|
128
131
|
List.new(return_value)
|
|
129
132
|
end
|
|
130
133
|
|
|
134
|
+
private
|
|
135
|
+
|
|
131
136
|
def with_context(collection, identifiers)
|
|
132
137
|
case collection
|
|
133
138
|
when Map
|
data/lib/cel/version.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.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tiago Cardoso
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/cel/extensions.rb
|
|
109
109
|
- lib/cel/extensions/bind.rb
|
|
110
110
|
- lib/cel/extensions/encoders.rb
|
|
111
|
+
- lib/cel/extensions/list.rb
|
|
111
112
|
- lib/cel/extensions/math.rb
|
|
112
113
|
- lib/cel/extensions/network.rb
|
|
113
114
|
- lib/cel/extensions/string.rb
|
|
@@ -137,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
138
|
- !ruby/object:Gem::Version
|
|
138
139
|
version: '0'
|
|
139
140
|
requirements: []
|
|
140
|
-
rubygems_version:
|
|
141
|
+
rubygems_version: 4.0.16
|
|
141
142
|
specification_version: 4
|
|
142
143
|
summary: Pure Ruby implementation of Google Common Expression Language, https://opensource.google/projects/cel.
|
|
143
144
|
test_files: []
|