json_p3 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "function"
5
+
6
+ module JSONP3 # rubocop:disable Style/Documentation
7
+ # Base class for all filter expression nodes.
8
+ class Expression
9
+ # @dynamic token
10
+ attr_reader :token
11
+
12
+ def initialize(token)
13
+ @token = token
14
+ end
15
+
16
+ # Evaluate the filter expression in the given context.
17
+ def evaluate(_context)
18
+ raise "filter expressions must implement `evaluate(context)`"
19
+ end
20
+ end
21
+
22
+ # An expression that evaluates to true or false.
23
+ class FilterExpression < Expression
24
+ attr_reader :expression
25
+
26
+ def initialize(token, expression)
27
+ super(token)
28
+ @expression = expression
29
+ end
30
+
31
+ def evaluate(context)
32
+ JSONP3.truthy?(@expression.evaluate(context))
33
+ end
34
+
35
+ def to_s
36
+ @expression.to_s
37
+ end
38
+
39
+ def ==(other)
40
+ self.class == other.class &&
41
+ @expression == other.expression &&
42
+ @token == other.token
43
+ end
44
+
45
+ alias eql? ==
46
+
47
+ def hash
48
+ [@expression, @token].hash
49
+ end
50
+ end
51
+
52
+ # Base class for expression literals.
53
+ class FilterExpressionLiteral < Expression
54
+ attr_reader :value
55
+
56
+ def initialize(token, value)
57
+ super(token)
58
+ @value = value
59
+ end
60
+
61
+ def evaluate(_context)
62
+ @value
63
+ end
64
+
65
+ def to_s
66
+ @value.to_s
67
+ end
68
+
69
+ def ==(other)
70
+ self.class == other.class &&
71
+ @value == other.value &&
72
+ @token == other.token
73
+ end
74
+
75
+ alias eql? ==
76
+
77
+ def hash
78
+ [@value, @token].hash
79
+ end
80
+ end
81
+
82
+ # Literal true or false.
83
+ class BooleanLiteral < FilterExpressionLiteral; end
84
+
85
+ # A double or single quoted string literal.
86
+ class StringLiteral < FilterExpressionLiteral
87
+ def to_s
88
+ JSON.generate(@value)
89
+ end
90
+ end
91
+
92
+ # A literal integer.
93
+ class IntegerLiteral < FilterExpressionLiteral; end
94
+
95
+ # A literal float
96
+ class FloatLiteral < FilterExpressionLiteral; end
97
+
98
+ # A literal null
99
+ class NullLiteral < FilterExpressionLiteral
100
+ def to_s
101
+ "null"
102
+ end
103
+ end
104
+
105
+ # An expression prefixed with the logical not operator.
106
+ class LogicalNotExpression < Expression
107
+ attr_reader :expression
108
+
109
+ def initialize(token, expression)
110
+ super(token)
111
+ @expression = expression
112
+ end
113
+
114
+ def evaluate(context)
115
+ !JSONP3.truthy?(@expression.evaluate(context))
116
+ end
117
+
118
+ def to_s
119
+ "!#{@expression}"
120
+ end
121
+
122
+ def ==(other)
123
+ self.class == other.class &&
124
+ @expression == other.expression &&
125
+ @token == other.token
126
+ end
127
+
128
+ alias eql? ==
129
+
130
+ def hash
131
+ [@expression, @token].hash
132
+ end
133
+ end
134
+
135
+ # Base class for expression with a left expression, operator and right expression.
136
+ class InfixExpression < Expression
137
+ attr_reader :left, :right
138
+
139
+ def initialize(token, left, right)
140
+ super(token)
141
+ @left = left
142
+ @right = right
143
+ end
144
+
145
+ def evaluate(_context)
146
+ raise "infix expressions must implement `evaluate(context)`"
147
+ end
148
+
149
+ def to_s
150
+ raise "infix expressions must implement `to_s`"
151
+ end
152
+
153
+ def ==(other)
154
+ self.class == other.class &&
155
+ @left == other.left &&
156
+ @right == other.right &&
157
+ @token == other.token
158
+ end
159
+
160
+ alias eql? ==
161
+
162
+ def hash
163
+ [@left, @right, @token].hash
164
+ end
165
+ end
166
+
167
+ # A logical `&&` expression.
168
+ class LogicalAndExpression < InfixExpression
169
+ def evaluate(context)
170
+ JSONP3.truthy?(@left.evaluate(context)) && JSONP3.truthy?(@right.evaluate(context))
171
+ end
172
+
173
+ def to_s
174
+ "#{@left} && #{@right}"
175
+ end
176
+ end
177
+
178
+ # A logical `||` expression.
179
+ class LogicalOrExpression < InfixExpression
180
+ def evaluate(context)
181
+ JSONP3.truthy?(@left.evaluate(context)) || JSONP3.truthy?(@right.evaluate(context))
182
+ end
183
+
184
+ def to_s
185
+ "#{@left} || #{@right}"
186
+ end
187
+ end
188
+
189
+ # An `==` expression.
190
+ class EqExpression < InfixExpression
191
+ def evaluate(context)
192
+ JSONP3.eq?(@left.evaluate(context), @right.evaluate(context))
193
+ end
194
+
195
+ def to_s
196
+ "#{@left} == #{@right}"
197
+ end
198
+ end
199
+
200
+ # A `!=` expression.
201
+ class NeExpression < InfixExpression
202
+ def evaluate(context)
203
+ !JSONP3.eq?(@left.evaluate(context), @right.evaluate(context))
204
+ end
205
+
206
+ def to_s
207
+ "#{@left} != #{@right}"
208
+ end
209
+ end
210
+
211
+ # A `<=` expression.
212
+ class LeExpression < InfixExpression
213
+ def evaluate(context)
214
+ left = @left.evaluate(context)
215
+ right = @right.evaluate(context)
216
+ JSONP3.eq?(left, right) || JSONP3.lt?(left, right)
217
+ end
218
+
219
+ def to_s
220
+ "#{@left} <= #{@right}"
221
+ end
222
+ end
223
+
224
+ # A `>=` expression.
225
+ class GeExpression < InfixExpression
226
+ def evaluate(context)
227
+ left = @left.evaluate(context)
228
+ right = @right.evaluate(context)
229
+ JSONP3.eq?(left, right) || JSONP3.lt?(right, left)
230
+ end
231
+
232
+ def to_s
233
+ "#{@left} >= #{@right}"
234
+ end
235
+ end
236
+
237
+ # A `<` expression.
238
+ class LtExpression < InfixExpression
239
+ def evaluate(context)
240
+ JSONP3.lt?(@left.evaluate(context), @right.evaluate(context))
241
+ end
242
+
243
+ def to_s
244
+ "#{@left} < #{@right}"
245
+ end
246
+ end
247
+
248
+ # A `>` expression.
249
+ class GtExpression < InfixExpression
250
+ def evaluate(context)
251
+ JSONP3.lt?(@right.evaluate(context), @left.evaluate(context))
252
+ end
253
+
254
+ def to_s
255
+ "#{@left} > #{@right}"
256
+ end
257
+ end
258
+
259
+ # Base class for all embedded filter queries
260
+ class QueryExpression < Expression
261
+ attr_reader :query
262
+
263
+ def initialize(token, query)
264
+ super(token)
265
+ @query = query
266
+ end
267
+
268
+ def evaluate(_context)
269
+ raise "query expressions must implement `evaluate(context)`"
270
+ end
271
+
272
+ def to_s
273
+ raise "query expressions must implement `to_s`"
274
+ end
275
+
276
+ def ==(other)
277
+ self.class == other.class &&
278
+ @query == other.query &&
279
+ @token == other.token
280
+ end
281
+
282
+ alias eql? ==
283
+
284
+ def hash
285
+ [@query, @token].hash
286
+ end
287
+ end
288
+
289
+ # An embedded query starting at the current node.
290
+ class RelativeQueryExpression < QueryExpression
291
+ def evaluate(context)
292
+ unless context.current.is_a?(Array) || context.current.is_a?(Hash)
293
+ return @query.empty? ? context.current : JSONPathNodeList.new
294
+ end
295
+
296
+ @query.find(context.current)
297
+ end
298
+
299
+ def to_s
300
+ "@#{@query.to_s[1..]}"
301
+ end
302
+ end
303
+
304
+ # An embedded query starting at the root node.
305
+ class RootQueryExpression < QueryExpression
306
+ def evaluate(context)
307
+ @query.find(context.root)
308
+ end
309
+
310
+ def to_s
311
+ @query.to_s
312
+ end
313
+ end
314
+
315
+ # A filter function call.
316
+ class FunctionExpression < Expression
317
+ attr_reader :name, :args
318
+
319
+ # @param name [String]
320
+ # @param args [Array<Expression>]
321
+ def initialize(token, name, args)
322
+ super(token)
323
+ @name = name
324
+ @args = args
325
+ end
326
+
327
+ def evaluate(context)
328
+ func = context.env.function_extensions.fetch(@name)
329
+ args = @args.map { |arg| arg.evaluate(context) }
330
+ unpacked_args = unpack_node_lists(func, args)
331
+ func.call(*unpacked_args)
332
+ rescue KeyError
333
+ :nothing
334
+ end
335
+
336
+ def to_s
337
+ args = @args.map(&:to_s).join(", ")
338
+ "#{@name}(#{args})"
339
+ end
340
+
341
+ def ==(other)
342
+ self.class == other.class &&
343
+ @name == other.name &&
344
+ @args == other.args &&
345
+ @token == other.token
346
+ end
347
+
348
+ alias eql? ==
349
+
350
+ def hash
351
+ [@name, @args, @token].hash
352
+ end
353
+
354
+ private
355
+
356
+ # @param func [Proc]
357
+ # @param args [Array<Object>]
358
+ # @return [Array<Object>]
359
+ def unpack_node_lists(func, args) # rubocop:disable Metrics/MethodLength
360
+ unpacked_args = []
361
+ args.each_with_index do |arg, i|
362
+ unless arg.is_a?(JSONPathNodeList) && func.class::ARG_TYPES[i] != ExpressionType::NODES
363
+ unpacked_args << arg
364
+ next
365
+ end
366
+
367
+ unpacked_args << case arg.length
368
+ when 0
369
+ :nothing
370
+ when 1
371
+ arg.first.value
372
+ else
373
+ arg
374
+ end
375
+ end
376
+ unpacked_args
377
+ end
378
+ end
379
+
380
+ def self.truthy?(obj)
381
+ return !obj.empty? if obj.is_a?(JSONPathNodeList)
382
+ return false if obj == :nothing
383
+
384
+ obj != false
385
+ end
386
+
387
+ def self.eq?(left, right) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
388
+ left = left.first.value if left.is_a?(JSONPathNodeList) && left.length == 1
389
+ right = right.first.value if right.is_a?(JSONPathNodeList) && right.length == 1
390
+
391
+ right, left = left, right if right.is_a?(JSONPathNodeList)
392
+
393
+ if left.is_a? JSONPathNodeList
394
+ return left == right if right.is_a? JSONPathNodeList
395
+ return right == :nothing if left.empty?
396
+ return left.first == right if left.length == 1
397
+
398
+ return false
399
+ end
400
+
401
+ return true if left == :nothing && right == :nothing
402
+
403
+ left == right
404
+ end
405
+
406
+ def self.lt?(left, right) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
407
+ left = left.first.value if left.is_a?(JSONPathNodeList) && left.length == 1
408
+ right = right.first.value if right.is_a?(JSONPathNodeList) && right.length == 1
409
+ return left < right if left.is_a?(String) && right.is_a?(String)
410
+ return left < right if (left.is_a?(Integer) || left.is_a?(Float)) &&
411
+ (right.is_a?(Integer) || right.is_a?(Float))
412
+
413
+ false
414
+ end
415
+
416
+ # Contextual information and data used for evaluating a filter expression.
417
+ class FilterContext
418
+ attr_reader :env, :current, :root
419
+
420
+ def initialize(env, current, root)
421
+ @env = env
422
+ @current = current
423
+ @root = root
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONP3
4
+ class ExpressionType
5
+ VALUE = :value_expression
6
+ LOGICAL = :logical_expression
7
+ NODES = :nodes_expression
8
+ end
9
+
10
+ # Base class for all filter functions.
11
+ class FunctionExtension
12
+ def call(*_args, **_kwargs)
13
+ raise "function extensions must implement `call()`"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../function"
4
+
5
+ module JSONP3
6
+ # The standard `count` function.
7
+ class Count < FunctionExtension
8
+ ARG_TYPES = [ExpressionType::NODES].freeze
9
+ RETURN_TYPE = ExpressionType::VALUE
10
+
11
+ def call(node_list)
12
+ node_list.length
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../function"
4
+
5
+ module JSONP3
6
+ # The standard `length` function.
7
+ class Length < FunctionExtension
8
+ ARG_TYPES = [ExpressionType::VALUE].freeze
9
+ RETURN_TYPE = ExpressionType::VALUE
10
+
11
+ def call(obj)
12
+ return :nothing unless obj.is_a?(Array) || obj.is_a?(Hash) || obj.is_a?(String)
13
+
14
+ obj.length
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache"
4
+ require_relative "../function"
5
+ require_relative "pattern"
6
+
7
+ module JSONP3
8
+ # The standard `match` function.
9
+ class Match < FunctionExtension
10
+ ARG_TYPES = [ExpressionType::VALUE, ExpressionType::VALUE].freeze
11
+ RETURN_TYPE = ExpressionType::LOGICAL
12
+
13
+ # @param cache_size [Integer] the maximum size of the regexp cache. Set it to
14
+ # zero or negative to disable the cache.
15
+ # @param raise_errors [Boolean] if _false_ (the default), return _false_ when this
16
+ # function causes a RegexpError instead of raising the exception.
17
+ def initialize(cache_size = 128, raise_errors: false)
18
+ super()
19
+ @cache_size = cache_size
20
+ @raise_errors = raise_errors
21
+ @cache = LRUCache.new(cache_size)
22
+ end
23
+
24
+ # @param value [String]
25
+ # @param pattern [String]
26
+ # @return Boolean
27
+ def call(value, pattern) # rubocop:disable Metrics/MethodLength
28
+ return false unless pattern.is_a?(String) && value.is_a?(String)
29
+
30
+ if @cache_size.positive?
31
+ re = @cache[pattern] || Regexp.new(full_match(pattern))
32
+ else
33
+ re = Regexp.new(full_match(pattern))
34
+ @cache[pattern] = re
35
+ end
36
+
37
+ re.match?(value)
38
+ rescue RegexpError
39
+ raise if @raise_errors
40
+
41
+ false
42
+ end
43
+
44
+ private
45
+
46
+ def full_match(pattern)
47
+ parts = []
48
+ explicit_caret = pattern.start_with?("^")
49
+ explicit_dollar = pattern.end_with?("$")
50
+
51
+ # Replace '^' with '\A' and '$' with '\z'
52
+ pattern = pattern.sub("^", "\\A") if explicit_caret
53
+ pattern = "#{pattern[..-1]}\\z" if explicit_dollar
54
+
55
+ # Wrap with '\A' and '\z' if they are not already part of the pattern.
56
+ parts << "\\A(?:" if !explicit_caret && !explicit_dollar
57
+ parts << JSONP3.map_iregexp(pattern)
58
+ parts << ")\\z" if !explicit_caret && !explicit_dollar
59
+ parts.join
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONP3 # rubocop:disable Style/Documentation
4
+ # Map I-Regexp pattern to Ruby regex pattern.
5
+ # @param pattern [String]
6
+ # @return [String]
7
+ def self.map_iregexp(pattern) # rubocop:disable Metrics/MethodLength
8
+ escaped = false
9
+ char_class = false
10
+ mapped = String.new(encoding: "UTF-8")
11
+
12
+ pattern.each_char do |c|
13
+ if escaped
14
+ mapped << c
15
+ escaped = false
16
+ next
17
+ end
18
+
19
+ case c
20
+ when "."
21
+ # mapped << (char_class ? c : "(?:(?![\\r\\n])\\P{Cs}|\\p{Cs}\\p{Cs})")
22
+ mapped << (char_class ? c : "[^\\n\\r]")
23
+ when "\\"
24
+ escaped = true
25
+ mapped << "\\"
26
+ when "["
27
+ char_class = true
28
+ mapped << "["
29
+ when "]"
30
+ char_class = false
31
+ mapped << "]"
32
+ else
33
+ mapped << c
34
+ end
35
+ end
36
+
37
+ mapped
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cache"
4
+ require_relative "../function"
5
+ require_relative "pattern"
6
+
7
+ module JSONP3
8
+ # The standard `search` function.
9
+ class Search < FunctionExtension
10
+ ARG_TYPES = [ExpressionType::VALUE, ExpressionType::VALUE].freeze
11
+ RETURN_TYPE = ExpressionType::LOGICAL
12
+
13
+ # @param cache_size [Integer] the maximum size of the regexp cache. Set it to
14
+ # zero or negative to disable the cache.
15
+ # @param raise_errors [Boolean] if _false_ (the default), return _false_ when this
16
+ # function causes a RegexpError instead of raising the exception.
17
+ def initialize(cache_size = 128, raise_errors: false)
18
+ super()
19
+ @cache_size = cache_size
20
+ @raise_errors = raise_errors
21
+ @cache = LRUCache.new(cache_size)
22
+ end
23
+
24
+ # @param value [String]
25
+ # @param pattern [String]
26
+ # @return Boolean
27
+ def call(value, pattern) # rubocop:disable Metrics/MethodLength
28
+ return false unless pattern.is_a?(String) && value.is_a?(String)
29
+
30
+ if @cache_size.positive?
31
+ re = @cache[pattern] || Regexp.new(JSONP3.map_iregexp(pattern))
32
+ else
33
+ re = Regexp.new(JSONP3.map_iregexp(pattern))
34
+ @cache[pattern] = re
35
+ end
36
+
37
+ re.match?(value)
38
+ rescue RegexpError
39
+ raise if @raise_errors
40
+
41
+ false
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../function"
4
+
5
+ module JSONP3
6
+ # The standard `value` function.
7
+ class Value < FunctionExtension
8
+ ARG_TYPES = [ExpressionType::NODES].freeze
9
+ RETURN_TYPE = ExpressionType::VALUE
10
+
11
+ def call(node_list)
12
+ node_list.length == 1 ? node_list.first.value : :nothing
13
+ end
14
+ end
15
+ end