uri_template 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/README +29 -27
- data/lib/uri_template.rb +17 -4
- data/lib/uri_template/colon.rb +2 -2
- data/lib/uri_template/draft7.rb +55 -699
- data/lib/uri_template/rfc6570.rb +992 -0
- data/lib/uri_template/utils.rb +22 -4
- data/uri_template.gemspec +5 -4
- metadata +29 -17
@@ -0,0 +1,992 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
3
|
+
# it under the terms of the Affero GNU General Public License as published by
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
5
|
+
# (at your option) any later version.
|
6
|
+
#
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
10
|
+
# GNU General Public License for more details.
|
11
|
+
#
|
12
|
+
# You should have received a copy of the GNU General Public License
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
|
+
#
|
15
|
+
# (c) 2011 - 2012 by Hannes Georg
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'strscan'
|
19
|
+
require 'set'
|
20
|
+
require 'forwardable'
|
21
|
+
|
22
|
+
require 'uri_template'
|
23
|
+
require 'uri_template/utils'
|
24
|
+
|
25
|
+
# A uri template which should comply with the rfc 6570 ( http://tools.ietf.org/html/rfc6570 ).
|
26
|
+
# @note
|
27
|
+
# Most specs and examples refer to this class directly, because they are acutally refering to this specific implementation. If you just want uri templates, you should rather use the methods on {URITemplate} to create templates since they will select an implementation.
|
28
|
+
class URITemplate::RFC6570
|
29
|
+
|
30
|
+
TYPE = :rfc6570
|
31
|
+
|
32
|
+
include URITemplate
|
33
|
+
extend Forwardable
|
34
|
+
|
35
|
+
# @private
|
36
|
+
Utils = URITemplate::Utils
|
37
|
+
|
38
|
+
if SUPPORTS_UNICODE_CHARS
|
39
|
+
# @private
|
40
|
+
# \/ - unicode ctrl-chars
|
41
|
+
LITERAL = /([^"'%<>\\^`{|}\u0000-\u001F\u007F-\u009F\s]|%[0-9a-fA-F]{2})+/u
|
42
|
+
else
|
43
|
+
# @private
|
44
|
+
LITERAL = Regexp.compile('([^"\'%<>\\\\^`{|}\x00-\x1F\x7F-\x9F\s]|%[0-9a-fA-F]{2})+',Utils::KCODE_UTF8)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @private
|
48
|
+
CHARACTER_CLASSES = {
|
49
|
+
|
50
|
+
:unreserved => {
|
51
|
+
:class => '(?:[A-Za-z0-9\-\._]|%[0-9a-fA-F]{2})',
|
52
|
+
:grabs_comma => false
|
53
|
+
},
|
54
|
+
:unreserved_reserved_pct => {
|
55
|
+
:class => '(?:[A-Za-z0-9\-\._:\/?#\[\]@!\$%\'\(\)*+,;=]|%[0-9a-fA-F]{2})',
|
56
|
+
:grabs_comma => true
|
57
|
+
},
|
58
|
+
|
59
|
+
:varname => {
|
60
|
+
:class => '(?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*?',
|
61
|
+
:class_name => 'c_vn_'
|
62
|
+
}
|
63
|
+
|
64
|
+
}
|
65
|
+
|
66
|
+
# Specifies that no processing should be done upon extraction.
|
67
|
+
# @see #extract
|
68
|
+
NO_PROCESSING = []
|
69
|
+
|
70
|
+
# Specifies that the extracted values should be processed.
|
71
|
+
# @see #extract
|
72
|
+
CONVERT_VALUES = [:convert_values]
|
73
|
+
|
74
|
+
# Specifies that the extracted variable list should be processed.
|
75
|
+
# @see #extract
|
76
|
+
CONVERT_RESULT = [:convert_result]
|
77
|
+
|
78
|
+
# Default processing. Means: convert values and the list itself.
|
79
|
+
# @see #extract
|
80
|
+
DEFAULT_PROCESSING = CONVERT_VALUES + CONVERT_RESULT
|
81
|
+
|
82
|
+
# @private
|
83
|
+
VAR = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
|
84
|
+
((?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2})(?:\.?(?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2}))*)(\*)?(?::(\d+))?
|
85
|
+
__REGEXP__
|
86
|
+
|
87
|
+
# @private
|
88
|
+
EXPRESSION = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
|
89
|
+
\{([+#\./;?&]?)((?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2})(?:\.?(?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2}))*\*?(?::\d+)?(?:,(?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2})(?:\.?(?:[a-zA-Z0-9_]|%[0-9a-fA-F]{2}))*\*?(?::\d+)?)*)\}
|
90
|
+
__REGEXP__
|
91
|
+
|
92
|
+
# @private
|
93
|
+
URI = Regexp.compile(<<__REGEXP__.strip, Utils::KCODE_UTF8)
|
94
|
+
\\A(#{LITERAL.source}|#{EXPRESSION.source})*\\z
|
95
|
+
__REGEXP__
|
96
|
+
|
97
|
+
SLASH = ?/
|
98
|
+
|
99
|
+
# @private
|
100
|
+
class Token
|
101
|
+
end
|
102
|
+
|
103
|
+
# @private
|
104
|
+
class Literal < Token
|
105
|
+
|
106
|
+
include URITemplate::Literal
|
107
|
+
|
108
|
+
def initialize(string)
|
109
|
+
@string = string
|
110
|
+
end
|
111
|
+
|
112
|
+
def level
|
113
|
+
1
|
114
|
+
end
|
115
|
+
|
116
|
+
def arity
|
117
|
+
0
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_r_source(*_)
|
121
|
+
Regexp.escape(@string)
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
@string
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# @private
|
132
|
+
class Expression < Token
|
133
|
+
|
134
|
+
include URITemplate::Expression
|
135
|
+
|
136
|
+
attr_reader :variables, :max_length
|
137
|
+
|
138
|
+
def initialize(vars)
|
139
|
+
@variable_specs = vars
|
140
|
+
@variables = vars.map(&:first)
|
141
|
+
@variables.uniq!
|
142
|
+
end
|
143
|
+
|
144
|
+
PREFIX = ''.freeze
|
145
|
+
SEPARATOR = ','.freeze
|
146
|
+
PAIR_CONNECTOR = '='.freeze
|
147
|
+
PAIR_IF_EMPTY = true
|
148
|
+
LIST_CONNECTOR = ','.freeze
|
149
|
+
BASE_LEVEL = 1
|
150
|
+
|
151
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
|
152
|
+
|
153
|
+
NAMED = false
|
154
|
+
OPERATOR = ''
|
155
|
+
|
156
|
+
def level
|
157
|
+
if @variable_specs.none?{|_,expand,ml| expand || (ml > 0) }
|
158
|
+
if @variable_specs.size == 1
|
159
|
+
return self.class::BASE_LEVEL
|
160
|
+
else
|
161
|
+
return 3
|
162
|
+
end
|
163
|
+
else
|
164
|
+
return 4
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def expands?
|
169
|
+
@variable_specs.any?{|_,expand,_| expand }
|
170
|
+
end
|
171
|
+
|
172
|
+
def arity
|
173
|
+
@variable_specs.size
|
174
|
+
end
|
175
|
+
|
176
|
+
def expand( vars )
|
177
|
+
result = []
|
178
|
+
@variable_specs.each{| var, expand , max_length |
|
179
|
+
unless vars[var].nil?
|
180
|
+
if vars[var].kind_of?(Hash) or Utils.pair_array?(vars[var])
|
181
|
+
if max_length && max_length > 0
|
182
|
+
raise InvalidValue::LengthLimitInapplicable.new(var,vars[var])
|
183
|
+
end
|
184
|
+
result.push( *transform_hash(var, vars[var], expand, max_length) )
|
185
|
+
elsif vars[var].kind_of? Array
|
186
|
+
if max_length && max_length > 0
|
187
|
+
raise InvalidValue::LengthLimitInapplicable.new(var,vars[var])
|
188
|
+
end
|
189
|
+
result.push( *transform_array(var, vars[var], expand, max_length) )
|
190
|
+
else
|
191
|
+
if self.class::NAMED
|
192
|
+
result.push( pair(var, vars[var], max_length) )
|
193
|
+
else
|
194
|
+
result.push( cut( escape(vars[var]), max_length ) )
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
}
|
199
|
+
if result.any?
|
200
|
+
return (self.class::PREFIX + result.join(self.class::SEPARATOR))
|
201
|
+
else
|
202
|
+
return ''
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_s
|
207
|
+
return '{' + self.class::OPERATOR + @variable_specs.map{|name,expand,max_length| name + (expand ? '*': '') + (max_length > 0 ? (':' + max_length.to_s) : '') }.join(',') + '}'
|
208
|
+
end
|
209
|
+
|
210
|
+
#TODO: certain things after a slurpy variable will never get matched. therefore, it's pointless to add expressions for them
|
211
|
+
#TODO: variables, which appear twice could be compacted, don't they?
|
212
|
+
def to_r_source
|
213
|
+
source = []
|
214
|
+
first = true
|
215
|
+
vs = @variable_specs.size - 1
|
216
|
+
i = 0
|
217
|
+
if self.class::NAMED
|
218
|
+
@variable_specs.each{| var, expand , max_length |
|
219
|
+
value = "(?:#{self.class::CHARACTER_CLASS[:class]}|,)#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*'}"
|
220
|
+
if expand
|
221
|
+
#if self.class::PAIR_IF_EMPTY
|
222
|
+
pair = "#{CHARACTER_CLASSES[:varname][:class]}#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value}"
|
223
|
+
|
224
|
+
if first
|
225
|
+
source << "((?:#{pair})(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*)"
|
226
|
+
else
|
227
|
+
source << "((?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*)"
|
228
|
+
end
|
229
|
+
else
|
230
|
+
if self.class::PAIR_IF_EMPTY
|
231
|
+
pair = "#{Regexp.escape(var)}(#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})"
|
232
|
+
else
|
233
|
+
pair = "#{Regexp.escape(var)}(#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value}|)"
|
234
|
+
end
|
235
|
+
|
236
|
+
if first
|
237
|
+
source << "(?:#{pair})"
|
238
|
+
else
|
239
|
+
source << "(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})?"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
first = false
|
244
|
+
i = i+1
|
245
|
+
}
|
246
|
+
else
|
247
|
+
@variable_specs.each{| var, expand , max_length |
|
248
|
+
last = (vs == i)
|
249
|
+
if expand
|
250
|
+
# could be list or map, too
|
251
|
+
value = "#{self.class::CHARACTER_CLASS[:class]}#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*'}"
|
252
|
+
|
253
|
+
pair = "(?:#{CHARACTER_CLASSES[:varname][:class]}#{Regexp.escape(self.class::PAIR_CONNECTOR)})?#{value}"
|
254
|
+
|
255
|
+
value = "#{pair}(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*"
|
256
|
+
elsif last
|
257
|
+
# the last will slurp lists
|
258
|
+
if self.class::CHARACTER_CLASS[:grabs_comma]
|
259
|
+
value = "#{self.class::CHARACTER_CLASS[:class]}#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*?'}"
|
260
|
+
else
|
261
|
+
value = "(?:#{self.class::CHARACTER_CLASS[:class]}|,)#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*?'}"
|
262
|
+
end
|
263
|
+
else
|
264
|
+
value = "#{self.class::CHARACTER_CLASS[:class]}#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*?'}"
|
265
|
+
end
|
266
|
+
if first
|
267
|
+
source << "(#{value})"
|
268
|
+
first = false
|
269
|
+
else
|
270
|
+
source << "(?:#{Regexp.escape(self.class::SEPARATOR)}(#{value}))?"
|
271
|
+
end
|
272
|
+
i = i+1
|
273
|
+
}
|
274
|
+
end
|
275
|
+
return '(?:' + Regexp.escape(self.class::PREFIX) + source.join + ')?'
|
276
|
+
end
|
277
|
+
|
278
|
+
def extract(position,matched)
|
279
|
+
name, expand, max_length = @variable_specs[position]
|
280
|
+
if matched.nil?
|
281
|
+
return [[ name , matched ]]
|
282
|
+
end
|
283
|
+
if expand
|
284
|
+
#TODO: do we really need this? - this could be stolen from rack
|
285
|
+
ex = self.class.hash_extractor(max_length)
|
286
|
+
rest = matched
|
287
|
+
splitted = []
|
288
|
+
if self.class::NAMED
|
289
|
+
# 1 = name
|
290
|
+
# 2 = value
|
291
|
+
# 3 = rest
|
292
|
+
until rest.size == 0
|
293
|
+
match = ex.match(rest)
|
294
|
+
if match.nil?
|
295
|
+
raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
|
296
|
+
end
|
297
|
+
if match.post_match.size == 0
|
298
|
+
rest = match[3].to_s
|
299
|
+
else
|
300
|
+
rest = ''
|
301
|
+
end
|
302
|
+
splitted << [ match[1], decode(match[2] + rest , false) ]
|
303
|
+
rest = match.post_match
|
304
|
+
end
|
305
|
+
result = Utils.pair_array_to_hash2( splitted )
|
306
|
+
if result.size == 1 && result[0][0] == name
|
307
|
+
return result
|
308
|
+
else
|
309
|
+
return [ [ name , result ] ]
|
310
|
+
end
|
311
|
+
else
|
312
|
+
found_value = false
|
313
|
+
# 1 = name and seperator
|
314
|
+
# 2 = value
|
315
|
+
# 3 = rest
|
316
|
+
until rest.size == 0
|
317
|
+
match = ex.match(rest)
|
318
|
+
if match.nil?
|
319
|
+
raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
|
320
|
+
end
|
321
|
+
if match.post_match.size == 0
|
322
|
+
rest = match[3].to_s
|
323
|
+
else
|
324
|
+
rest = ''
|
325
|
+
end
|
326
|
+
if match[1]
|
327
|
+
found_value = true
|
328
|
+
splitted << [ match[1][0..-2], decode(match[2] + rest , false) ]
|
329
|
+
else
|
330
|
+
splitted << [ match[2] + rest, nil ]
|
331
|
+
end
|
332
|
+
rest = match.post_match
|
333
|
+
end
|
334
|
+
if !found_value
|
335
|
+
return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
|
336
|
+
else
|
337
|
+
return [ [ name, splitted ] ]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
elsif self.class::NAMED
|
341
|
+
return [ [ name, decode( matched[1..-1] ) ] ]
|
342
|
+
end
|
343
|
+
|
344
|
+
return [ [ name, decode( matched ) ] ]
|
345
|
+
end
|
346
|
+
|
347
|
+
protected
|
348
|
+
|
349
|
+
module ClassMethods
|
350
|
+
|
351
|
+
def hash_extractor(max_length)
|
352
|
+
@hash_extractors ||= {}
|
353
|
+
@hash_extractors[max_length] ||= begin
|
354
|
+
value = "#{self::CHARACTER_CLASS[:class]}#{(max_length > 0)?'{0,'+max_length.to_s+'}':'*?'}"
|
355
|
+
if self::NAMED
|
356
|
+
pair = "(#{CHARACTER_CLASSES[:varname][:class]})#{Regexp.escape(self::PAIR_CONNECTOR)}(#{value})"
|
357
|
+
else
|
358
|
+
pair = "(#{CHARACTER_CLASSES[:varname][:class]}#{Regexp.escape(self::PAIR_CONNECTOR)})?(#{value})"
|
359
|
+
end
|
360
|
+
source = "\\A#{Regexp.escape(self::SEPARATOR)}?" + pair + "(\\z|#{Regexp.escape(self::SEPARATOR)}(?!#{Regexp.escape(self::SEPARATOR)}))"
|
361
|
+
Regexp.new( source , Utils::KCODE_UTF8)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
end
|
366
|
+
|
367
|
+
extend ClassMethods
|
368
|
+
|
369
|
+
def escape(x)
|
370
|
+
Utils.escape_url(Utils.object_to_param(x))
|
371
|
+
end
|
372
|
+
|
373
|
+
def unescape(x)
|
374
|
+
Utils.unescape_url(x)
|
375
|
+
end
|
376
|
+
|
377
|
+
SPLITTER = /^(?:,(,*)|([^,]+))/
|
378
|
+
|
379
|
+
def decode(x, split = true)
|
380
|
+
if x.nil?
|
381
|
+
if self.class::PAIR_IF_EMPTY
|
382
|
+
return x
|
383
|
+
else
|
384
|
+
return ''
|
385
|
+
end
|
386
|
+
elsif split
|
387
|
+
r = []
|
388
|
+
v = x
|
389
|
+
until v.size == 0
|
390
|
+
m = SPLITTER.match(v)
|
391
|
+
if m[1] and m[1].size > 0
|
392
|
+
if m.post_match.size == 0
|
393
|
+
r << m[1]
|
394
|
+
else
|
395
|
+
r << m[1][0..-2]
|
396
|
+
end
|
397
|
+
elsif m[2]
|
398
|
+
r << unescape(m[2])
|
399
|
+
end
|
400
|
+
v = m.post_match
|
401
|
+
end
|
402
|
+
case(r.size)
|
403
|
+
when 0 then ''
|
404
|
+
when 1 then r.first
|
405
|
+
else r
|
406
|
+
end
|
407
|
+
else
|
408
|
+
unescape(x)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def cut(str,chars)
|
413
|
+
if chars > 0
|
414
|
+
md = Regexp.compile("\\A#{self.class::CHARACTER_CLASS[:class]}{0,#{chars.to_s}}", Utils::KCODE_UTF8).match(str)
|
415
|
+
#TODO: handle invalid matches
|
416
|
+
return md[0]
|
417
|
+
else
|
418
|
+
return str
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def pair(key, value, max_length = 0)
|
423
|
+
ek = escape(key)
|
424
|
+
ev = escape(value)
|
425
|
+
if !self.class::PAIR_IF_EMPTY and ev.size == 0
|
426
|
+
return ek
|
427
|
+
else
|
428
|
+
return ek + self.class::PAIR_CONNECTOR + cut( ev, max_length )
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
def transform_hash(name, hsh, expand , max_length)
|
433
|
+
if expand
|
434
|
+
hsh.map{|key,value| pair(key,value) }
|
435
|
+
elsif hsh.none?
|
436
|
+
[]
|
437
|
+
else
|
438
|
+
[ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + hsh.map{|key,value| escape(key)+self.class::LIST_CONNECTOR+escape(value) }.join(self.class::LIST_CONNECTOR) ]
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def transform_array(name, ary, expand , max_length)
|
443
|
+
if expand
|
444
|
+
self.class::NAMED ? ary.map{|value| pair(name,value) } : ary.map{|value| escape(value) }
|
445
|
+
elsif ary.none?
|
446
|
+
[]
|
447
|
+
else
|
448
|
+
[ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| escape(value) }.join(self.class::LIST_CONNECTOR) ]
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
class Reserved < self
|
453
|
+
|
454
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
455
|
+
OPERATOR = '+'.freeze
|
456
|
+
BASE_LEVEL = 2
|
457
|
+
|
458
|
+
def escape(x)
|
459
|
+
Utils.escape_uri(Utils.object_to_param(x))
|
460
|
+
end
|
461
|
+
|
462
|
+
def unescape(x)
|
463
|
+
Utils.unescape_uri(x)
|
464
|
+
end
|
465
|
+
|
466
|
+
end
|
467
|
+
|
468
|
+
class Fragment < self
|
469
|
+
|
470
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
471
|
+
PREFIX = '#'.freeze
|
472
|
+
OPERATOR = '#'.freeze
|
473
|
+
BASE_LEVEL = 2
|
474
|
+
|
475
|
+
def escape(x)
|
476
|
+
Utils.escape_uri(Utils.object_to_param(x))
|
477
|
+
end
|
478
|
+
|
479
|
+
def unescape(x)
|
480
|
+
Utils.unescape_uri(x)
|
481
|
+
end
|
482
|
+
|
483
|
+
end
|
484
|
+
|
485
|
+
class Label < self
|
486
|
+
|
487
|
+
SEPARATOR = '.'.freeze
|
488
|
+
PREFIX = '.'.freeze
|
489
|
+
OPERATOR = '.'.freeze
|
490
|
+
BASE_LEVEL = 3
|
491
|
+
|
492
|
+
end
|
493
|
+
|
494
|
+
class Path < self
|
495
|
+
|
496
|
+
SEPARATOR = '/'.freeze
|
497
|
+
PREFIX = '/'.freeze
|
498
|
+
OPERATOR = '/'.freeze
|
499
|
+
BASE_LEVEL = 3
|
500
|
+
|
501
|
+
end
|
502
|
+
|
503
|
+
class PathParameters < self
|
504
|
+
|
505
|
+
SEPARATOR = ';'.freeze
|
506
|
+
PREFIX = ';'.freeze
|
507
|
+
NAMED = true
|
508
|
+
PAIR_IF_EMPTY = false
|
509
|
+
OPERATOR = ';'.freeze
|
510
|
+
BASE_LEVEL = 3
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
class FormQuery < self
|
515
|
+
|
516
|
+
SEPARATOR = '&'.freeze
|
517
|
+
PREFIX = '?'.freeze
|
518
|
+
NAMED = true
|
519
|
+
OPERATOR = '?'.freeze
|
520
|
+
BASE_LEVEL = 3
|
521
|
+
|
522
|
+
end
|
523
|
+
|
524
|
+
class FormQueryContinuation < self
|
525
|
+
|
526
|
+
SEPARATOR = '&'.freeze
|
527
|
+
PREFIX = '&'.freeze
|
528
|
+
NAMED = true
|
529
|
+
OPERATOR = '&'.freeze
|
530
|
+
BASE_LEVEL = 3
|
531
|
+
|
532
|
+
end
|
533
|
+
|
534
|
+
end
|
535
|
+
|
536
|
+
# @private
|
537
|
+
OPERATORS = {
|
538
|
+
'' => Expression,
|
539
|
+
'+' => Expression::Reserved,
|
540
|
+
'#' => Expression::Fragment,
|
541
|
+
'.' => Expression::Label,
|
542
|
+
'/' => Expression::Path,
|
543
|
+
';' => Expression::PathParameters,
|
544
|
+
'?' => Expression::FormQuery,
|
545
|
+
'&' => Expression::FormQueryContinuation
|
546
|
+
}
|
547
|
+
|
548
|
+
# This error is raised when an invalid pattern was given.
|
549
|
+
class Invalid < StandardError
|
550
|
+
|
551
|
+
include URITemplate::Invalid
|
552
|
+
|
553
|
+
attr_reader :pattern, :position
|
554
|
+
|
555
|
+
def initialize(source, position)
|
556
|
+
@pattern = source
|
557
|
+
@position = position
|
558
|
+
super("Invalid expression found in #{source.inspect} at #{position}: '#{source[position..-1]}'")
|
559
|
+
end
|
560
|
+
|
561
|
+
end
|
562
|
+
|
563
|
+
class InvalidValue < StandardError
|
564
|
+
|
565
|
+
include URITemplate::InvalidValue
|
566
|
+
|
567
|
+
attr_reader :variable, :value
|
568
|
+
|
569
|
+
def initialize(variable, value)
|
570
|
+
@variable = variable
|
571
|
+
@value = value
|
572
|
+
super(generate_message())
|
573
|
+
end
|
574
|
+
protected
|
575
|
+
|
576
|
+
def generate_message()
|
577
|
+
return "The template variable " + variable.inspect + " cannot expand the given value "+ value.inspect
|
578
|
+
end
|
579
|
+
|
580
|
+
end
|
581
|
+
|
582
|
+
class InvalidValue::LengthLimitInapplicable < InvalidValue
|
583
|
+
|
584
|
+
protected
|
585
|
+
def generate_message()
|
586
|
+
return "The template variable "+variable.inspect+" has a length limit and therefore cannot expand an associative value ("+value.inspect+")."
|
587
|
+
end
|
588
|
+
|
589
|
+
end
|
590
|
+
|
591
|
+
# @private
|
592
|
+
class Tokenizer
|
593
|
+
|
594
|
+
include Enumerable
|
595
|
+
|
596
|
+
attr_reader :source
|
597
|
+
|
598
|
+
def initialize(source, ops)
|
599
|
+
@source = source
|
600
|
+
@operators = ops
|
601
|
+
end
|
602
|
+
|
603
|
+
def each
|
604
|
+
if !block_given?
|
605
|
+
return Enumerator.new(self)
|
606
|
+
end
|
607
|
+
scanner = StringScanner.new(@source)
|
608
|
+
until scanner.eos?
|
609
|
+
expression = scanner.scan(EXPRESSION)
|
610
|
+
if expression
|
611
|
+
vars = scanner[2].split(',').map{|name|
|
612
|
+
match = VAR.match(name)
|
613
|
+
# 1 = varname
|
614
|
+
# 2 = explode
|
615
|
+
# 3 = length
|
616
|
+
[ match[1], match[2] == '*', match[3].to_i ]
|
617
|
+
}
|
618
|
+
yield @operators[scanner[1]].new(vars)
|
619
|
+
else
|
620
|
+
literal = scanner.scan(LITERAL)
|
621
|
+
if literal
|
622
|
+
yield(Literal.new(literal))
|
623
|
+
else
|
624
|
+
raise Invalid.new(@source,scanner.pos)
|
625
|
+
end
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
end
|
631
|
+
|
632
|
+
# The class methods for all rfc6570 templates.
|
633
|
+
module ClassMethods
|
634
|
+
|
635
|
+
# Tries to convert the given param in to a instance of {RFC6570}
|
636
|
+
# It basically passes thru instances of that class, parses strings and return nil on everything else.
|
637
|
+
#
|
638
|
+
# @example
|
639
|
+
# URITemplate::RFC6570.try_convert( Object.new ) #=> nil
|
640
|
+
# tpl = URITemplate::RFC6570.new('{foo}')
|
641
|
+
# URITemplate::RFC6570.try_convert( tpl ) #=> tpl
|
642
|
+
# URITemplate::RFC6570.try_convert('{foo}') #=> tpl
|
643
|
+
# URITemplate::RFC6570.try_convert(URITemplate.new(:colon, ':foo')) #=> tpl
|
644
|
+
# URITemplate::RFC6570.try_convert(URITemplate.new(:draft7, '{foo}')) #=> tpl
|
645
|
+
# # Draft7 and RFC6570 handle expansion of named variables a bit differently:
|
646
|
+
# URITemplate::RFC6570.try_convert(URITemplate.new(:draft7, '{?list*}')) #=> nil
|
647
|
+
# # This pattern is invalid, so it wont be parsed:
|
648
|
+
# URITemplate::RFC6570.try_convert('{foo') #=> nil
|
649
|
+
#
|
650
|
+
def try_convert(x)
|
651
|
+
if x.class == self
|
652
|
+
return x
|
653
|
+
elsif x.kind_of? String and valid? x
|
654
|
+
return new(x)
|
655
|
+
elsif x.kind_of? URITemplate::Colon
|
656
|
+
return new( x.tokens.map{|tk|
|
657
|
+
if tk.literal?
|
658
|
+
Literal.new(tk.string)
|
659
|
+
else
|
660
|
+
Expression.new([[tk.variables.first, false, 0]])
|
661
|
+
end
|
662
|
+
})
|
663
|
+
elsif (x.class == URITemplate::Draft7 and self == URITemplate::RFC6570) or (x.class == URITemplate::RFC6570 and self == URITemplate::Draft7)
|
664
|
+
if x.tokens.none?{|t| t.class::NAMED and t.expands? }
|
665
|
+
return self.new(x.to_s)
|
666
|
+
end
|
667
|
+
else
|
668
|
+
return nil
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
# Like {.try_convert}, but raises an ArgumentError, when the conversion failed.
|
673
|
+
#
|
674
|
+
# @raise ArgumentError
|
675
|
+
def convert(x)
|
676
|
+
o = self.try_convert(x)
|
677
|
+
if o.nil?
|
678
|
+
raise ArgumentError, "Expected to receive something that can be converted to an #{self.class}, but got: #{x.inspect}."
|
679
|
+
else
|
680
|
+
return o
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
# Tests whether a given pattern is a valid template pattern.
|
685
|
+
# @example
|
686
|
+
# URITemplate::RFC6570.valid? 'foo' #=> true
|
687
|
+
# URITemplate::RFC6570.valid? '{foo}' #=> true
|
688
|
+
# URITemplate::RFC6570.valid? '{foo' #=> false
|
689
|
+
def valid?(pattern)
|
690
|
+
URI === pattern
|
691
|
+
end
|
692
|
+
|
693
|
+
end
|
694
|
+
|
695
|
+
extend ClassMethods
|
696
|
+
|
697
|
+
attr_reader :options
|
698
|
+
|
699
|
+
# @param pattern_or_tokens [String,Array] either a pattern as String or an Array of tokens
|
700
|
+
# @param options [Hash] some options
|
701
|
+
# @option :lazy [true,false] If true the pattern will be parsed on first access, this also means that syntax errors will not be detected unless accessed.
|
702
|
+
def initialize(pattern_or_tokens,options={})
|
703
|
+
@options = options.dup.freeze
|
704
|
+
if pattern_or_tokens.kind_of? String
|
705
|
+
@pattern = pattern_or_tokens.dup
|
706
|
+
@pattern.freeze
|
707
|
+
unless @options[:lazy]
|
708
|
+
self.tokens
|
709
|
+
end
|
710
|
+
elsif pattern_or_tokens.kind_of? Array
|
711
|
+
@tokens = pattern_or_tokens.dup
|
712
|
+
@tokens.freeze
|
713
|
+
else
|
714
|
+
raise ArgumentError, "Expected to receive a pattern string, but got #{pattern_or_tokens.inspect}"
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
# @method expand(variables = {})
|
719
|
+
# Expands the template with the given variables.
|
720
|
+
# The expansion should be compatible to uritemplate spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
|
721
|
+
# @note
|
722
|
+
# All keys of the supplied hash should be strings as anything else won't be recognised.
|
723
|
+
# @note
|
724
|
+
# There are neither default values for variables nor will anything be raised if a variable is missing. Please read the spec if you want to know how undefined variables are handled.
|
725
|
+
# @example
|
726
|
+
# URITemplate::RFC6570.new('{foo}').expand('foo'=>'bar') #=> 'bar'
|
727
|
+
# URITemplate::RFC6570.new('{?args*}').expand('args'=>{'key'=>'value'}) #=> '?key=value'
|
728
|
+
# URITemplate::RFC6570.new('{undef}').expand() #=> ''
|
729
|
+
#
|
730
|
+
# @param variables [Hash]
|
731
|
+
# @return String
|
732
|
+
|
733
|
+
# Compiles this template into a regular expression which can be used to test whether a given uri matches this template. This template is also used for {#===}.
|
734
|
+
#
|
735
|
+
# @example
|
736
|
+
# tpl = URITemplate::RFC6570.new('/foo/{bar}/')
|
737
|
+
# regex = tpl.to_r
|
738
|
+
# regex === '/foo/baz/' #=> true
|
739
|
+
# regex === '/foz/baz/' #=> false
|
740
|
+
#
|
741
|
+
# @return Regexp
|
742
|
+
def to_r
|
743
|
+
@regexp ||= begin
|
744
|
+
source = tokens.map(&:to_r_source)
|
745
|
+
source.unshift('\A')
|
746
|
+
source.push('\z')
|
747
|
+
Regexp.new( source.join, Utils::KCODE_UTF8)
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
# Extracts variables from a uri ( given as string ) or an instance of MatchData ( which was matched by the regexp of this template.
|
752
|
+
# The actual result depends on the value of post_processing.
|
753
|
+
# This argument specifies whether pair arrays should be converted to hashes.
|
754
|
+
#
|
755
|
+
# @example Default Processing
|
756
|
+
# URITemplate::RFC6570.new('{var}').extract('value') #=> {'var'=>'value'}
|
757
|
+
# URITemplate::RFC6570.new('{&args*}').extract('&a=1&b=2') #=> {'args'=>{'a'=>'1','b'=>'2'}}
|
758
|
+
# URITemplate::RFC6570.new('{&arg,arg}').extract('&arg=1&arg=2') #=> {'arg'=>'2'}
|
759
|
+
#
|
760
|
+
# @example No Processing
|
761
|
+
# URITemplate::RFC6570.new('{var}').extract('value', URITemplate::RFC6570::NO_PROCESSING) #=> [['var','value']]
|
762
|
+
# URITemplate::RFC6570.new('{&args*}').extract('&a=1&b=2', URITemplate::RFC6570::NO_PROCESSING) #=> [['args',[['a','1'],['b','2']]]]
|
763
|
+
# URITemplate::RFC6570.new('{&arg,arg}').extract('&arg=1&arg=2', URITemplate::RFC6570::NO_PROCESSING) #=> [['arg','1'],['arg','2']]
|
764
|
+
#
|
765
|
+
# @raise Encoding::InvalidByteSequenceError when the given uri was not properly encoded.
|
766
|
+
# @raise Encoding::UndefinedConversionError when the given uri could not be converted to utf-8.
|
767
|
+
# @raise Encoding::CompatibilityError when the given uri could not be converted to utf-8.
|
768
|
+
#
|
769
|
+
# @param uri_or_match [String,MatchData] Uri_or_MatchData A uri or a matchdata from which the variables should be extracted.
|
770
|
+
# @param post_processing [Array] Processing Specifies which processing should be done.
|
771
|
+
#
|
772
|
+
# @note
|
773
|
+
# Don't expect that an extraction can fully recover the expanded variables. Extract rather generates a variable list which should expand to the uri from which it were extracted. In general the following equation should hold true:
|
774
|
+
# a_tpl.expand( a_tpl.extract( an_uri ) ) == an_uri
|
775
|
+
#
|
776
|
+
# @example Extraction cruces
|
777
|
+
# two_lists = URITemplate::RFC6570.new('{listA*,listB*}')
|
778
|
+
# uri = two_lists.expand('listA'=>[1,2],'listB'=>[3,4]) #=> "1,2,3,4"
|
779
|
+
# variables = two_lists.extract( uri ) #=> {'listA'=>["1","2","3","4"],'listB'=>nil}
|
780
|
+
# # However, like said in the note:
|
781
|
+
# two_lists.expand( variables ) == uri #=> true
|
782
|
+
#
|
783
|
+
# @note
|
784
|
+
# The current implementation drops duplicated variables instead of checking them.
|
785
|
+
#
|
786
|
+
#
|
787
|
+
def extract(uri_or_match, post_processing = DEFAULT_PROCESSING )
|
788
|
+
if uri_or_match.kind_of? String
|
789
|
+
m = self.to_r.match(uri_or_match)
|
790
|
+
elsif uri_or_match.kind_of?(MatchData)
|
791
|
+
if uri_or_match.respond_to?(:regexp) and uri_or_match.regexp != self.to_r
|
792
|
+
raise ArgumentError, "Trying to extract variables from MatchData which was not generated by this template."
|
793
|
+
end
|
794
|
+
m = uri_or_match
|
795
|
+
elsif uri_or_match.nil?
|
796
|
+
return nil
|
797
|
+
else
|
798
|
+
raise ArgumentError, "Expected to receive a String or a MatchData, but got #{uri_or_match.inspect}."
|
799
|
+
end
|
800
|
+
if m.nil?
|
801
|
+
return nil
|
802
|
+
else
|
803
|
+
result = extract_matchdata(m, post_processing)
|
804
|
+
if block_given?
|
805
|
+
return yield result
|
806
|
+
end
|
807
|
+
|
808
|
+
return result
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
# Extracts variables without any proccessing.
|
813
|
+
# This is equivalent to {#extract} with options {NO_PROCESSING}.
|
814
|
+
# @see #extract
|
815
|
+
def extract_simple(uri_or_match)
|
816
|
+
extract( uri_or_match, NO_PROCESSING )
|
817
|
+
end
|
818
|
+
|
819
|
+
# Returns the pattern for this template.
|
820
|
+
def pattern
|
821
|
+
@pattern ||= tokens.map(&:to_s).join
|
822
|
+
end
|
823
|
+
|
824
|
+
alias to_s pattern
|
825
|
+
|
826
|
+
# Compares two template patterns.
|
827
|
+
def ==(o)
|
828
|
+
this, other, this_converted, _ = URITemplate.coerce( self, o )
|
829
|
+
if this_converted
|
830
|
+
return this == other
|
831
|
+
end
|
832
|
+
return this.pattern == other.pattern
|
833
|
+
end
|
834
|
+
|
835
|
+
# @method ===(uri)
|
836
|
+
# Alias for to_r.=== . Tests whether this template matches a given uri.
|
837
|
+
# @return TrueClass, FalseClass
|
838
|
+
def_delegators :to_r, :===
|
839
|
+
|
840
|
+
# @method match(uri)
|
841
|
+
# Alias for to_r.match . Matches this template against the given uri.
|
842
|
+
# @yield MatchData
|
843
|
+
# @return MatchData, Object
|
844
|
+
def_delegators :to_r, :match
|
845
|
+
|
846
|
+
# The type of this template.
|
847
|
+
#
|
848
|
+
# @example
|
849
|
+
# tpl1 = URITemplate::RFC6570.new('/foo')
|
850
|
+
# tpl2 = URITemplate.new( tpl1.pattern, tpl1.type )
|
851
|
+
# tpl1 == tpl2 #=> true
|
852
|
+
#
|
853
|
+
# @see {URITemplate#type}
|
854
|
+
def type
|
855
|
+
self.class::TYPE
|
856
|
+
end
|
857
|
+
|
858
|
+
# Returns the level of this template according to the draft ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07#section-1.2 ). Higher level means higher complexity.
|
859
|
+
# Basically this is defined as:
|
860
|
+
#
|
861
|
+
# * Level 1: no operators, one variable per expansion, no variable modifiers
|
862
|
+
# * Level 2: '+' and '#' operators, one variable per expansion, no variable modifiers
|
863
|
+
# * Level 3: all operators, multiple variables per expansion, no variable modifiers
|
864
|
+
# * Level 4: all operators, multiple variables per expansion, all variable modifiers
|
865
|
+
#
|
866
|
+
# @example
|
867
|
+
# URITemplate::RFC6570.new('/foo/').level #=> 1
|
868
|
+
# URITemplate::RFC6570.new('/foo{bar}').level #=> 1
|
869
|
+
# URITemplate::RFC6570.new('/foo{#bar}').level #=> 2
|
870
|
+
# URITemplate::RFC6570.new('/foo{.bar}').level #=> 3
|
871
|
+
# URITemplate::RFC6570.new('/foo{bar,baz}').level #=> 3
|
872
|
+
# URITemplate::RFC6570.new('/foo{bar:20}').level #=> 4
|
873
|
+
# URITemplate::RFC6570.new('/foo{bar*}').level #=> 4
|
874
|
+
#
|
875
|
+
# Templates of lower levels might be convertible to other formats while templates of higher levels might be incompatible. Level 1 for example should be convertible to any other format since it just contains simple expansions.
|
876
|
+
#
|
877
|
+
def level
|
878
|
+
tokens.map(&:level).max
|
879
|
+
end
|
880
|
+
|
881
|
+
# Tries to concatenate two templates, as if they were path segments.
|
882
|
+
# Removes double slashes or insert one if they are missing.
|
883
|
+
#
|
884
|
+
# @example
|
885
|
+
# tpl = URITemplate::RFC6570.new('/xy/')
|
886
|
+
# (tpl / '/z/' ).pattern #=> '/xy/z/'
|
887
|
+
# (tpl / 'z/' ).pattern #=> '/xy/z/'
|
888
|
+
# (tpl / '{/z}' ).pattern #=> '/xy{/z}'
|
889
|
+
# (tpl / 'a' / 'b' ).pattern #=> '/xy/a/b'
|
890
|
+
#
|
891
|
+
def /(o)
|
892
|
+
this, other, this_converted, _ = URITemplate.coerce( self, o )
|
893
|
+
if this_converted
|
894
|
+
return this / other
|
895
|
+
end
|
896
|
+
klass = self.class
|
897
|
+
if other.absolute?
|
898
|
+
raise ArgumentError, "Expected to receive a relative template but got an absoulte one: #{other.inspect}. If you think this is a bug, please report it."
|
899
|
+
end
|
900
|
+
|
901
|
+
if other.pattern == ''
|
902
|
+
return self
|
903
|
+
end
|
904
|
+
# Merge!
|
905
|
+
# Analyze the last token of this an the first token of the next and try to merge them
|
906
|
+
if self.tokens.last.kind_of?(klass::Literal)
|
907
|
+
if self.tokens.last.string[-1] == SLASH # the last token ends with an /
|
908
|
+
if other.tokens.first.kind_of? klass::Literal
|
909
|
+
# both seems to be paths, merge them!
|
910
|
+
if other.tokens.first.string[0] == SLASH
|
911
|
+
# strip one '/'
|
912
|
+
return self.class.new( self.tokens[0..-2] + [ klass::Literal.new(self.tokens.last.string + other.tokens.first.string[1..-1]) ] + other.tokens[1..-1] )
|
913
|
+
else
|
914
|
+
# no problem, but we can merge them
|
915
|
+
return self.class.new( self.tokens[0..-2] + [ klass::Literal.new(self.tokens.last.string + other.tokens.first.string) ] + other.tokens[1..-1] )
|
916
|
+
end
|
917
|
+
elsif other.tokens.first.kind_of? klass::Expression::Path
|
918
|
+
# this will automatically insert '/'
|
919
|
+
# so we can strip one '/'
|
920
|
+
return self.class.new( self.tokens[0..-2] + [ klass::Literal.new(self.tokens.last.string[0..-2]) ] + other.tokens )
|
921
|
+
end
|
922
|
+
elsif other.tokens.first.kind_of? klass::Literal
|
923
|
+
# okay, this template does not end with /, but the next starts with a literal => merge them!
|
924
|
+
if other.tokens.first.string[0] == SLASH
|
925
|
+
return self.class.new( self.tokens[0..-2] + [ klass::Literal.new(self.tokens.last.string + other.tokens.first.string)] + other.tokens[1..-1] )
|
926
|
+
else
|
927
|
+
return self.class.new( self.tokens[0..-2] + [ klass::Literal.new(self.tokens.last.string + '/' + other.tokens.first.string)] + other.tokens[1..-1] )
|
928
|
+
end
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
if other.tokens.first.kind_of?(klass::Literal)
|
933
|
+
if other.tokens.first.string[0] == SLASH
|
934
|
+
return self.class.new( self.tokens + other.tokens )
|
935
|
+
else
|
936
|
+
return self.class.new( self.tokens + [ klass::Literal.new('/' + other.tokens.first.string)]+ other.tokens[1..-1] )
|
937
|
+
end
|
938
|
+
elsif other.tokens.first.kind_of?(klass::Expression::Path)
|
939
|
+
return self.class.new( self.tokens + other.tokens )
|
940
|
+
else
|
941
|
+
return self.class.new( self.tokens + [ klass::Literal.new('/')] + other.tokens )
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
# Returns an array containing a the template tokens.
|
946
|
+
def tokens
|
947
|
+
@tokens ||= tokenize!
|
948
|
+
end
|
949
|
+
|
950
|
+
protected
|
951
|
+
# @private
|
952
|
+
def tokenize!
|
953
|
+
self.class::Tokenizer.new(pattern, self.class::OPERATORS).to_a
|
954
|
+
end
|
955
|
+
|
956
|
+
def arity
|
957
|
+
@arity ||= tokens.inject(0){|a,t| a + t.arity }
|
958
|
+
end
|
959
|
+
|
960
|
+
# @private
|
961
|
+
def extract_matchdata(matchdata, post_processing)
|
962
|
+
bc = 1
|
963
|
+
vars = []
|
964
|
+
tokens.each{|part|
|
965
|
+
next if part.literal?
|
966
|
+
i = 0
|
967
|
+
pa = part.arity
|
968
|
+
while i < pa
|
969
|
+
vars << part.extract(i, matchdata[bc])
|
970
|
+
bc += 1
|
971
|
+
i += 1
|
972
|
+
end
|
973
|
+
}
|
974
|
+
if post_processing.include? :convert_result
|
975
|
+
if post_processing.include? :convert_values
|
976
|
+
vars.flatten!(1)
|
977
|
+
return Hash[*vars.map!{|k,v| [k,Utils.pair_array_to_hash(v)] }.flatten(1) ]
|
978
|
+
else
|
979
|
+
vars.flatten!(2)
|
980
|
+
return Hash[*vars]
|
981
|
+
end
|
982
|
+
else
|
983
|
+
if post_processing.include? :convert_value
|
984
|
+
vars.flatten!(1)
|
985
|
+
return vars.collect{|k,v| [k,Utils.pair_array_to_hash(v)] }
|
986
|
+
else
|
987
|
+
return vars.flatten(1)
|
988
|
+
end
|
989
|
+
end
|
990
|
+
end
|
991
|
+
|
992
|
+
end
|