jsonata 2.2.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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_local.yml +52 -0
- data/AGENTS.md +171 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +189 -0
- data/Rakefile +41 -0
- data/lib/jsonata/datetime.rb +1237 -0
- data/lib/jsonata/error.rb +113 -0
- data/lib/jsonata/evaluator.rb +1669 -0
- data/lib/jsonata/functions.rb +1505 -0
- data/lib/jsonata/parser.rb +1206 -0
- data/lib/jsonata/signature.rb +235 -0
- data/lib/jsonata/utils.rb +600 -0
- data/lib/jsonata/version.rb +5 -0
- data/lib/jsonata.rb +47 -0
- metadata +108 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2016, 2018 All Rights Reserved
|
|
4
|
+
# Project name: JSONata
|
|
5
|
+
# This project is licensed under the MIT License, see LICENSE
|
|
6
|
+
|
|
7
|
+
# Port of jsonata-js src/utils.js
|
|
8
|
+
#
|
|
9
|
+
# In addition to the functions in utils.js, this file contains the helpers that
|
|
10
|
+
# emulate JavaScript semantics that the rest of the port depends on:
|
|
11
|
+
#
|
|
12
|
+
# * `nil` represents JavaScript `undefined`, and `Jsonata::NULL` represents JSON `null`.
|
|
13
|
+
# * `Jsonata::JArray` is an Array that can carry the extra properties that
|
|
14
|
+
# jsonata-js attaches to arrays (sequence, keepSingleton, tupleStream, cons, outerWrapper).
|
|
15
|
+
# * JSON objects are Hashes with String keys.
|
|
16
|
+
|
|
17
|
+
module Jsonata
|
|
18
|
+
# Represents the JSON value `null` (as distinct from `nil`, which represents `undefined`)
|
|
19
|
+
NULL = Object.new
|
|
20
|
+
class << NULL
|
|
21
|
+
def inspect = 'Jsonata::NULL'
|
|
22
|
+
def to_s = 'null'
|
|
23
|
+
def to_json(*) = 'null'
|
|
24
|
+
end
|
|
25
|
+
NULL.freeze
|
|
26
|
+
|
|
27
|
+
# An Array that carries the additional properties that jsonata-js sets on arrays
|
|
28
|
+
class JArray < Array
|
|
29
|
+
attr_accessor :sequence, :keep_singleton, :tuple_stream, :cons, :outer_wrapper, :max_length
|
|
30
|
+
|
|
31
|
+
def push(*items)
|
|
32
|
+
if max_length && length + items.length > max_length
|
|
33
|
+
raise Jsonata::Error.new('D2015', :value => max_length)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
alias << push
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module Utils
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Check if value is a finite number
|
|
45
|
+
# @param n - number to evaluate
|
|
46
|
+
# @returns true if n is a finite number
|
|
47
|
+
def numeric?(n)
|
|
48
|
+
is_num = false
|
|
49
|
+
if n.kind_of?(Numeric)
|
|
50
|
+
is_num = !(n.kind_of?(Float) && n.nan?)
|
|
51
|
+
if is_num && n.kind_of?(Float) && n.infinite?
|
|
52
|
+
raise Jsonata::Error.new('D1001', :value => n)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
is_num
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns true if the arg is an array of strings
|
|
59
|
+
def array_of_strings?(arg)
|
|
60
|
+
result = false
|
|
61
|
+
if arg.kind_of?(Array)
|
|
62
|
+
result = arg.all?(String)
|
|
63
|
+
end
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns true if the arg is an array of numbers
|
|
68
|
+
def array_of_numbers?(arg)
|
|
69
|
+
result = false
|
|
70
|
+
if arg.kind_of?(Array)
|
|
71
|
+
result = arg.all? { |item| numeric?(item) }
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Tests if a value is a sequence
|
|
77
|
+
def sequence?(value)
|
|
78
|
+
value.kind_of?(JArray) && value.sequence == true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @returns true if it is a function (lambda or built-in)
|
|
82
|
+
def function?(arg)
|
|
83
|
+
arg.kind_of?(Lambda) || arg.kind_of?(NativeFunction) || callable?(arg)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Equivalent of JavaScript `typeof arg === 'function'`
|
|
87
|
+
def callable?(arg)
|
|
88
|
+
arg.kind_of?(Proc) || arg.kind_of?(Method) || arg.kind_of?(Closure)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the arity (number of arguments) of the function
|
|
92
|
+
def get_function_arity(func)
|
|
93
|
+
case func
|
|
94
|
+
when Closure then func.arity
|
|
95
|
+
when NativeFunction then parameter_arity(func.implementation)
|
|
96
|
+
when Lambda then func.arguments.length
|
|
97
|
+
else parameter_arity(func)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Equivalent of JavaScript Function.length - the number of declared parameters
|
|
102
|
+
# before the first one with a default value
|
|
103
|
+
def parameter_arity(callable)
|
|
104
|
+
callable.parameters.take_while { |type, _| type == :req }.length
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Names of the declared parameters of a Ruby callable (used for partial application of native functions)
|
|
108
|
+
NON_POSITIONAL_PARAMETER_TYPES = %i[block keyrest key keyreq].freeze
|
|
109
|
+
|
|
110
|
+
def parameter_names(callable)
|
|
111
|
+
callable.parameters.filter_map { |type, name| name.to_s unless NON_POSITIONAL_PARAMETER_TYPES.include?(type) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Invokes a Ruby callable with JavaScript calling semantics - missing arguments are
|
|
115
|
+
# passed as undefined (nil), and extraneous arguments are dropped.
|
|
116
|
+
# If `receiver` is given and the callable is an UnboundMethod, it is bound to the receiver
|
|
117
|
+
# (the equivalent of Function.prototype.apply(this, args))
|
|
118
|
+
def invoke(callable, args, receiver = nil)
|
|
119
|
+
required, max = invoke_arity(callable)
|
|
120
|
+
unless max.nil?
|
|
121
|
+
args = args.take(max) if args.length > max
|
|
122
|
+
args += Array.new(required - args.length) if args.length < required
|
|
123
|
+
end
|
|
124
|
+
if callable.kind_of?(UnboundMethod)
|
|
125
|
+
callable.bind_call(receiver, *args)
|
|
126
|
+
else
|
|
127
|
+
callable.call(*args)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
INVOKE_ARITY_CACHE = ObjectSpace::WeakMap.new
|
|
132
|
+
|
|
133
|
+
# The number of required parameters, and the maximum number of arguments (nil if unlimited) of a callable
|
|
134
|
+
def invoke_arity(callable)
|
|
135
|
+
cached = INVOKE_ARITY_CACHE[callable]
|
|
136
|
+
return cached if cached
|
|
137
|
+
|
|
138
|
+
params = callable.parameters
|
|
139
|
+
arity = if params.any? { |type, _| type == :rest }
|
|
140
|
+
[0, nil]
|
|
141
|
+
else
|
|
142
|
+
required = params.count { |type, _| type == :req }
|
|
143
|
+
[required, required + params.count { |type, _| type == :opt }]
|
|
144
|
+
end
|
|
145
|
+
INVOKE_ARITY_CACHE[callable] = arity.freeze
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Tests whether arg is a lambda function
|
|
149
|
+
def lambda?(arg)
|
|
150
|
+
arg.kind_of?(Lambda)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Compares two values for equality
|
|
154
|
+
# @returns true if they are deep equal
|
|
155
|
+
def deep_equal?(lhs, rhs)
|
|
156
|
+
return true if lhs.equal?(rhs)
|
|
157
|
+
|
|
158
|
+
if lhs.kind_of?(Array) && rhs.kind_of?(Array)
|
|
159
|
+
# both arrays (or sequences)
|
|
160
|
+
# must be the same length
|
|
161
|
+
return false if lhs.length != rhs.length
|
|
162
|
+
|
|
163
|
+
# must contain same values in same order
|
|
164
|
+
lhs.each_index do |ii|
|
|
165
|
+
return false unless deep_equal?(lhs[ii], rhs[ii])
|
|
166
|
+
end
|
|
167
|
+
return true
|
|
168
|
+
end
|
|
169
|
+
if lhs.kind_of?(Hash) && rhs.kind_of?(Hash)
|
|
170
|
+
# both objects
|
|
171
|
+
# must have the same set of keys (in any order)
|
|
172
|
+
return false if lhs.length != rhs.length
|
|
173
|
+
|
|
174
|
+
lkeys = lhs.keys.sort
|
|
175
|
+
rkeys = rhs.keys.sort
|
|
176
|
+
return false if lkeys != rkeys
|
|
177
|
+
|
|
178
|
+
# must have the same values
|
|
179
|
+
lkeys.each do |key|
|
|
180
|
+
return false unless deep_equal?(lhs[key], rhs[key])
|
|
181
|
+
end
|
|
182
|
+
return true
|
|
183
|
+
end
|
|
184
|
+
return false if lhs.kind_of?(Array) || lhs.kind_of?(Hash) || rhs.kind_of?(Array) || rhs.kind_of?(Hash)
|
|
185
|
+
|
|
186
|
+
strict_equal?(lhs, rhs)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Equivalent of the JavaScript `===` operator
|
|
190
|
+
def strict_equal?(lhs, rhs)
|
|
191
|
+
if lhs.kind_of?(Numeric) && rhs.kind_of?(Numeric)
|
|
192
|
+
lhs == rhs
|
|
193
|
+
elsif lhs.kind_of?(String) && rhs.kind_of?(String)
|
|
194
|
+
lhs == rhs
|
|
195
|
+
else
|
|
196
|
+
lhs.equal?(rhs)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# converts a string to an array of characters
|
|
201
|
+
def string_to_array(str)
|
|
202
|
+
str.chars
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @returns the keys (property names) of the object
|
|
206
|
+
def keys(arg)
|
|
207
|
+
case arg
|
|
208
|
+
when Hash then arg.keys
|
|
209
|
+
when Array then (0...arg.length).map(&:to_s)
|
|
210
|
+
else []
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
##################################################################
|
|
215
|
+
# JavaScript emulation helpers (not part of utils.js)
|
|
216
|
+
##################################################################
|
|
217
|
+
|
|
218
|
+
# Equivalent of JavaScript `typeof value`
|
|
219
|
+
def typeof(value)
|
|
220
|
+
case value
|
|
221
|
+
when nil then 'undefined'
|
|
222
|
+
when String then 'string'
|
|
223
|
+
when Numeric then 'number'
|
|
224
|
+
when true, false then 'boolean'
|
|
225
|
+
else
|
|
226
|
+
callable?(value) ? 'function' : 'object'
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Equivalent of JavaScript truthiness
|
|
231
|
+
def truthy?(value)
|
|
232
|
+
case value
|
|
233
|
+
when nil, false, NULL, '' then false
|
|
234
|
+
when Numeric then !(value.zero? || (value.kind_of?(Float) && value.nan?))
|
|
235
|
+
else true
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Converts a numeric result of a floating point operation to an Integer, if it is integral
|
|
240
|
+
def normalize_number(num)
|
|
241
|
+
if num.kind_of?(Float) && num.finite? && num == num.truncate && num.abs < 9_007_199_254_740_992
|
|
242
|
+
num.to_i
|
|
243
|
+
else
|
|
244
|
+
num
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Equivalent of JavaScript Number.isInteger
|
|
249
|
+
def integer?(value)
|
|
250
|
+
value.kind_of?(Integer) || (value.kind_of?(Float) && value.finite? && value == value.truncate)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Equivalent of the JavaScript Number::toString() algorithm
|
|
254
|
+
def number_to_s(num)
|
|
255
|
+
if num.kind_of?(Integer)
|
|
256
|
+
return num.to_s if num.abs < 10**21
|
|
257
|
+
|
|
258
|
+
num = num.to_f
|
|
259
|
+
end
|
|
260
|
+
return 'NaN' if num.nan?
|
|
261
|
+
return (num.positive? ? 'Infinity' : '-Infinity') if num.infinite?
|
|
262
|
+
return '0' if num.zero?
|
|
263
|
+
|
|
264
|
+
digits, point = shortest_digits(num.abs)
|
|
265
|
+
k = digits.length
|
|
266
|
+
str = if point.between?(k, 21)
|
|
267
|
+
digits + ('0' * (point - k))
|
|
268
|
+
elsif point.positive? && point <= 21
|
|
269
|
+
"#{digits[0, point]}.#{digits[point..]}"
|
|
270
|
+
elsif point > -6 && point <= 0
|
|
271
|
+
"0.#{'0' * -point}#{digits}"
|
|
272
|
+
else
|
|
273
|
+
e = point - 1
|
|
274
|
+
exp = e.negative? ? "-#{-e}" : "+#{e}"
|
|
275
|
+
k == 1 ? "#{digits}e#{exp}" : "#{digits[0]}.#{digits[1..]}e#{exp}"
|
|
276
|
+
end
|
|
277
|
+
num.negative? ? "-#{str}" : str
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Returns the shortest round-trip decimal digits of a positive finite float,
|
|
281
|
+
# and the position of the decimal point relative to the start of the digits
|
|
282
|
+
def shortest_digits(num)
|
|
283
|
+
mantissa, exp = num.to_s.split('e')
|
|
284
|
+
int_part, frac_part = mantissa.split('.')
|
|
285
|
+
digits = int_part + (frac_part || '')
|
|
286
|
+
point = int_part.length + exp.to_i
|
|
287
|
+
stripped = digits.sub(/\A0+/, '')
|
|
288
|
+
point -= digits.length - stripped.length
|
|
289
|
+
digits = stripped.sub(/0+\z/, '')
|
|
290
|
+
[digits, point]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Equivalent of JavaScript Number.prototype.toFixed
|
|
294
|
+
def to_fixed(num, digits)
|
|
295
|
+
return number_to_s(num) if num.kind_of?(Float) && !num.finite?
|
|
296
|
+
return number_to_s(num) if num.abs >= 1e21
|
|
297
|
+
|
|
298
|
+
negative = num.negative?
|
|
299
|
+
scaled = (num.abs.to_r * (10**digits)).round(:half => :up)
|
|
300
|
+
str = scaled.to_s
|
|
301
|
+
if digits.positive?
|
|
302
|
+
str = str.rjust(digits + 1, '0')
|
|
303
|
+
str = "#{str[0...-digits]}.#{str[-digits..]}"
|
|
304
|
+
end
|
|
305
|
+
negative && scaled != 0 ? "-#{str}" : str
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Equivalent of JavaScript Number.prototype.toPrecision, followed by conversion back to a number
|
|
309
|
+
def to_precision(num, precision)
|
|
310
|
+
return num if num.zero? || !num.finite?
|
|
311
|
+
|
|
312
|
+
exact = num.abs.to_r
|
|
313
|
+
e = Math.log10(num.abs).floor
|
|
314
|
+
# correct for floating point inaccuracies in log10
|
|
315
|
+
e += 1 while exact >= Rational(10)**(e + 1)
|
|
316
|
+
e -= 1 while exact < Rational(10)**e
|
|
317
|
+
scale = Rational(10)**(precision - 1 - e)
|
|
318
|
+
n = (exact * scale).round(:half => :up)
|
|
319
|
+
result = (n / scale).to_f
|
|
320
|
+
num.negative? ? -result : result
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Equivalent of JavaScript parseInt(str, 10) - returns nil (NaN) if no digits
|
|
324
|
+
def parse_int(str)
|
|
325
|
+
m = /\A\s*([-+]?\d+)/.match(str.to_s)
|
|
326
|
+
m ? m[1].to_i : nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Equivalent of Array.prototype.slice / String.prototype.slice
|
|
330
|
+
def js_slice(obj, start, end_pos = nil)
|
|
331
|
+
len = obj.length
|
|
332
|
+
relative = lambda do |val|
|
|
333
|
+
val = val.to_f
|
|
334
|
+
return 0 if val.nan?
|
|
335
|
+
|
|
336
|
+
val = val.truncate if val.finite?
|
|
337
|
+
val = len + val if val.negative?
|
|
338
|
+
val.clamp(0, len).to_i
|
|
339
|
+
end
|
|
340
|
+
from = relative.call(start)
|
|
341
|
+
to = end_pos.nil? ? len : relative.call(end_pos)
|
|
342
|
+
return obj.kind_of?(String) ? +'' : [] if to <= from
|
|
343
|
+
|
|
344
|
+
obj[from...to]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Equivalent of String.prototype.split(separator, limit) for a string separator
|
|
348
|
+
def js_split(str, separator, limit = nil)
|
|
349
|
+
result = if separator == ''
|
|
350
|
+
str.chars
|
|
351
|
+
else
|
|
352
|
+
parts = []
|
|
353
|
+
position = 0
|
|
354
|
+
index = str.index(separator, position)
|
|
355
|
+
until index.nil?
|
|
356
|
+
parts.push(str[position...index])
|
|
357
|
+
position = index + separator.length
|
|
358
|
+
index = str.index(separator, position)
|
|
359
|
+
end
|
|
360
|
+
parts.push(str[position..])
|
|
361
|
+
parts
|
|
362
|
+
end
|
|
363
|
+
limit.nil? ? result : result.take(limit.to_i)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Equivalent of the JavaScript Number(str) conversion
|
|
367
|
+
def js_number(str)
|
|
368
|
+
s = str.strip
|
|
369
|
+
return 0 if s.empty?
|
|
370
|
+
|
|
371
|
+
case s
|
|
372
|
+
when /\A0[xX][0-9a-fA-F]+\z/, /\A0[oO][0-7]+\z/, /\A0[bB][01]+\z/
|
|
373
|
+
Integer(s)
|
|
374
|
+
when /\A[-+]?(\d+\.?\d*([eE][-+]?\d+)?|\.\d+([eE][-+]?\d+)?)\z/
|
|
375
|
+
normalize_number(Float(s))
|
|
376
|
+
when /\A[-+]?Infinity\z/
|
|
377
|
+
s.start_with?('-') ? -Float::INFINITY : Float::INFINITY
|
|
378
|
+
else
|
|
379
|
+
Float::NAN
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
class URIError < StandardError; end
|
|
384
|
+
|
|
385
|
+
URI_UNRESERVED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()"
|
|
386
|
+
URI_RESERVED = ';/?:@&=+$,#'
|
|
387
|
+
|
|
388
|
+
# Equivalent of encodeURI / encodeURIComponent
|
|
389
|
+
# @param unescaped - the set of characters that are not escaped
|
|
390
|
+
def encode_uri(str, unescaped)
|
|
391
|
+
raise URIError unless str.valid_encoding?
|
|
392
|
+
|
|
393
|
+
str.each_char.map do |ch|
|
|
394
|
+
if unescaped.include?(ch)
|
|
395
|
+
ch
|
|
396
|
+
else
|
|
397
|
+
ch.bytes.map { |b| '%%%02X' % b }.join
|
|
398
|
+
end
|
|
399
|
+
end.join
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Equivalent of decodeURI / decodeURIComponent
|
|
403
|
+
# @param reserved - the set of characters whose escape sequences are not decoded
|
|
404
|
+
def decode_uri(str, reserved)
|
|
405
|
+
result = +''
|
|
406
|
+
k = 0
|
|
407
|
+
length = str.length
|
|
408
|
+
while k < length
|
|
409
|
+
ch = str[k]
|
|
410
|
+
if ch != '%'
|
|
411
|
+
result << ch
|
|
412
|
+
k += 1
|
|
413
|
+
next
|
|
414
|
+
end
|
|
415
|
+
start = k
|
|
416
|
+
raise URIError if k + 2 >= length || !str[k + 1, 2].match?(/\A\h\h\z/)
|
|
417
|
+
|
|
418
|
+
b = str[k + 1, 2].to_i(16)
|
|
419
|
+
k += 2
|
|
420
|
+
if b < 0x80
|
|
421
|
+
c = b.chr
|
|
422
|
+
result << (reserved.include?(c) ? str[start..k] : c)
|
|
423
|
+
else
|
|
424
|
+
n = if (b & 0xE0) == 0xC0 then 2
|
|
425
|
+
elsif (b & 0xF0) == 0xE0 then 3
|
|
426
|
+
elsif (b & 0xF8) == 0xF0 then 4
|
|
427
|
+
else raise URIError
|
|
428
|
+
end
|
|
429
|
+
bytes = [b]
|
|
430
|
+
(n - 1).times do
|
|
431
|
+
k += 1
|
|
432
|
+
raise URIError if k + 2 >= length || str[k] != '%' || !str[k + 1, 2].match?(/\A\h\h\z/)
|
|
433
|
+
|
|
434
|
+
byte = str[k + 1, 2].to_i(16)
|
|
435
|
+
raise URIError if (byte & 0xC0) != 0x80
|
|
436
|
+
|
|
437
|
+
bytes.push(byte)
|
|
438
|
+
k += 2
|
|
439
|
+
end
|
|
440
|
+
decoded = bytes.pack('C*').force_encoding(Encoding::UTF_8)
|
|
441
|
+
raise URIError unless decoded.valid_encoding?
|
|
442
|
+
|
|
443
|
+
result << decoded
|
|
444
|
+
end
|
|
445
|
+
k += 1
|
|
446
|
+
end
|
|
447
|
+
result
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Equivalent of String.fromCharCode for a single code point.
|
|
451
|
+
# Lone surrogates are encoded as (invalid) UTF-8 bytes, so that they can be detected later
|
|
452
|
+
def from_char_code(codepoint)
|
|
453
|
+
[codepoint].pack('U').force_encoding(Encoding::UTF_8)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Translates a JavaScript regular expression into a Ruby Regexp
|
|
457
|
+
# @param source - the JavaScript regex pattern
|
|
458
|
+
# @param flags - the JavaScript regex flags (only 'i' and 'm' are significant)
|
|
459
|
+
def js_regexp(source, flags = '')
|
|
460
|
+
multiline = flags.include?('m')
|
|
461
|
+
out = +''
|
|
462
|
+
in_class = false
|
|
463
|
+
i = 0
|
|
464
|
+
while i < source.length
|
|
465
|
+
ch = source[i]
|
|
466
|
+
if ch == '\\'
|
|
467
|
+
out << ch << (source[i + 1] || '')
|
|
468
|
+
i += 2
|
|
469
|
+
next
|
|
470
|
+
end
|
|
471
|
+
if in_class
|
|
472
|
+
in_class = false if ch == ']'
|
|
473
|
+
out << ch
|
|
474
|
+
elsif ch == '['
|
|
475
|
+
if source[i + 1] == ']'
|
|
476
|
+
# empty class - matches nothing
|
|
477
|
+
out << '(?!)'
|
|
478
|
+
i += 2
|
|
479
|
+
next
|
|
480
|
+
elsif source[i + 1, 2] == '^]'
|
|
481
|
+
# negated empty class - matches anything
|
|
482
|
+
out << '[\s\S]'
|
|
483
|
+
i += 3
|
|
484
|
+
next
|
|
485
|
+
end
|
|
486
|
+
in_class = true
|
|
487
|
+
out << ch
|
|
488
|
+
# a ']' immediately after '[' or '[^' is a literal in Ruby but closes the class in JS
|
|
489
|
+
elsif ch == '^' && !multiline
|
|
490
|
+
out << '\A'
|
|
491
|
+
elsif ch == '$' && !multiline
|
|
492
|
+
out << '\z'
|
|
493
|
+
else
|
|
494
|
+
out << ch
|
|
495
|
+
end
|
|
496
|
+
i += 1
|
|
497
|
+
end
|
|
498
|
+
options = flags.include?('i') ? Regexp::IGNORECASE : 0
|
|
499
|
+
Regexp.new("(?a)#{out}", options)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Converts a Ruby value supplied by the caller into the representation used internally:
|
|
503
|
+
# Symbol keys are converted to Strings and nil is converted to NULL
|
|
504
|
+
def from_ruby(value)
|
|
505
|
+
case value
|
|
506
|
+
when nil then NULL
|
|
507
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.kind_of?(Symbol) ? k.to_s : k] = from_ruby(v) }
|
|
508
|
+
when JArray then value
|
|
509
|
+
when Array then value.map { |v| from_ruby(v) }
|
|
510
|
+
when Symbol then value.to_s
|
|
511
|
+
else value
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Converts an internal value into a plain Ruby value for the caller:
|
|
516
|
+
# NULL is converted to nil, and JArrays to plain Arrays
|
|
517
|
+
def to_ruby(value)
|
|
518
|
+
case value
|
|
519
|
+
when NULL then nil
|
|
520
|
+
when Hash then value.transform_values { |v| to_ruby(v) }
|
|
521
|
+
when Array then value.map { |v| to_ruby(v) }
|
|
522
|
+
else value
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# JSON.stringify with JavaScript semantics
|
|
527
|
+
# @param replacer - optional proc(value) returning the replaced value
|
|
528
|
+
# @param space - number of spaces to indent
|
|
529
|
+
def json_stringify(value, replacer = nil, space = 0)
|
|
530
|
+
value = replacer.call(value) if replacer
|
|
531
|
+
stringify_value(value, replacer, space, '')
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def stringify_value(value, replacer, space, indent)
|
|
535
|
+
case value
|
|
536
|
+
when nil then nil
|
|
537
|
+
when NULL, true, false then value == NULL ? 'null' : value.to_s
|
|
538
|
+
when String then quote_json_string(value)
|
|
539
|
+
when Numeric
|
|
540
|
+
value.kind_of?(Float) && !value.finite? ? 'null' : number_to_s(value)
|
|
541
|
+
when Array
|
|
542
|
+
return '[]' if value.empty?
|
|
543
|
+
|
|
544
|
+
inner = indent + (' ' * space)
|
|
545
|
+
items = value.map do |item|
|
|
546
|
+
item = replacer.call(item) if replacer
|
|
547
|
+
stringify_value(item, replacer, space, inner) || 'null'
|
|
548
|
+
end
|
|
549
|
+
if space.positive?
|
|
550
|
+
"[\n#{inner}#{items.join(",\n#{inner}")}\n#{indent}]"
|
|
551
|
+
else
|
|
552
|
+
"[#{items.join(',')}]"
|
|
553
|
+
end
|
|
554
|
+
when Hash
|
|
555
|
+
inner = indent + (' ' * space)
|
|
556
|
+
sep = space.positive? ? ': ' : ':'
|
|
557
|
+
items = value.filter_map do |k, v|
|
|
558
|
+
v = replacer.call(v) if replacer
|
|
559
|
+
str = stringify_value(v, replacer, space, inner)
|
|
560
|
+
"#{quote_json_string(k.to_s)}#{sep}#{str}" unless str.nil?
|
|
561
|
+
end
|
|
562
|
+
return '{}' if items.empty?
|
|
563
|
+
|
|
564
|
+
if space.positive?
|
|
565
|
+
"{\n#{inner}#{items.join(",\n#{inner}")}\n#{indent}}"
|
|
566
|
+
else
|
|
567
|
+
"{#{items.join(',')}}"
|
|
568
|
+
end
|
|
569
|
+
else
|
|
570
|
+
callable?(value) ? nil : '{}'
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
JSON_ESCAPES = {'"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t'}.freeze
|
|
575
|
+
|
|
576
|
+
def quote_json_string(str)
|
|
577
|
+
unless str.valid_encoding?
|
|
578
|
+
# lone surrogates (see #from_char_code) are escaped, as they are by JSON.stringify
|
|
579
|
+
marked = str.scrub { |bytes| "\uFFFF#{bytes.unpack1('H*')}\uFFFF" }
|
|
580
|
+
return quote_json_string(marked).gsub(/(?:\uFFFF\h+\uFFFF)+/) do |run|
|
|
581
|
+
bytes = run.scan(/\h\h/).map(&:hex)
|
|
582
|
+
bytes.each_slice(3).map do |b0, b1, b2|
|
|
583
|
+
codepoint = ((b0 & 0x0F) << 12) | (((b1 || 0) & 0x3F) << 6) | ((b2 || 0) & 0x3F)
|
|
584
|
+
'\\u%04x' % codepoint
|
|
585
|
+
end.join
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
escaped = str.gsub(/["\\\u0000-\u001f]/) do |ch|
|
|
590
|
+
JSON_ESCAPES[ch] || ('\\u%04x' % ch.ord)
|
|
591
|
+
end
|
|
592
|
+
"\"#{escaped}\""
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Equivalent of Math.floor - the result is an Integer only if it can be represented exactly
|
|
596
|
+
def js_floor(num)
|
|
597
|
+
num.kind_of?(Integer) ? num : normalize_number(num.floor.to_f)
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
data/lib/jsonata.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'jsonata/version'
|
|
4
|
+
require_relative 'jsonata/error'
|
|
5
|
+
require_relative 'jsonata/utils'
|
|
6
|
+
require_relative 'jsonata/signature'
|
|
7
|
+
require_relative 'jsonata/parser'
|
|
8
|
+
require_relative 'jsonata/functions'
|
|
9
|
+
require_relative 'jsonata/datetime'
|
|
10
|
+
require_relative 'jsonata/evaluator'
|
|
11
|
+
|
|
12
|
+
# JSONata - JSON query and transformation language
|
|
13
|
+
#
|
|
14
|
+
# This is a Ruby port of jsonata-js (https://github.com/jsonata-js/jsonata).
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# require 'jsonata'
|
|
18
|
+
#
|
|
19
|
+
# data = { example: [{ value: 4 }, { value: 7 }, { value: 13 }] }
|
|
20
|
+
# expression = Jsonata.expression('$sum(example.value)')
|
|
21
|
+
# result = expression.evaluate(data) # => 24
|
|
22
|
+
#
|
|
23
|
+
# @see http://jsonata.org/ JSONata official website
|
|
24
|
+
# @see http://docs.jsonata.org/ JSONata documentation
|
|
25
|
+
module Jsonata
|
|
26
|
+
class << self
|
|
27
|
+
# Compile a JSONata expression (jsonata-js: `jsonata(expr, options)`)
|
|
28
|
+
#
|
|
29
|
+
# @param expr [String] the JSONata expression
|
|
30
|
+
# @param options [Hash] see Jsonata::Expression#initialize
|
|
31
|
+
# @return [Expression] a compiled JSONata expression
|
|
32
|
+
# @raise [Jsonata::Error] if the expression is invalid
|
|
33
|
+
def expression(expr, options = nil)
|
|
34
|
+
Expression.new(expr, options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Evaluate a JSONata expression against data (convenience method)
|
|
38
|
+
#
|
|
39
|
+
# @param expr [String] the JSONata expression
|
|
40
|
+
# @param data [Hash, Array, Object] the data to query
|
|
41
|
+
# @param bindings [Hash] variable bindings
|
|
42
|
+
# @return [Object] the result of the evaluation
|
|
43
|
+
def evaluate(expr, data = nil, bindings = nil)
|
|
44
|
+
expression(expr).evaluate(data, bindings)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|