cel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|