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