jmespath 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of jmespath might be problematic. Click here for more details.

@@ -0,0 +1,40 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Node
5
+ def visit(value)
6
+ end
7
+
8
+ def hash_like?(value)
9
+ Hash === value || Struct === value
10
+ end
11
+
12
+ def optimize
13
+ self
14
+ end
15
+
16
+ def chains_with?(other)
17
+ false
18
+ end
19
+ end
20
+
21
+ autoload :Comparator, 'jmespath/nodes/comparator'
22
+ autoload :Condition, 'jmespath/nodes/condition'
23
+ autoload :Current, 'jmespath/nodes/current'
24
+ autoload :Expression, 'jmespath/nodes/expression'
25
+ autoload :Field, 'jmespath/nodes/field'
26
+ autoload :Flatten, 'jmespath/nodes/flatten'
27
+ autoload :Function, 'jmespath/nodes/function'
28
+ autoload :Index, 'jmespath/nodes/index'
29
+ autoload :Literal, 'jmespath/nodes/literal'
30
+ autoload :MultiSelectHash, 'jmespath/nodes/multi_select_hash'
31
+ autoload :MultiSelectList, 'jmespath/nodes/multi_select_list'
32
+ autoload :Or, 'jmespath/nodes/or'
33
+ autoload :Pipe, 'jmespath/nodes/pipe'
34
+ autoload :Projection, 'jmespath/nodes/projection'
35
+ autoload :ArrayProjection, 'jmespath/nodes/projection'
36
+ autoload :ObjectProjection, 'jmespath/nodes/projection'
37
+ autoload :Slice, 'jmespath/nodes/slice'
38
+ autoload :Subexpression, 'jmespath/nodes/subexpression'
39
+ end
40
+ end
@@ -0,0 +1,77 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Comparator < Node
5
+ attr_reader :left, :right
6
+
7
+ def initialize(left, right)
8
+ @left = left
9
+ @right = right
10
+ end
11
+
12
+ def self.create(relation, left, right)
13
+ type = begin
14
+ case relation
15
+ when '==' then EqComparator
16
+ when '!=' then NeqComparator
17
+ when '>' then GtComparator
18
+ when '>=' then GteComparator
19
+ when '<' then LtComparator
20
+ when '<=' then LteComparator
21
+ end
22
+ end
23
+ type.new(left, right)
24
+ end
25
+
26
+ def visit(value)
27
+ check(@left.visit(value), @right.visit(value))
28
+ end
29
+
30
+ def optimize
31
+ self.class.new(@left.optimize, @right.optimize)
32
+ end
33
+
34
+ private
35
+
36
+ def check(left_value, right_value)
37
+ nil
38
+ end
39
+ end
40
+
41
+ class EqComparator < Comparator
42
+ def check(left_value, right_value)
43
+ left_value == right_value
44
+ end
45
+ end
46
+
47
+ class NeqComparator < Comparator
48
+ def check(left_value, right_value)
49
+ left_value != right_value
50
+ end
51
+ end
52
+
53
+ class GtComparator < Comparator
54
+ def check(left_value, right_value)
55
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value
56
+ end
57
+ end
58
+
59
+ class GteComparator < Comparator
60
+ def check(left_value, right_value)
61
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value
62
+ end
63
+ end
64
+
65
+ class LtComparator < Comparator
66
+ def check(left_value, right_value)
67
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value
68
+ end
69
+ end
70
+
71
+ class LteComparator < Comparator
72
+ def check(left_value, right_value)
73
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,136 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Condition < Node
5
+ def initialize(test, child)
6
+ @test = test
7
+ @child = child
8
+ end
9
+
10
+ def visit(value)
11
+ if @test.visit(value)
12
+ @child.visit(value)
13
+ else
14
+ nil
15
+ end
16
+ end
17
+
18
+ def optimize
19
+ test = @test.optimize
20
+ if (new_type = ComparatorCondition::COMPARATOR_TO_CONDITION[@test.class])
21
+ new_type.new(test.left, test.right, @child).optimize
22
+ else
23
+ self.class.new(test, @child.optimize)
24
+ end
25
+ end
26
+ end
27
+
28
+ class ComparatorCondition < Node
29
+ COMPARATOR_TO_CONDITION = {}
30
+
31
+ def initialize(left, right, child)
32
+ @left = left
33
+ @right = right
34
+ @child = child
35
+ end
36
+
37
+ def visit(value)
38
+ nil
39
+ end
40
+ end
41
+
42
+ class EqCondition < ComparatorCondition
43
+ COMPARATOR_TO_CONDITION[EqComparator] = self
44
+
45
+ def visit(value)
46
+ @left.visit(value) == @right.visit(value) ? @child.visit(value) : nil
47
+ end
48
+
49
+ def optimize
50
+ if @right.is_a?(Literal)
51
+ LiteralRightEqCondition.new(@left, @right, @child)
52
+ else
53
+ self
54
+ end
55
+ end
56
+ end
57
+
58
+ class LiteralRightEqCondition < EqCondition
59
+ def initialize(left, right, child)
60
+ super
61
+ @right = @right.value
62
+ end
63
+
64
+ def visit(value)
65
+ @left.visit(value) == @right ? @child.visit(value) : nil
66
+ end
67
+ end
68
+
69
+ class NeqCondition < ComparatorCondition
70
+ COMPARATOR_TO_CONDITION[NeqComparator] = self
71
+
72
+ def visit(value)
73
+ @left.visit(value) != @right.visit(value) ? @child.visit(value) : nil
74
+ end
75
+
76
+ def optimize
77
+ if @right.is_a?(Literal)
78
+ LiteralRightNeqCondition.new(@left, @right, @child)
79
+ else
80
+ self
81
+ end
82
+ end
83
+ end
84
+
85
+ class LiteralRightNeqCondition < NeqCondition
86
+ def initialize(left, right, child)
87
+ super
88
+ @right = @right.value
89
+ end
90
+
91
+ def visit(value)
92
+ @left.visit(value) != @right ? @child.visit(value) : nil
93
+ end
94
+ end
95
+
96
+ class GtCondition < ComparatorCondition
97
+ COMPARATOR_TO_CONDITION[GtComparator] = self
98
+
99
+ def visit(value)
100
+ left_value = @left.visit(value)
101
+ right_value = @right.visit(value)
102
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value ? @child.visit(value) : nil
103
+ end
104
+ end
105
+
106
+ class GteCondition < ComparatorCondition
107
+ COMPARATOR_TO_CONDITION[GteComparator] = self
108
+
109
+ def visit(value)
110
+ left_value = @left.visit(value)
111
+ right_value = @right.visit(value)
112
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value ? @child.visit(value) : nil
113
+ end
114
+ end
115
+
116
+ class LtCondition < ComparatorCondition
117
+ COMPARATOR_TO_CONDITION[LtComparator] = self
118
+
119
+ def visit(value)
120
+ left_value = @left.visit(value)
121
+ right_value = @right.visit(value)
122
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value ? @child.visit(value) : nil
123
+ end
124
+ end
125
+
126
+ class LteCondition < ComparatorCondition
127
+ COMPARATOR_TO_CONDITION[LteComparator] = self
128
+
129
+ def visit(value)
130
+ left_value = @left.visit(value)
131
+ right_value = @right.visit(value)
132
+ left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value ? @child.visit(value) : nil
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,10 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Current < Node
5
+ def visit(value)
6
+ value
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Expression < Node
5
+ attr_reader :expression
6
+
7
+ def initialize(expression)
8
+ @expression = expression
9
+ end
10
+
11
+ def visit(value)
12
+ self
13
+ end
14
+
15
+ def eval(value)
16
+ @expression.visit(value)
17
+ end
18
+
19
+ def optimize
20
+ self.class.new(@expression.optimize)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,74 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Field < Node
5
+ def initialize(key)
6
+ @key = key
7
+ @key_sym = key.respond_to?(:to_sym) ? key.to_sym : nil
8
+ end
9
+
10
+ def visit(value)
11
+ if value.is_a?(Array) && @key.is_a?(Integer)
12
+ value[@key]
13
+ elsif value.is_a?(Hash)
14
+ if !(v = value[@key]).nil?
15
+ v
16
+ elsif @key_sym && !(v = value[@key_sym]).nil?
17
+ v
18
+ end
19
+ elsif value.is_a?(Struct) && value.respond_to?(@key)
20
+ value[@key]
21
+ end
22
+ end
23
+
24
+ def chains_with?(other)
25
+ other.is_a?(Field)
26
+ end
27
+
28
+ def chain(other)
29
+ ChainedField.new([@key, *other.keys])
30
+ end
31
+
32
+ protected
33
+
34
+ def keys
35
+ [@key]
36
+ end
37
+ end
38
+
39
+ class ChainedField < Field
40
+ def initialize(keys)
41
+ @keys = keys
42
+ @key_syms = keys.each_with_object({}) do |k, syms|
43
+ if k.respond_to?(:to_sym)
44
+ syms[k] = k.to_sym
45
+ end
46
+ end
47
+ end
48
+
49
+ def visit(value)
50
+ @keys.reduce(value) do |value, key|
51
+ if value.is_a?(Array) && key.is_a?(Integer)
52
+ value[key]
53
+ elsif value.is_a?(Hash)
54
+ if !(v = value[key]).nil?
55
+ v
56
+ elsif (sym = @key_syms[key]) && !(v = value[sym]).nil?
57
+ v
58
+ end
59
+ elsif value.is_a?(Struct) && value.respond_to?(key)
60
+ value[key]
61
+ end
62
+ end
63
+ end
64
+
65
+ def chain(other)
66
+ ChainedField.new([*@keys, *other.keys])
67
+ end
68
+
69
+ private
70
+
71
+ attr_reader :keys
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,29 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Flatten < Node
5
+ def initialize(child)
6
+ @child = child
7
+ end
8
+
9
+ def visit(value)
10
+ value = @child.visit(value)
11
+ if Array === value
12
+ value.each_with_object([]) do |v, values|
13
+ if Array === v
14
+ values.concat(v)
15
+ else
16
+ values.push(v)
17
+ end
18
+ end
19
+ else
20
+ nil
21
+ end
22
+ end
23
+
24
+ def optimize
25
+ self.class.new(@child.optimize)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,591 @@
1
+ module JMESPath
2
+ # @api private
3
+ module Nodes
4
+ class Function < Node
5
+
6
+ FUNCTIONS = {}
7
+
8
+ def initialize(children)
9
+ @children = children
10
+ end
11
+
12
+ def self.create(name, children)
13
+ if (type = FUNCTIONS[name])
14
+ type.new(children)
15
+ else
16
+ raise Errors::UnknownFunctionError, "unknown function #{name}()"
17
+ end
18
+ end
19
+
20
+ def visit(value)
21
+ call(@children.map { |child| child.visit(value) })
22
+ end
23
+
24
+ def optimize
25
+ self.class.new(@children.map(&:optimize))
26
+ end
27
+
28
+ class FunctionName
29
+ attr_reader :name
30
+
31
+ def initialize(name)
32
+ @name = name
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def call(args)
39
+ nil
40
+ end
41
+ end
42
+
43
+ module TypeChecker
44
+ def get_type(value)
45
+ case value
46
+ when String then STRING_TYPE
47
+ when true, false then BOOLEAN_TYPE
48
+ when nil then NULL_TYPE
49
+ when Numeric then NUMBER_TYPE
50
+ when Hash, Struct then OBJECT_TYPE
51
+ when Array then ARRAY_TYPE
52
+ when Expression then EXPRESSION_TYPE
53
+ end
54
+ end
55
+
56
+ ARRAY_TYPE = 0
57
+ BOOLEAN_TYPE = 1
58
+ EXPRESSION_TYPE = 2
59
+ NULL_TYPE = 3
60
+ NUMBER_TYPE = 4
61
+ OBJECT_TYPE = 5
62
+ STRING_TYPE = 6
63
+
64
+ TYPE_NAMES = {
65
+ ARRAY_TYPE => 'array',
66
+ BOOLEAN_TYPE => 'boolean',
67
+ EXPRESSION_TYPE => 'expression',
68
+ NULL_TYPE => 'null',
69
+ NUMBER_TYPE => 'number',
70
+ OBJECT_TYPE => 'object',
71
+ STRING_TYPE => 'string',
72
+ }.freeze
73
+ end
74
+
75
+ class AbsFunction < Function
76
+ FUNCTIONS['abs'] = self
77
+
78
+ def call(args)
79
+ if args.count == 1
80
+ value = args.first
81
+ else
82
+ raise Errors::InvalidArityError, "function abs() expects one argument"
83
+ end
84
+ if Numeric === value
85
+ value.abs
86
+ else
87
+ raise Errors::InvalidTypeError, "function abs() expects a number"
88
+ end
89
+ end
90
+ end
91
+
92
+ class AvgFunction < Function
93
+ FUNCTIONS['avg'] = self
94
+
95
+ def call(args)
96
+ if args.count == 1
97
+ values = args.first
98
+ else
99
+ raise Errors::InvalidArityError, "function avg() expects one argument"
100
+ end
101
+ if Array === values
102
+ values.inject(0) do |total,n|
103
+ if Numeric === n
104
+ total + n
105
+ else
106
+ raise Errors::InvalidTypeError, "function avg() expects numeric values"
107
+ end
108
+ end / values.size.to_f
109
+ else
110
+ raise Errors::InvalidTypeError, "function avg() expects a number"
111
+ end
112
+ end
113
+ end
114
+
115
+ class CeilFunction < Function
116
+ FUNCTIONS['ceil'] = self
117
+
118
+ def call(args)
119
+ if args.count == 1
120
+ value = args.first
121
+ else
122
+ raise Errors::InvalidArityError, "function ceil() expects one argument"
123
+ end
124
+ if Numeric === value
125
+ value.ceil
126
+ else
127
+ raise Errors::InvalidTypeError, "function ceil() expects a numeric value"
128
+ end
129
+ end
130
+ end
131
+
132
+ class ContainsFunction < Function
133
+ FUNCTIONS['contains'] = self
134
+
135
+ def call(args)
136
+ if args.count == 2
137
+ haystack = args[0]
138
+ needle = args[1]
139
+ if String === haystack || Array === haystack
140
+ haystack.include?(needle)
141
+ else
142
+ raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list"
143
+ end
144
+ else
145
+ raise Errors::InvalidArityError, "function contains() expects 2 arguments"
146
+ end
147
+ end
148
+ end
149
+
150
+ class FloorFunction < Function
151
+ FUNCTIONS['floor'] = self
152
+
153
+ def call(args)
154
+ if args.count == 1
155
+ value = args.first
156
+ else
157
+ raise Errors::InvalidArityError, "function floor() expects one argument"
158
+ end
159
+ if Numeric === value
160
+ value.floor
161
+ else
162
+ raise Errors::InvalidTypeError, "function floor() expects a numeric value"
163
+ end
164
+ end
165
+ end
166
+
167
+ class LengthFunction < Function
168
+ FUNCTIONS['length'] = self
169
+
170
+ def call(args)
171
+ if args.count == 1
172
+ value = args.first
173
+ else
174
+ raise Errors::InvalidArityError, "function length() expects one argument"
175
+ end
176
+ case value
177
+ when Hash, Array, String then value.size
178
+ else raise Errors::InvalidTypeError, "function length() expects string, array or object"
179
+ end
180
+ end
181
+ end
182
+
183
+ class MaxFunction < Function
184
+ include TypeChecker
185
+
186
+ FUNCTIONS['max'] = self
187
+
188
+ def call(args)
189
+ if args.count == 1
190
+ values = args.first
191
+ else
192
+ raise Errors::InvalidArityError, "function max() expects one argument"
193
+ end
194
+ if Array === values
195
+ return nil if values.empty?
196
+ first = values.first
197
+ first_type = get_type(first)
198
+ unless first_type == NUMBER_TYPE || first_type == STRING_TYPE
199
+ msg = "function max() expects numeric or string values"
200
+ raise Errors::InvalidTypeError, msg
201
+ end
202
+ values.inject([first, first_type]) do |(max, max_type), v|
203
+ v_type = get_type(v)
204
+ if max_type == v_type
205
+ v > max ? [v, v_type] : [max, max_type]
206
+ else
207
+ msg = "function max() encountered a type mismatch in sequence: "
208
+ msg << "#{max_type}, #{v_type}"
209
+ raise Errors::InvalidTypeError, msg
210
+ end
211
+ end.first
212
+ else
213
+ raise Errors::InvalidTypeError, "function max() expects an array"
214
+ end
215
+ end
216
+ end
217
+
218
+ class MinFunction < Function
219
+ include TypeChecker
220
+
221
+ FUNCTIONS['min'] = self
222
+
223
+ def call(args)
224
+ if args.count == 1
225
+ values = args.first
226
+ else
227
+ raise Errors::InvalidArityError, "function min() expects one argument"
228
+ end
229
+ if Array === values
230
+ return nil if values.empty?
231
+ first = values.first
232
+ first_type = get_type(first)
233
+ unless first_type == NUMBER_TYPE || first_type == STRING_TYPE
234
+ msg = "function min() expects numeric or string values"
235
+ raise Errors::InvalidTypeError, msg
236
+ end
237
+ values.inject([first, first_type]) do |(min, min_type), v|
238
+ v_type = get_type(v)
239
+ if min_type == v_type
240
+ v < min ? [v, v_type] : [min, min_type]
241
+ else
242
+ msg = "function min() encountered a type mismatch in sequence: "
243
+ msg << "#{min_type}, #{v_type}"
244
+ raise Errors::InvalidTypeError, msg
245
+ end
246
+ end.first
247
+ else
248
+ raise Errors::InvalidTypeError, "function min() expects an array"
249
+ end
250
+ end
251
+ end
252
+
253
+ class TypeFunction < Function
254
+ include TypeChecker
255
+
256
+ FUNCTIONS['type'] = self
257
+
258
+ def call(args)
259
+ if args.count == 1
260
+ TYPE_NAMES[get_type(args.first)]
261
+ else
262
+ raise Errors::InvalidArityError, "function type() expects one argument"
263
+ end
264
+ end
265
+ end
266
+
267
+ class KeysFunction < Function
268
+ FUNCTIONS['keys'] = self
269
+
270
+ def call(args)
271
+ if args.count == 1
272
+ value = args.first
273
+ if hash_like?(value)
274
+ case value
275
+ when Hash then value.keys.map(&:to_s)
276
+ when Struct then value.members.map(&:to_s)
277
+ else raise NotImplementedError
278
+ end
279
+ else
280
+ raise Errors::InvalidTypeError, "function keys() expects a hash"
281
+ end
282
+ else
283
+ raise Errors::InvalidArityError, "function keys() expects one argument"
284
+ end
285
+ end
286
+ end
287
+
288
+ class ValuesFunction < Function
289
+ FUNCTIONS['values'] = self
290
+
291
+ def call(args)
292
+ if args.count == 1
293
+ value = args.first
294
+ if hash_like?(value)
295
+ value.values
296
+ elsif Array === value
297
+ value
298
+ else
299
+ raise Errors::InvalidTypeError, "function values() expects an array or a hash"
300
+ end
301
+ else
302
+ raise Errors::InvalidArityError, "function values() expects one argument"
303
+ end
304
+ end
305
+ end
306
+
307
+ class JoinFunction < Function
308
+ FUNCTIONS['join'] = self
309
+
310
+ def call(args)
311
+ if args.count == 2
312
+ glue = args[0]
313
+ values = args[1]
314
+ if !(String === glue)
315
+ raise Errors::InvalidTypeError, "function join() expects the first argument to be a string"
316
+ elsif Array === values && values.all? { |v| String === v }
317
+ values.join(glue)
318
+ else
319
+ raise Errors::InvalidTypeError, "function join() expects values to be an array of strings"
320
+ end
321
+ else
322
+ raise Errors::InvalidArityError, "function join() expects an array of strings"
323
+ end
324
+ end
325
+ end
326
+
327
+ class ToStringFunction < Function
328
+ FUNCTIONS['to_string'] = self
329
+
330
+ def call(args)
331
+ if args.count == 1
332
+ value = args.first
333
+ String === value ? value : JSON.dump(value)
334
+ else
335
+ raise Errors::InvalidArityError, "function to_string() expects one argument"
336
+ end
337
+ end
338
+ end
339
+
340
+ class ToNumberFunction < Function
341
+ FUNCTIONS['to_number'] = self
342
+
343
+ def call(args)
344
+ if args.count == 1
345
+ begin
346
+ value = Float(args.first)
347
+ Integer(value) === value ? value.to_i : value
348
+ rescue
349
+ nil
350
+ end
351
+ else
352
+ raise Errors::InvalidArityError, "function to_number() expects one argument"
353
+ end
354
+ end
355
+ end
356
+
357
+ class SumFunction < Function
358
+ FUNCTIONS['sum'] = self
359
+
360
+ def call(args)
361
+ if args.count == 1 && Array === args.first
362
+ args.first.inject(0) do |sum,n|
363
+ if Numeric === n
364
+ sum + n
365
+ else
366
+ raise Errors::InvalidTypeError, "function sum() expects values to be numeric"
367
+ end
368
+ end
369
+ else
370
+ raise Errors::InvalidArityError, "function sum() expects one argument"
371
+ end
372
+ end
373
+ end
374
+
375
+ class NotNullFunction < Function
376
+ FUNCTIONS['not_null'] = self
377
+
378
+ def call(args)
379
+ if args.count > 0
380
+ args.find { |value| !value.nil? }
381
+ else
382
+ raise Errors::InvalidArityError, "function not_null() expects one or more arguments"
383
+ end
384
+ end
385
+ end
386
+
387
+ class SortFunction < Function
388
+ include TypeChecker
389
+
390
+ FUNCTIONS['sort'] = self
391
+
392
+ def call(args)
393
+ if args.count == 1
394
+ value = args.first
395
+ if Array === value
396
+ value.sort do |a, b|
397
+ a_type = get_type(a)
398
+ b_type = get_type(b)
399
+ if (a_type == STRING_TYPE || a_type == NUMBER_TYPE) && a_type == b_type
400
+ a <=> b
401
+ else
402
+ raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers"
403
+ end
404
+ end
405
+ else
406
+ raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers"
407
+ end
408
+ else
409
+ raise Errors::InvalidArityError, "function sort() expects one argument"
410
+ end
411
+ end
412
+ end
413
+
414
+ class SortByFunction < Function
415
+ include TypeChecker
416
+
417
+ FUNCTIONS['sort_by'] = self
418
+
419
+ def call(args)
420
+ if args.count == 2
421
+ if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE
422
+ values = args[0]
423
+ expression = args[1]
424
+ values.sort do |a,b|
425
+ a_value = expression.eval(a)
426
+ b_value = expression.eval(b)
427
+ a_type = get_type(a_value)
428
+ b_type = get_type(b_value)
429
+ if (a_type == STRING_TYPE || a_type == NUMBER_TYPE) && a_type == b_type
430
+ a_value <=> b_value
431
+ else
432
+ raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers"
433
+ end
434
+ end
435
+ else
436
+ raise Errors::InvalidTypeError, "function sort_by() expects an array and an expression"
437
+ end
438
+ else
439
+ raise Errors::InvalidArityError, "function sort_by() expects two arguments"
440
+ end
441
+ end
442
+ end
443
+
444
+ module CompareBy
445
+ include TypeChecker
446
+
447
+ def compare_by(mode, *args)
448
+ if args.count == 2
449
+ values = args[0]
450
+ expression = args[1]
451
+ if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE
452
+ type = get_type(expression.eval(values.first))
453
+ if type != NUMBER_TYPE && type != STRING_TYPE
454
+ msg = "function #{mode}() expects values to be strings or numbers"
455
+ raise Errors::InvalidTypeError, msg
456
+ end
457
+ values.send(mode) do |entry|
458
+ value = expression.eval(entry)
459
+ value_type = get_type(value)
460
+ if value_type != type
461
+ msg = "function #{mode}() encountered a type mismatch in "
462
+ msg << "sequence: #{type}, #{value_type}"
463
+ raise Errors::InvalidTypeError, msg
464
+ end
465
+ value
466
+ end
467
+ else
468
+ msg = "function #{mode}() expects an array and an expression"
469
+ raise Errors::InvalidTypeError, msg
470
+ end
471
+ else
472
+ msg = "function #{mode}() expects two arguments"
473
+ raise Errors::InvalidArityError, msg
474
+ end
475
+ end
476
+ end
477
+
478
+ class MaxByFunction < Function
479
+ include CompareBy
480
+
481
+ FUNCTIONS['max_by'] = self
482
+
483
+ def call(args)
484
+ compare_by(:max_by, *args)
485
+ end
486
+ end
487
+
488
+ class MinByFunction < Function
489
+ include CompareBy
490
+
491
+ FUNCTIONS['min_by'] = self
492
+
493
+ def call(args)
494
+ compare_by(:min_by, *args)
495
+ end
496
+ end
497
+
498
+ class EndsWithFunction < Function
499
+ include TypeChecker
500
+
501
+ FUNCTIONS['ends_with'] = self
502
+
503
+ def call(args)
504
+ if args.count == 2
505
+ search, suffix = args
506
+ search_type = get_type(search)
507
+ suffix_type = get_type(suffix)
508
+ if search_type != STRING_TYPE
509
+ msg = "function ends_with() expects first argument to be a string"
510
+ raise Errors::InvalidTypeError, msg
511
+ end
512
+ if suffix_type != STRING_TYPE
513
+ msg = "function ends_with() expects second argument to be a string"
514
+ raise Errors::InvalidTypeError, msg
515
+ end
516
+ search.end_with?(suffix)
517
+ else
518
+ msg = "function ends_with() expects two arguments"
519
+ raise Errors::InvalidArityError, msg
520
+ end
521
+ end
522
+ end
523
+
524
+ class StartsWithFunction < Function
525
+ include TypeChecker
526
+
527
+ FUNCTIONS['starts_with'] = self
528
+
529
+ def call(args)
530
+ if args.count == 2
531
+ search, prefix = args
532
+ search_type = get_type(search)
533
+ prefix_type = get_type(prefix)
534
+ if search_type != STRING_TYPE
535
+ msg = "function starts_with() expects first argument to be a string"
536
+ raise Errors::InvalidTypeError, msg
537
+ end
538
+ if prefix_type != STRING_TYPE
539
+ msg = "function starts_with() expects second argument to be a string"
540
+ raise Errors::InvalidTypeError, msg
541
+ end
542
+ search.start_with?(prefix)
543
+ else
544
+ msg = "function starts_with() expects two arguments"
545
+ raise Errors::InvalidArityError, msg
546
+ end
547
+ end
548
+ end
549
+
550
+ class MergeFunction < Function
551
+ FUNCTIONS['merge'] = self
552
+
553
+ def call(args)
554
+ if args.count == 0
555
+ msg = "function merge() expects 1 or more arguments"
556
+ raise Errors::InvalidArityError, msg
557
+ end
558
+ args.inject({}) do |h, v|
559
+ h.merge(v)
560
+ end
561
+ end
562
+ end
563
+
564
+ class ReverseFunction < Function
565
+ FUNCTIONS['reverse'] = self
566
+
567
+ def call(args)
568
+ if args.count == 0
569
+ msg = "function reverse() expects 1 or more arguments"
570
+ raise Errors::InvalidArityError, msg
571
+ end
572
+ value = args.first
573
+ if Array === value || String === value
574
+ value.reverse
575
+ else
576
+ msg = "function reverse() expects an array or string"
577
+ raise Errors::InvalidTypeError, msg
578
+ end
579
+ end
580
+ end
581
+
582
+ class ToArrayFunction < Function
583
+ FUNCTIONS['to_array'] = self
584
+
585
+ def call(args)
586
+ value = args.first
587
+ Array === value ? value : [value]
588
+ end
589
+ end
590
+ end
591
+ end