cel 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +239 -0
- data/README.md +101 -0
- data/lib/cel/ast/elements.rb +322 -0
- data/lib/cel/ast/types.rb +78 -0
- data/lib/cel/checker.rb +323 -0
- data/lib/cel/context.rb +36 -0
- data/lib/cel/environment.rb +48 -0
- data/lib/cel/errors.rb +31 -0
- data/lib/cel/macro.rb +75 -0
- data/lib/cel/parser.rb +996 -0
- data/lib/cel/program.rb +147 -0
- data/lib/cel/protobuf.rb +175 -0
- data/lib/cel/version.rb +7 -0
- data/lib/cel.rb +29 -0
- metadata +80 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
class Type
|
5
|
+
def initialize(type)
|
6
|
+
@type = type
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
other == @type || super
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_str
|
14
|
+
@type.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def type
|
18
|
+
TYPES[:type]
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast(value)
|
22
|
+
case @type
|
23
|
+
when :int
|
24
|
+
Number.new(:int, Integer(value))
|
25
|
+
when :uint
|
26
|
+
Number.new(:uint, Integer(value).abs)
|
27
|
+
when :double
|
28
|
+
Number.new(:double, Float(value))
|
29
|
+
when :string
|
30
|
+
String.new(String(value))
|
31
|
+
when :bytes
|
32
|
+
Bytes.new(value.bytes)
|
33
|
+
when :bool
|
34
|
+
Bool.new(value)
|
35
|
+
when :any
|
36
|
+
value
|
37
|
+
else
|
38
|
+
raise Error, "unsupported cast operation to #{@type}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ListType < Type
|
44
|
+
attr_accessor :element_type
|
45
|
+
|
46
|
+
def initialize(type_list)
|
47
|
+
super(:list)
|
48
|
+
@type_list = type_list
|
49
|
+
@element_type = @type_list.empty? ? TYPES[:any] : @type_list.sample.type
|
50
|
+
end
|
51
|
+
|
52
|
+
def get(idx)
|
53
|
+
@type_list[idx]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class MapType < Type
|
58
|
+
attr_accessor :element_type
|
59
|
+
|
60
|
+
def initialize(type_map)
|
61
|
+
super(:map)
|
62
|
+
@type_map = type_map
|
63
|
+
@element_type = @type_map.empty? ? TYPES[:any] : @type_map.keys.sample.type
|
64
|
+
end
|
65
|
+
|
66
|
+
def get(attrib)
|
67
|
+
_, value = @type_map.find { |k, _| k == attrib.to_s }
|
68
|
+
value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Primitive Cel Types
|
73
|
+
|
74
|
+
PRIMITIVE_TYPES = %i[int uint double bool string bytes list map null_type type].freeze
|
75
|
+
TYPES = PRIMITIVE_TYPES.map { |typ| [typ, Type.new(typ)] }.to_h
|
76
|
+
TYPES[:type] = Type.new(:type)
|
77
|
+
TYPES[:any] = Type.new(:any)
|
78
|
+
end
|
data/lib/cel/checker.rb
ADDED
@@ -0,0 +1,323 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
class Checker
|
5
|
+
def initialize(declarations)
|
6
|
+
@declarations = declarations
|
7
|
+
end
|
8
|
+
|
9
|
+
def check(ast)
|
10
|
+
case ast
|
11
|
+
when Group
|
12
|
+
check(ast.value)
|
13
|
+
when Invoke
|
14
|
+
check_invoke(ast)
|
15
|
+
when Operation
|
16
|
+
check_operation(ast)
|
17
|
+
when Literal
|
18
|
+
ast.type
|
19
|
+
when Identifier
|
20
|
+
check_identifier(ast)
|
21
|
+
when Condition
|
22
|
+
check_condition(ast)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :call, :check
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def merge(declarations)
|
31
|
+
Checker.new(@declarations ? @declarations.merge(declarations) : declarations)
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO: add protobuf timestamp and duration
|
35
|
+
LOGICAL_EXPECTED_TYPES = %i[bool int uint double string bytes].freeze
|
36
|
+
ADD_EXPECTED_TYPES = %i[int uint double string bytes list].freeze
|
37
|
+
SUB_EXPECTED_TYPES = %i[int uint double].freeze
|
38
|
+
MULTIDIV_EXPECTED_TYPES = %i[int uint double].freeze
|
39
|
+
REMAINDER_EXPECTED_TYPES = %i[int uint].freeze
|
40
|
+
|
41
|
+
def check_operation(operation)
|
42
|
+
type = infer_operation_type(operation)
|
43
|
+
operation.type = type
|
44
|
+
type
|
45
|
+
end
|
46
|
+
|
47
|
+
BOOLABLE_OPERATORS = %w[&& || == != < <= >= >].freeze
|
48
|
+
|
49
|
+
def infer_operation_type(operation)
|
50
|
+
op = operation.op
|
51
|
+
|
52
|
+
values = operation.operands.map do |operand|
|
53
|
+
ev_operand = call(operand)
|
54
|
+
|
55
|
+
return TYPES[:any] if ev_operand == :any && !BOOLABLE_OPERATORS.include?(op)
|
56
|
+
|
57
|
+
ev_operand
|
58
|
+
end
|
59
|
+
|
60
|
+
if values.size == 1
|
61
|
+
# unary ops
|
62
|
+
type = values.first
|
63
|
+
case op
|
64
|
+
when "!"
|
65
|
+
return type if type == :bool
|
66
|
+
|
67
|
+
when "-"
|
68
|
+
return type if type == :int || type == :double # rubocop:disable Style/MultipleComparison
|
69
|
+
|
70
|
+
else
|
71
|
+
unsupported_type(operation)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
|
75
|
+
case op
|
76
|
+
when "&&", "||", "==", "!=", "<", "<=", ">=", ">"
|
77
|
+
return TYPES[:bool]
|
78
|
+
when "in"
|
79
|
+
return TYPES[:bool] if find_match_all_types(%i[list map], values.last)
|
80
|
+
when "+"
|
81
|
+
if (type = find_match_all_types(ADD_EXPECTED_TYPES, values))
|
82
|
+
return type
|
83
|
+
end
|
84
|
+
when "-"
|
85
|
+
if (type = find_match_all_types(SUB_EXPECTED_TYPES, values))
|
86
|
+
return type
|
87
|
+
end
|
88
|
+
when "*", "/"
|
89
|
+
if (type = find_match_all_types(MULTIDIV_EXPECTED_TYPES, values))
|
90
|
+
return type
|
91
|
+
end
|
92
|
+
when "%"
|
93
|
+
if (type = find_match_all_types(REMAINDER_EXPECTED_TYPES, values))
|
94
|
+
return type
|
95
|
+
end
|
96
|
+
else
|
97
|
+
unsupported_type(operation)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
unsupported_type(operation)
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_invoke(funcall, var_type = nil)
|
104
|
+
var = funcall.var
|
105
|
+
func = funcall.func
|
106
|
+
args = funcall.args
|
107
|
+
|
108
|
+
return check_standard_func(funcall) unless var
|
109
|
+
|
110
|
+
var_type ||= case var
|
111
|
+
when Identifier
|
112
|
+
check_identifier(var)
|
113
|
+
when Invoke
|
114
|
+
check_invoke(var)
|
115
|
+
else
|
116
|
+
var.type
|
117
|
+
end
|
118
|
+
|
119
|
+
case var_type
|
120
|
+
when MapType
|
121
|
+
# A field selection expression, e.f, can be applied both to messages and
|
122
|
+
# to maps. For maps, selection is interpreted as the field being a string key.
|
123
|
+
case func
|
124
|
+
when :[]
|
125
|
+
attribute = var_type.get(args)
|
126
|
+
unsupported_operation(funcall) unless attribute
|
127
|
+
when :all, :exists, :exists_one
|
128
|
+
check_arity(funcall, args, 2)
|
129
|
+
identifier, predicate = args
|
130
|
+
|
131
|
+
unsupported_type(funcall) unless identifier.is_a?(Identifier)
|
132
|
+
|
133
|
+
element_checker = merge(identifier.to_sym => var_type.element_type)
|
134
|
+
|
135
|
+
unsupported_type(funcall) if element_checker.check(predicate) != :bool
|
136
|
+
|
137
|
+
return TYPES[:bool]
|
138
|
+
else
|
139
|
+
attribute = var_type.get(func)
|
140
|
+
unsupported_operation(funcall) unless attribute
|
141
|
+
end
|
142
|
+
|
143
|
+
call(attribute)
|
144
|
+
when ListType
|
145
|
+
case func
|
146
|
+
when :[]
|
147
|
+
attribute = var_type.get(args)
|
148
|
+
unsupported_operation(funcall) unless attribute
|
149
|
+
call(attribute)
|
150
|
+
when :all, :exists, :exists_one
|
151
|
+
check_arity(funcall, args, 2)
|
152
|
+
identifier, predicate = args
|
153
|
+
|
154
|
+
unsupported_type(funcall) unless identifier.is_a?(Identifier)
|
155
|
+
|
156
|
+
element_checker = merge(identifier.to_sym => var_type.element_type)
|
157
|
+
|
158
|
+
unsupported_type(funcall) if element_checker.check(predicate) != :bool
|
159
|
+
|
160
|
+
TYPES[:bool]
|
161
|
+
when :filter
|
162
|
+
check_arity(funcall, args, 2)
|
163
|
+
identifier, predicate = args
|
164
|
+
|
165
|
+
unsupported_type(funcall) unless identifier.is_a?(Identifier)
|
166
|
+
|
167
|
+
element_checker = merge(identifier.to_sym => var_type.element_type)
|
168
|
+
|
169
|
+
unsupported_type(funcall) if element_checker.check(predicate) != :bool
|
170
|
+
|
171
|
+
var_type
|
172
|
+
when :map
|
173
|
+
check_arity(funcall, args, 2)
|
174
|
+
identifier, predicate = args
|
175
|
+
|
176
|
+
unsupported_type(funcall) unless identifier.is_a?(Identifier)
|
177
|
+
|
178
|
+
element_checker = merge(identifier.to_sym => var_type.element_type)
|
179
|
+
|
180
|
+
var_type.element_type = element_checker.check(predicate)
|
181
|
+
var_type
|
182
|
+
else
|
183
|
+
unsupported_operation(funcall)
|
184
|
+
end
|
185
|
+
when TYPES[:string]
|
186
|
+
case func
|
187
|
+
when :contains, :endsWith, :startsWith
|
188
|
+
check_arity(funcall, args, 1)
|
189
|
+
return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
|
190
|
+
when :matches # rubocop:disable Lint/DuplicateBranch
|
191
|
+
check_arity(funcall, args, 1)
|
192
|
+
# TODO: verify if string can be transformed into a regex
|
193
|
+
return TYPES[:bool] if find_match_all_types(%i[string], call(args.first))
|
194
|
+
else
|
195
|
+
unsupported_type(funcall)
|
196
|
+
end
|
197
|
+
unsupported_operation(funcall)
|
198
|
+
else
|
199
|
+
TYPES[:any]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
CAST_ALLOWED_TYPES = {
|
204
|
+
int: %i[uint double string], # TODO: enum, timestamp
|
205
|
+
uint: %i[int double string],
|
206
|
+
string: %i[int uint double bytes], # TODO: timestamp, duration
|
207
|
+
double: %i[int uint string],
|
208
|
+
bytes: %i[string],
|
209
|
+
}.freeze
|
210
|
+
|
211
|
+
def check_standard_func(funcall)
|
212
|
+
func = funcall.func
|
213
|
+
args = funcall.args
|
214
|
+
|
215
|
+
case func
|
216
|
+
when :type
|
217
|
+
check_arity(func, args, 1)
|
218
|
+
return TYPES[:type]
|
219
|
+
when :has
|
220
|
+
check_arity(func, args, 1)
|
221
|
+
unsupported_type(funcall) unless args.first.is_a?(Invoke)
|
222
|
+
|
223
|
+
return TYPES[:bool]
|
224
|
+
when :size
|
225
|
+
check_arity(func, args, 1)
|
226
|
+
return TYPES[:int] if find_match_all_types(%i[string bytes list map], call(args.first))
|
227
|
+
when :int, :uint, :string, :double, :bytes # :duration, :timestamp
|
228
|
+
check_arity(func, args, 1)
|
229
|
+
allowed_types = CAST_ALLOWED_TYPES[func]
|
230
|
+
|
231
|
+
return TYPES[func] if find_match_all_types(allowed_types, call(args.first))
|
232
|
+
when :matches
|
233
|
+
check_arity(func, args, 2)
|
234
|
+
return TYPES[:bool] if find_match_all_types(%i[string], args.map { |arg| call(arg) })
|
235
|
+
when :dyn
|
236
|
+
check_arity(func, args, 1)
|
237
|
+
arg_type = call(args.first)
|
238
|
+
case arg_type
|
239
|
+
when ListType, MapType
|
240
|
+
arg_type.element_type = TYPES[:any]
|
241
|
+
end
|
242
|
+
return arg_type
|
243
|
+
else
|
244
|
+
unsupported_type(funcall)
|
245
|
+
end
|
246
|
+
|
247
|
+
unsupported_operation(funcall)
|
248
|
+
end
|
249
|
+
|
250
|
+
def check_identifier(identifier)
|
251
|
+
return unless identifier.type == :any
|
252
|
+
|
253
|
+
return TYPES[:type] if Cel::PRIMITIVE_TYPES.include?(identifier.to_sym)
|
254
|
+
|
255
|
+
id_type = infer_dec_type(identifier.id)
|
256
|
+
|
257
|
+
return TYPES[:any] unless id_type
|
258
|
+
|
259
|
+
identifier.type = id_type
|
260
|
+
|
261
|
+
id_type
|
262
|
+
end
|
263
|
+
|
264
|
+
def check_condition(condition)
|
265
|
+
then_type = call(condition.then)
|
266
|
+
else_type = call(condition.else)
|
267
|
+
|
268
|
+
return then_type if then_type == else_type
|
269
|
+
|
270
|
+
TYPES[:any]
|
271
|
+
end
|
272
|
+
|
273
|
+
def infer_dec_type(id)
|
274
|
+
return unless @declarations
|
275
|
+
|
276
|
+
var_name, *id_call_chain = id.split(".").map(&:to_sym)
|
277
|
+
|
278
|
+
typ = @declarations[var_name]
|
279
|
+
|
280
|
+
return unless typ
|
281
|
+
|
282
|
+
return convert(typ) if id_call_chain.empty?
|
283
|
+
end
|
284
|
+
|
285
|
+
def convert(typ)
|
286
|
+
case typ
|
287
|
+
when Symbol
|
288
|
+
TYPES[typ] or
|
289
|
+
raise CheckError, "#{typ} is not aa valid type"
|
290
|
+
else
|
291
|
+
typ
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def find_match_all_types(expected, types)
|
296
|
+
# at least an expected type must match all values
|
297
|
+
type = expected.find do |expected_type|
|
298
|
+
case types
|
299
|
+
when Array
|
300
|
+
types.all? { |typ| typ == expected_type }
|
301
|
+
else
|
302
|
+
types == expected_type
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
TYPES[type]
|
307
|
+
end
|
308
|
+
|
309
|
+
def check_arity(func, args, arity)
|
310
|
+
return if args.size == arity
|
311
|
+
|
312
|
+
raise CheckError, "`#{func}` invoked with wrong number of arguments (should be #{arity})"
|
313
|
+
end
|
314
|
+
|
315
|
+
def unsupported_type(op)
|
316
|
+
raise NoMatchingOverloadError, op
|
317
|
+
end
|
318
|
+
|
319
|
+
def unsupported_operation(op)
|
320
|
+
raise CheckError, "unsupported operation (#{op})"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
data/lib/cel/context.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
class Context
|
5
|
+
def initialize(bindings)
|
6
|
+
@bindings = bindings.dup
|
7
|
+
|
8
|
+
return unless @bindings
|
9
|
+
|
10
|
+
@bindings.each do |k, v|
|
11
|
+
@bindings[k] = to_cel_type(v)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def lookup(identifier)
|
16
|
+
raise EvaluateError, "no value in context for #{identifier}" unless @bindings
|
17
|
+
|
18
|
+
id = identifier.id
|
19
|
+
val = @bindings.dig(*id.split(".").map(&:to_sym))
|
20
|
+
|
21
|
+
raise EvaluateError, "no value in context for #{identifier}" unless val
|
22
|
+
|
23
|
+
val
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge(bindings)
|
27
|
+
Context.new(@bindings ? @bindings.merge(bindings) : bindings)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def to_cel_type(v)
|
33
|
+
Literal.to_cel_type(v)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
class Environment
|
5
|
+
def initialize(declarations = nil)
|
6
|
+
@declarations = declarations
|
7
|
+
@parser = Parser.new
|
8
|
+
@checker = Checker.new(@declarations)
|
9
|
+
end
|
10
|
+
|
11
|
+
def compile(expr)
|
12
|
+
ast = @parser.parse(expr)
|
13
|
+
@checker.check(ast)
|
14
|
+
ast
|
15
|
+
end
|
16
|
+
|
17
|
+
def check(expr)
|
18
|
+
ast = @parser.parse(expr)
|
19
|
+
@checker.check(ast)
|
20
|
+
end
|
21
|
+
|
22
|
+
def program(expr)
|
23
|
+
expr = compile(expr) if expr.is_a?(::String)
|
24
|
+
Runner.new(expr)
|
25
|
+
end
|
26
|
+
|
27
|
+
def evaluate(expr, bindings = nil)
|
28
|
+
context = Context.new(bindings)
|
29
|
+
expr = compile(expr) if expr.is_a?(::String)
|
30
|
+
Program.new(context).evaluate(expr)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate(ast, structs); end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Runner
|
39
|
+
def initialize(ast)
|
40
|
+
@ast = ast
|
41
|
+
end
|
42
|
+
|
43
|
+
def evaluate(bindings = nil)
|
44
|
+
context = Context.new(bindings)
|
45
|
+
Program.new(context).evaluate(@ast)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/cel/errors.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ParseError < Error; end
|
7
|
+
|
8
|
+
class CheckError < Error; end
|
9
|
+
|
10
|
+
class EvaluateError < Error; end
|
11
|
+
|
12
|
+
class NoSuchFieldError < EvaluateError
|
13
|
+
attr_reader :code
|
14
|
+
|
15
|
+
def initialize(var, attrib)
|
16
|
+
super("No such field: #{var}.#{attrib}")
|
17
|
+
@code = :no_such_field
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NoMatchingOverloadError < CheckError
|
22
|
+
attr_reader :code
|
23
|
+
|
24
|
+
def initialize(op)
|
25
|
+
super("No matching overload: #{op}")
|
26
|
+
@code = :no_matching_overload
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class BindingError < EvaluateError; end
|
31
|
+
end
|
data/lib/cel/macro.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cel
|
4
|
+
module Macro
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# If e evaluates to a protocol buffers version 2 message and f is a defined field:
|
8
|
+
# If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
|
9
|
+
# If f is a singular or oneof field, has(e.f) indicates whether the field is set.
|
10
|
+
# If e evaluates to a protocol buffers version 3 message and f is a defined field:
|
11
|
+
# If f is a repeated field or map field, has(e.f) indicates whether the field is non-empty.
|
12
|
+
# If f is a oneof or singular message field, has(e.f) indicates whether the field is set.
|
13
|
+
# If f is some other singular field, has(e.f) indicates whether the field's value is its default
|
14
|
+
# value (zero for numeric fields, false for booleans, empty for strings and bytes).
|
15
|
+
def has(invoke)
|
16
|
+
var = invoke.var
|
17
|
+
func = invoke.func
|
18
|
+
|
19
|
+
case var
|
20
|
+
when Message
|
21
|
+
# If e evaluates to a message and f is not a declared field for the message,
|
22
|
+
# has(e.f) raises a no_such_field error.
|
23
|
+
raise NoSuchFieldError.new(var, func) unless var.field?(func)
|
24
|
+
|
25
|
+
Bool.new(!var.public_send(func).nil?)
|
26
|
+
when Map
|
27
|
+
# If e evaluates to a map, then has(e.f) indicates whether the string f
|
28
|
+
# is a key in the map (note that f must syntactically be an identifier).
|
29
|
+
Bool.new(var.respond_to?(func))
|
30
|
+
else
|
31
|
+
# In all other cases, has(e.f) evaluates to an error.
|
32
|
+
raise EvaluateError, "#{invoke} is not supported"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def size(literal)
|
37
|
+
literal.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def matches(string, pattern)
|
41
|
+
pattern = Regexp.new(pattern)
|
42
|
+
Bool.new(pattern.match?(string))
|
43
|
+
end
|
44
|
+
|
45
|
+
def all(collection, identifier, predicate, context:)
|
46
|
+
collection.all? do |element, *|
|
47
|
+
Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def exists(collection, identifier, predicate, context:)
|
52
|
+
collection.any? do |element, *|
|
53
|
+
Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exists_one(collection, identifier, predicate, context:)
|
58
|
+
collection.one? do |element, *|
|
59
|
+
Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def filter(collection, identifier, predicate, context:)
|
64
|
+
collection.select do |element, *|
|
65
|
+
Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate).value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def map(collection, identifier, predicate, context:)
|
70
|
+
collection.map do |element, *|
|
71
|
+
Program.new(context.merge(identifier.to_sym => element)).evaluate(predicate)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|