json_p3 0.2.1

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.
@@ -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