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,235 @@
|
|
|
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/signature.js
|
|
8
|
+
|
|
9
|
+
module Jsonata
|
|
10
|
+
class Signature
|
|
11
|
+
# A mapping between the function signature symbols and the full plural of the type
|
|
12
|
+
# Expected to be used in error messages
|
|
13
|
+
ARRAY_SIGNATURE_MAPPING = {
|
|
14
|
+
'a' => 'arrays',
|
|
15
|
+
'b' => 'booleans',
|
|
16
|
+
'f' => 'functions',
|
|
17
|
+
'n' => 'numbers',
|
|
18
|
+
'o' => 'objects',
|
|
19
|
+
's' => 'strings'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
Param = Struct.new(:regex, :type, :array, :context, :context_regex, :subtype)
|
|
23
|
+
|
|
24
|
+
attr_reader :definition
|
|
25
|
+
|
|
26
|
+
# Parses a function signature definition and returns a validation function
|
|
27
|
+
# @param signature - the signature between the <angle brackets>
|
|
28
|
+
def initialize(signature)
|
|
29
|
+
@definition = signature
|
|
30
|
+
# create a Regex that represents this signature and return a function that when invoked,
|
|
31
|
+
# returns the validated (possibly fixed-up) arguments, or throws a validation error
|
|
32
|
+
# step through the signature, one symbol at a time
|
|
33
|
+
position = 1
|
|
34
|
+
@params = []
|
|
35
|
+
param = Param.new
|
|
36
|
+
prev_param = param
|
|
37
|
+
next_param = lambda do
|
|
38
|
+
@params.push(param)
|
|
39
|
+
prev_param = param
|
|
40
|
+
param = Param.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
while position < signature.length
|
|
44
|
+
symbol = signature[position]
|
|
45
|
+
if symbol == ':'
|
|
46
|
+
# TODO figure out what to do with the return type
|
|
47
|
+
# ignore it for now
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
case symbol
|
|
52
|
+
when 's', 'n', 'b', 'l', 'o' # string, number, boolean, null, object
|
|
53
|
+
param.regex = "[#{symbol}m]"
|
|
54
|
+
param.type = symbol
|
|
55
|
+
next_param.call
|
|
56
|
+
when 'a' # array
|
|
57
|
+
# normally treat any value as singleton array
|
|
58
|
+
param.regex = '[asnblfom]'
|
|
59
|
+
param.type = symbol
|
|
60
|
+
param.array = true
|
|
61
|
+
next_param.call
|
|
62
|
+
when 'f' # function
|
|
63
|
+
param.regex = 'f'
|
|
64
|
+
param.type = symbol
|
|
65
|
+
next_param.call
|
|
66
|
+
when 'j' # any JSON type
|
|
67
|
+
param.regex = '[asnblom]'
|
|
68
|
+
param.type = symbol
|
|
69
|
+
next_param.call
|
|
70
|
+
when 'x' # any type
|
|
71
|
+
param.regex = '[asnblfom]'
|
|
72
|
+
param.type = symbol
|
|
73
|
+
next_param.call
|
|
74
|
+
when '-' # use context if param not supplied
|
|
75
|
+
prev_param.context = true
|
|
76
|
+
prev_param.context_regex = Regexp.new(prev_param.regex) # pre-compiled to test the context type at runtime
|
|
77
|
+
prev_param.regex += '?'
|
|
78
|
+
when '?', '+' # optional param, one or more
|
|
79
|
+
prev_param.regex += symbol
|
|
80
|
+
when '(' # choice of types
|
|
81
|
+
# search forward for matching ')'
|
|
82
|
+
end_paren = find_closing_bracket(signature, position, '(', ')')
|
|
83
|
+
choice = signature[(position + 1)...end_paren]
|
|
84
|
+
if choice.index('<').nil?
|
|
85
|
+
# no parameterized types, simple regex
|
|
86
|
+
param.regex = "[#{choice}m]"
|
|
87
|
+
else
|
|
88
|
+
# TODO harder
|
|
89
|
+
raise Jsonata::Error.new('S0402', :value => choice, :offset => position)
|
|
90
|
+
end
|
|
91
|
+
param.type = "(#{choice})"
|
|
92
|
+
position = end_paren
|
|
93
|
+
next_param.call
|
|
94
|
+
when '<' # type parameter - can only be applied to 'a' and 'f'
|
|
95
|
+
if prev_param.type == 'a' || prev_param.type == 'f'
|
|
96
|
+
# search forward for matching '>'
|
|
97
|
+
end_pos = find_closing_bracket(signature, position, '<', '>')
|
|
98
|
+
prev_param.subtype = signature[(position + 1)...end_pos]
|
|
99
|
+
position = end_pos
|
|
100
|
+
else
|
|
101
|
+
raise Jsonata::Error.new('S0401', :value => prev_param.type, :offset => position)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
position += 1
|
|
105
|
+
end
|
|
106
|
+
regex_str = "^#{@params.map { |p| "(#{p.regex})" }.join}$"
|
|
107
|
+
@regex = Regexp.new(regex_str)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def validate(args, context)
|
|
111
|
+
supplied_sig = args.map { |arg| get_symbol(arg) }.join
|
|
112
|
+
is_valid = @regex.match(supplied_sig)
|
|
113
|
+
throw_validation_error(args, supplied_sig) unless is_valid
|
|
114
|
+
|
|
115
|
+
validated_args = []
|
|
116
|
+
arg_index = 0
|
|
117
|
+
@params.each_with_index do |param, index|
|
|
118
|
+
arg = args[arg_index]
|
|
119
|
+
match = is_valid[index + 1]
|
|
120
|
+
if match == ''
|
|
121
|
+
if param.context && param.context_regex
|
|
122
|
+
# substitute context value for missing arg
|
|
123
|
+
# first check that the context value is the right type
|
|
124
|
+
context_type = get_symbol(context)
|
|
125
|
+
# test contextType against the regex for this arg (without the trailing ?)
|
|
126
|
+
if param.context_regex.match?(context_type)
|
|
127
|
+
validated_args.push(context)
|
|
128
|
+
else
|
|
129
|
+
# context value not compatible with this argument
|
|
130
|
+
raise Jsonata::Error.new('T0411', :value => context, :index => arg_index + 1)
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
validated_args.push(arg)
|
|
134
|
+
arg_index += 1
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
# may have matched multiple args (if the regex ends with a '+')
|
|
138
|
+
# split into single tokens
|
|
139
|
+
match.each_char do |single|
|
|
140
|
+
arg = args[arg_index]
|
|
141
|
+
if param.type == 'a'
|
|
142
|
+
if single == 'm'
|
|
143
|
+
# missing (undefined)
|
|
144
|
+
arg = nil
|
|
145
|
+
else
|
|
146
|
+
array_ok = true
|
|
147
|
+
# is there type information on the contents of the array?
|
|
148
|
+
unless param.subtype.nil?
|
|
149
|
+
if single != 'a' && match != param.subtype
|
|
150
|
+
array_ok = false
|
|
151
|
+
elsif single == 'a'
|
|
152
|
+
if arg.length.positive?
|
|
153
|
+
item_type = get_symbol(arg[0])
|
|
154
|
+
if item_type != param.subtype[0] # TODO recurse further
|
|
155
|
+
array_ok = false
|
|
156
|
+
else
|
|
157
|
+
# make sure every item in the array is this type
|
|
158
|
+
array_ok = arg.all? { |val| get_symbol(val) == item_type }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
unless array_ok
|
|
164
|
+
raise Jsonata::Error.new('T0412', :value => arg, :index => arg_index + 1,
|
|
165
|
+
:type => ARRAY_SIGNATURE_MAPPING[param.subtype])
|
|
166
|
+
end
|
|
167
|
+
# the function expects an array. If it's not one, make it so
|
|
168
|
+
arg = [arg] if single != 'a'
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
validated_args.push(arg)
|
|
172
|
+
arg_index += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
validated_args
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
# returns the position of the closing symbol (e.g. bracket) in a string
|
|
182
|
+
# that balances the opening symbol at position start
|
|
183
|
+
def find_closing_bracket(str, start, open_symbol, close_symbol)
|
|
184
|
+
depth = 1
|
|
185
|
+
position = start
|
|
186
|
+
while position < str.length
|
|
187
|
+
position += 1
|
|
188
|
+
symbol = str[position]
|
|
189
|
+
if symbol == close_symbol
|
|
190
|
+
depth -= 1
|
|
191
|
+
break if depth.zero? # we're done
|
|
192
|
+
elsif symbol == open_symbol
|
|
193
|
+
depth += 1
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
position
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def get_symbol(value)
|
|
200
|
+
if Utils.function?(value)
|
|
201
|
+
'f'
|
|
202
|
+
else
|
|
203
|
+
case value
|
|
204
|
+
when String then 's'
|
|
205
|
+
when Numeric then 'n'
|
|
206
|
+
when true, false then 'b'
|
|
207
|
+
when NULL then 'l'
|
|
208
|
+
when Array then 'a'
|
|
209
|
+
when nil then 'm' # any value can be undefined, but should be allowed to match - m for missing
|
|
210
|
+
else 'o'
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def throw_validation_error(bad_args, bad_sig)
|
|
216
|
+
# to figure out where this went wrong we need apply each component of the
|
|
217
|
+
# regex to each argument until we get to the one that fails to match
|
|
218
|
+
partial_pattern = '^'
|
|
219
|
+
good_to = 0
|
|
220
|
+
@params.each do |param|
|
|
221
|
+
partial_pattern += param.regex
|
|
222
|
+
match = Regexp.new(partial_pattern).match(bad_sig)
|
|
223
|
+
if match.nil?
|
|
224
|
+
# failed here
|
|
225
|
+
raise Jsonata::Error.new('T0410', :value => bad_args[good_to], :index => good_to + 1)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
good_to = match[0].length
|
|
229
|
+
end
|
|
230
|
+
# if it got this far, it's probably because of extraneous arguments (we
|
|
231
|
+
# haven't added the trailing '$' in the regex yet.
|
|
232
|
+
raise Jsonata::Error.new('T0410', :value => bad_args[good_to], :index => good_to + 1)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|