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