uri_template 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.1.0 - 2.11.2011
2
+ - Removed Sections. They made too many headaches.
3
+ + Made draft7 template concatenateable. This should replace sections.
4
+ * BUGFIX: multiline uris were matched
5
+ * BUGFIX: variablenames were decoded when this was not appreciated
6
+
1
7
  # 0.0.2 - 1.11.2011
2
8
  * BUGFIX: Concatenating empty sections no more leads to catch-all templates, when an emtpy template was appreciated.
3
9
  + The extracted variables now contains the keys :suffix and :prefix if the match didn't consume the whole uri.
data/lib/uri_template.rb CHANGED
@@ -18,6 +18,7 @@
18
18
  # A base module for all implementations of a uri template.
19
19
  module URITemplate
20
20
 
21
+ autoload :Utils, 'uri_template/utils'
21
22
  autoload :Draft7, 'uri_template/draft7'
22
23
 
23
24
  # A hash with all available implementations.
@@ -39,6 +40,7 @@ module URITemplate
39
40
  # URITemplate.resolve_class("template",:draft7) #=> [ URITemplate::Draft7, ["template"] ]
40
41
  #
41
42
  # @raise ArgumentError when no class was found.
43
+ #
42
44
  def self.resolve_class(*args)
43
45
  symbols, rest = args.partition{|x| x.kind_of? Symbol }
44
46
  version = symbols.fetch(0, :default)
@@ -66,45 +68,18 @@ module URITemplate
66
68
  module Invalid
67
69
  end
68
70
 
69
- # A base module for all implementation of a template section.
70
- # Sections are a custom extension to the uri template spec.
71
- # A template section ( in comparison to a template ) can be unbounded on its ends. Therefore they don't necessarily match a whole uri and can be concatenated.
72
- module Section
73
-
74
- include URITemplate
75
-
76
- # Same as {URITemplate.new} but for sections
77
- def self.new(*args)
78
- klass, rest = URITemplate.resolve_class(*args)
79
- return klass::Section.new(*rest)
80
- end
81
-
82
- # @abstract
83
- # Concatenates this section with an other section.
84
- def >>(other)
85
- raise "Please implement #>> on #{self.class.inspect}"
86
- end
87
-
88
- # @abstract
89
- # Is this section left bounded?
90
- def left_bound?
91
- raise "Please implement #left_bound? on #{self.class.inspect}"
92
- end
93
-
94
- # @abstract
95
- # Is this section right bounded?
96
- def right_bound?
97
- raise "Please implement #right_bound? on #{self.class.inspect}"
98
- end
99
-
100
- end
101
-
102
71
  # @abstract
103
72
  # Expands this uri template with the given variables.
104
73
  # The variables should be converted to strings using {Utils#object_to_param}.
105
- # @raise Unconvertable if a variable could not be converted.
74
+ # @raise {Unconvertable} if a variable could not be converted to a string.
106
75
  def expand(variables={})
107
- raise "Please implement #expand on #{self.class.inspect}"
76
+ raise "Please implement #expand on #{self.class.inspect}."
77
+ end
78
+
79
+ # @abstract
80
+ # Returns the type of this template. The type is a symbol which can be used in {.resolve_class} to resolve the type of this template.
81
+ def type
82
+ raise "Please implement #type on #{self.class.inspect}."
108
83
  end
109
84
 
110
85
  end
File without changes
File without changes
@@ -34,7 +34,7 @@ class URITemplate::Draft7
34
34
  Utils = URITemplate::Utils
35
35
 
36
36
  # @private
37
- LITERAL = /^([^"'%<>\\^`{|}\s]|%\h\h)+/
37
+ LITERAL = /^([^"'%<>\\^`{|}\s\p{Cc}]|%\h\h)+/
38
38
 
39
39
  # @private
40
40
  CHARACTER_CLASSES = {
@@ -60,19 +60,19 @@ class URITemplate::Draft7
60
60
  }
61
61
 
62
62
  # Specifies that no processing should be done upon extraction.
63
- # @see extract
63
+ # @see #extract
64
64
  NO_PROCESSING = []
65
65
 
66
66
  # Specifies that the extracted values should be processed.
67
- # @see extract
67
+ # @see #extract
68
68
  CONVERT_VALUES = [:convert_values]
69
69
 
70
70
  # Specifies that the extracted variable list should be processed.
71
- # @see extract
71
+ # @see #extract
72
72
  CONVERT_RESULT = [:convert_result]
73
73
 
74
74
  # Default processing. Means: convert values and the list itself.
75
- # @see extract
75
+ # @see #extract
76
76
  DEFAULT_PROCESSING = CONVERT_VALUES + CONVERT_RESULT
77
77
 
78
78
  # @private
@@ -99,11 +99,15 @@ __REGEXP__
99
99
  (?<varchar> [a-zA-Z_]|%[0-9a-fA-F]{2}){0}
100
100
  (?<varname> \g<varchar>(?:\g<varchar>|\.)*){0}
101
101
  (?<varspec> \g<varname>\*?(?::\d+)?){0}
102
- ^(([^"'%<>^`{|}\s]|%\h\h)+|\{\g<operator>(?<vars>\g<varspec>(?:,\g<varspec>)*)\})*$
102
+ ^(([^"'%<>^`{|}\s\p{Cc}]|%\h\h)+|\{\g<operator>(?<vars>\g<varspec>(?:,\g<varspec>)*)\})*$
103
103
  __REGEXP__
104
104
 
105
105
  # @private
106
- class Literal
106
+ class Token
107
+ end
108
+
109
+ # @private
110
+ class Literal < Token
107
111
 
108
112
  attr_reader :string
109
113
 
@@ -115,6 +119,10 @@ __REGEXP__
115
119
  0
116
120
  end
117
121
 
122
+ def level
123
+ 1
124
+ end
125
+
118
126
  def expand(*_)
119
127
  return @string
120
128
  end
@@ -128,58 +136,9 @@ __REGEXP__
128
136
  end
129
137
 
130
138
  end
131
-
132
- # @private
133
- class Terminal
134
- def expand(*_)
135
- ''
136
- end
137
-
138
- def size
139
- 0
140
- end
141
- end
142
-
143
- # @private
144
- class LeftBound < Terminal
145
-
146
- def to_r_source(*_)
147
- '^'
148
- end
149
-
150
- def to_s
151
- ''
152
- end
153
-
154
- end
155
-
156
- # @private
157
- class RightBound < Terminal
158
-
159
- def to_r_source(*_)
160
- '$'
161
- end
162
-
163
- def to_s
164
- ''
165
- end
166
-
167
- end
168
-
169
- # @private
170
- class Open < Terminal
171
-
172
- def to_r_source(*_)
173
- ''
174
- end
175
-
176
- def to_s
177
- "\u2026"
178
- end
179
- end
180
-
139
+
181
140
  # @private
182
- class Expression
141
+ class Expression < Token
183
142
 
184
143
  attr_reader :variables, :max_length
185
144
 
@@ -192,6 +151,7 @@ __REGEXP__
192
151
  PAIR_CONNECTOR = '='.freeze
193
152
  PAIR_IF_EMPTY = true
194
153
  LIST_CONNECTOR = ','.freeze
154
+ BASE_LEVEL = 1
195
155
 
196
156
  CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
197
157
 
@@ -202,6 +162,18 @@ __REGEXP__
202
162
  @variables.size
203
163
  end
204
164
 
165
+ def level
166
+ if @variables.none?{|_,expand,ml| expand || (ml > 0) }
167
+ if @variables.size == 1
168
+ return self.class::BASE_LEVEL
169
+ else
170
+ return 3
171
+ end
172
+ else
173
+ return 4
174
+ end
175
+ end
176
+
205
177
  def expand( vars, options )
206
178
  result = []
207
179
  variables.each{| var, expand , max_length |
@@ -243,7 +215,7 @@ __REGEXP__
243
215
  value = "(?:\\g<#{self.class::CHARACTER_CLASS[:class_name]}>|,)#{(max_length > 0)?'{,'+max_length.to_s+'}':'*'}"
244
216
  if expand
245
217
  #if self.class::PAIR_IF_EMPTY
246
- pair = "\\g<c_vn_>(?:#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})?"
218
+ pair = "(?:\\g<c_vn_>#{Regexp.escape(self.class::PAIR_CONNECTOR)})?#{value}"
247
219
 
248
220
  if first
249
221
  source << "(?<v#{base_counter + i}>(?:#{pair})(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*)"
@@ -274,7 +246,7 @@ __REGEXP__
274
246
  # could be list or map, too
275
247
  value = "\\g<#{self.class::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*'}"
276
248
 
277
- pair = "\\g<c_vn_>(?:#{Regexp.escape(self.class::PAIR_CONNECTOR)}#{value})?"
249
+ pair = "(?:\\g<c_vn_>#{Regexp.escape(self.class::PAIR_CONNECTOR)})?#{value}"
278
250
 
279
251
  value = "#{pair}(?:#{Regexp.escape(self.class::SEPARATOR)}#{pair})*"
280
252
  elsif last
@@ -323,12 +295,12 @@ __REGEXP__
323
295
  found_value = true
324
296
  splitted << [ match['name'][0..-2], decode(match['value'] + rest , false) ]
325
297
  else
326
- splitted << [ decode(match['value'] + rest , false), nil ]
298
+ splitted << [ match['value'] + rest, nil ]
327
299
  end
328
300
  rest = match.post_match
329
301
  end
330
302
  if !found_value
331
- return [ [ name, splitted.map{|n,v| v || n } ] ]
303
+ return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
332
304
  else
333
305
  return [ [ name, splitted ] ]
334
306
  end
@@ -433,6 +405,7 @@ __REGEXP__
433
405
 
434
406
  CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
435
407
  OPERATOR = '+'.freeze
408
+ BASE_LEVEL = 2
436
409
 
437
410
  end
438
411
 
@@ -441,6 +414,7 @@ __REGEXP__
441
414
  CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
442
415
  PREFIX = '#'.freeze
443
416
  OPERATOR = '#'.freeze
417
+ BASE_LEVEL = 2
444
418
 
445
419
  end
446
420
 
@@ -449,6 +423,7 @@ __REGEXP__
449
423
  SEPARATOR = '.'.freeze
450
424
  PREFIX = '.'.freeze
451
425
  OPERATOR = '.'.freeze
426
+ BASE_LEVEL = 3
452
427
 
453
428
  end
454
429
 
@@ -457,6 +432,7 @@ __REGEXP__
457
432
  SEPARATOR = '/'.freeze
458
433
  PREFIX = '/'.freeze
459
434
  OPERATOR = '/'.freeze
435
+ BASE_LEVEL = 3
460
436
 
461
437
  end
462
438
 
@@ -467,6 +443,7 @@ __REGEXP__
467
443
  NAMED = true
468
444
  PAIR_IF_EMPTY = false
469
445
  OPERATOR = ';'.freeze
446
+ BASE_LEVEL = 3
470
447
 
471
448
  end
472
449
 
@@ -476,6 +453,7 @@ __REGEXP__
476
453
  PREFIX = '?'.freeze
477
454
  NAMED = true
478
455
  OPERATOR = '?'.freeze
456
+ BASE_LEVEL = 3
479
457
 
480
458
  end
481
459
 
@@ -485,6 +463,7 @@ __REGEXP__
485
463
  PREFIX = '&'.freeze
486
464
  NAMED = true
487
465
  OPERATOR = '&'.freeze
466
+ BASE_LEVEL = 3
488
467
 
489
468
  end
490
469
 
@@ -559,6 +538,7 @@ __REGEXP__
559
538
 
560
539
  # Tries to convert the given param in to a instance of {Draft7}
561
540
  # It basically passes thru instances of that class, parses strings and return nil on everything else.
541
+ #
562
542
  # @example
563
543
  # URITemplate::Draft7.try_convert( Object.new ) #=> nil
564
544
  # tpl = URITemplate::Draft7.new('{foo}')
@@ -566,6 +546,7 @@ __REGEXP__
566
546
  # URITemplate::Draft7.try_convert('{foo}') #=> tpl
567
547
  # # This pattern is invalid, so it wont be parsed:
568
548
  # URITemplate::Draft7.try_convert('{foo') #=> nil
549
+ #
569
550
  def try_convert(x)
570
551
  if x.kind_of? self
571
552
  return x
@@ -576,6 +557,19 @@ __REGEXP__
576
557
  end
577
558
  end
578
559
 
560
+
561
+ # Like {.try_convert}, but raises an ArgumentError, when the conversion failed.
562
+ #
563
+ # @raise ArgumentError
564
+ def convert(x)
565
+ o = self.try_convert(x)
566
+ if o.nil?
567
+ raise ArgumentError, "Expected to receive something that can be converted to an #{self.class}, but got: #{x.inspect}."
568
+ else
569
+ return o
570
+ end
571
+ end
572
+
579
573
  # Tests whether a given pattern is a valid template pattern.
580
574
  # @example
581
575
  # URITemplate::Draft7.valid? 'foo' #=> true
@@ -663,28 +657,49 @@ __REGEXP__
663
657
  def to_r
664
658
  classes = CHARACTER_CLASSES.map{|_,v| v[:class]+"{0}\n" }
665
659
  bc = 0
666
- @regexp ||= Regexp.new(classes.join + tokens.map{|part|
660
+ @regexp ||= Regexp.new(classes.join + '\A' + tokens.map{|part|
667
661
  r = part.to_r_source(bc)
668
662
  bc += part.size
669
663
  r
670
- }.join, Regexp::EXTENDED)
664
+ }.join + '\z' , Regexp::EXTENDED)
671
665
  end
672
666
 
673
667
 
674
668
  # Extracts variables from a uri ( given as string ) or an instance of MatchData ( which was matched by the regexp of this template.
675
- # The actual result depends on the value of @p post_processing.
669
+ # The actual result depends on the value of post_processing.
676
670
  # This argument specifies whether pair arrays should be converted to hashes.
677
671
  #
678
- # @example
672
+ # @example Default Processing
679
673
  # URITemplate::Draft7.new('{var}').extract('value') #=> {'var'=>'value'}
680
674
  # URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2') #=> {'args'=>{'a'=>'1','b'=>'2'}}
681
675
  # URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2') #=> {'arg'=>'2'}
682
676
  #
683
- # @example
677
+ # @example No Processing
684
678
  # URITemplate::Draft7.new('{var}').extract('value', URITemplate::Draft7::NO_PROCESSING) #=> [['var','value']]
685
679
  # URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2', URITemplate::Draft7::NO_PROCESSING) #=> [['args',[['a','1'],['b','2']]]]
686
680
  # URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2', URITemplate::Draft7::NO_PROCESSING) #=> [['arg','1'],['arg','2']]
687
681
  #
682
+ # @raise Encoding::InvalidByteSequenceError when the given uri was not properly encoded.
683
+ # @raise Encoding::UndefinedConversionError when the given uri could not be converted to utf-8.
684
+ # @raise Encoding::CompatibilityError when the given uri could not be converted to utf-8.
685
+ #
686
+ # @param [String,MatchData] Uri_or_MatchData A uri or a matchdata from which the variables should be extracted.
687
+ # @param [Array] Processing Specifies which processing should be done.
688
+ #
689
+ # @note
690
+ # 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:
691
+ # a_tpl.expand( a_tpl.extract( an_uri ) ) == an_uri
692
+ #
693
+ # @example Extraction cruces
694
+ # two_lists = URITemplate::Draft7.new('{listA*,listB*}')
695
+ # uri = two_lists.expand('listA'=>[1,2],'listB'=>[3,4]) #=> "1,2,3,4"
696
+ # variables = two_lists.extract( uri ) #=> {'listA'=>["1","2","3","4"],'listB'=>nil}
697
+ # # However, like said in the note:
698
+ # two_lists.expand( variables ) == uri #=> true
699
+ #
700
+ # @note
701
+ # The current implementation drops duplicated variables instead of checking them.
702
+ #
688
703
  #
689
704
  def extract(uri_or_match, post_processing = DEFAULT_PROCESSING )
690
705
  if uri_or_match.kind_of? String
@@ -703,12 +718,6 @@ __REGEXP__
703
718
  return nil
704
719
  else
705
720
  result = extract_matchdata(m)
706
- if m.pre_match and m.pre_match.size > 0
707
- result.unshift( [:prefix, m.pre_match] )
708
- end
709
- if m.post_match and m.post_match.size > 0
710
- result.push( [:suffix, m.post_match] )
711
- end
712
721
  if post_processing.include? :convert_values
713
722
  result.map!{|k,v| [k, Utils.pair_array_to_hash(v)] }
714
723
  end
@@ -738,140 +747,158 @@ __REGEXP__
738
747
  end
739
748
 
740
749
  alias to_s pattern
750
+
751
+ # Compares two template patterns.
752
+ def ==(tpl)
753
+ return false if self.class != tpl.class
754
+ return self.pattern == tpl.pattern
755
+ end
756
+
757
+ # @method ===(uri)
758
+ # Alias for to_r.=== . Tests whether this template matches a given uri.
759
+ # @return TrueClass, FalseClass
760
+ def_delegators :to_r, :===
761
+
762
+ # @method match(uri)
763
+ # Alias for to_r.match . Matches this template against the given uri.
764
+ # @yield MatchData
765
+ # @return MatchData, Object
766
+ def_delegators :to_r, :match
741
767
 
742
- # Sections are a custom extension to the uri template spec.
743
- # A template section ( in comparison to a template ) can be unbounded on its ends. Therefore they don't necessarily match a whole uri and can be concatenated.
744
- # Unboundedness is denoted with unicode character \u2026 ( … ).
745
- #
746
- # @example
747
- # prefix = URITemplate::Draft7::Section.new('/prefix…')
748
- # template = URITemplate::Draft7.new('/prefix')
749
- # prefix === '/prefix/something completly different' #=> true
750
- # template === '/prefix/something completly different' #=> false
751
- # prefix.to_r.match('/prefix/something completly different').post_match #=> '/something completly different'
752
- #
768
+ # The type of this template.
769
+ #
753
770
  # @example
754
- # prefix = URITemplate::Draft7::Section.new('/prefix…')
755
- # tpl = prefix >> '…/end'
756
- # tpl.pattern #=> '/prefix/end'
757
- #
758
- # This behavior is usefull for building routers:
771
+ # tpl1 = URITemplate::Draft7.new('/foo')
772
+ # tpl2 = URITemplate.new( tpl1.pattern, tpl1.type )
773
+ # tpl1 == tpl2 #=> true
774
+ #
775
+ # @see {URITemplate#type}
776
+ def type
777
+ :draft7
778
+ end
779
+
780
+ # 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.
781
+ # Basically this is defined as:
759
782
  #
783
+ # * Level 1: no operators, one variable per expansion, no variable modifiers
784
+ # * Level 2: '+' and '#' operators, one variable per expansion, no variable modifiers
785
+ # * Level 3: all operators, multiple variables per expansion, no variable modifiers
786
+ # * Level 4: all operators, multiple variables per expansion, all variable modifiers
787
+ #
760
788
  # @example
761
- #
762
- # def route( uri )
763
- # prefixes = [
764
- # [ 'app_a/…' , lambda{|vars, rest| "app_a: #{rest} with #{vars.inspect}" } ],
765
- # [ 'app_b/{x}/…', lambda{|vars, rest| "app_b: #{rest} with #{vars.inspect}" } ]
766
- # ]
767
- # prefixes.each do |tpl, lb|
768
- # tpl = URITemplate::Draft7::Section.new(tpl)
769
- # tpl.match(uri) do |match_data|
770
- # return lb.call(tpl.extract(match_data), match_data.post_match)
771
- # end
772
- # end
773
- # return "not found"
774
- # end
775
- # route( 'app_a/do_something' ) #=> "app_a: do_something with {:suffix=>\"do_something\"}"
776
- # route( 'app_b/1337/something_else' ) #=> "app_b: something_else with {\"x\"=>\"1337\", :suffix=>\"something_else\"}"
777
- # route( 'bla' ) #=> 'not found'
778
- #
779
- class Section < self
780
-
781
- include URITemplate::Section
789
+ # URITemplate::Draft7.new('/foo/').level #=> 1
790
+ # URITemplate::Draft7.new('/foo{bar}').level #=> 1
791
+ # URITemplate::Draft7.new('/foo{#bar}').level #=> 2
792
+ # URITemplate::Draft7.new('/foo{.bar}').level #=> 3
793
+ # URITemplate::Draft7.new('/foo{bar,baz}').level #=> 3
794
+ # URITemplate::Draft7.new('/foo{bar:20}').level #=> 4
795
+ # URITemplate::Draft7.new('/foo{bar*}').level #=> 4
796
+ #
797
+ # 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.
798
+ #
799
+ def level
800
+ tokens.map(&:level).max
801
+ end
782
802
 
783
- # The ellipsis character.
784
- ELLIPSIS = "\u2026".freeze
803
+ # Tries to conatenate two templates, as if they were path segments.
804
+ # Removes double slashes or insert one if they are missing.
805
+ #
806
+ # @example
807
+ # tpl = URITemplate::Draft7.new('/xy/')
808
+ # (tpl / '/z/' ).pattern #=> '/xy/z/'
809
+ # (tpl / 'z/' ).pattern #=> '/xy/z/'
810
+ # (tpl / '{/z}' ).pattern #=> '/xy{/z}'
811
+ # (tpl / 'a' / 'b' ).pattern #=> '/xy/a/b'
812
+ #
813
+ def /(o)
814
+ other = self.class.convert(o)
785
815
 
786
- # Is this section left bounded?
787
- def left_bound?
788
- tokens.first.kind_of? LeftBound
816
+ if other.absolute?
817
+ 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."
789
818
  end
790
819
 
791
- # Is this section right bounded?
792
- def right_bound?
793
- tokens.last.kind_of? RightBound
820
+ if other.pattern == ''
821
+ return self
794
822
  end
795
-
796
- # Concatenates this section with anything that can be coerced into a section.
797
- #
798
- # @example
799
- # sect = URITemplate::Draft7::Section.new('/prefix…')
800
- # sect >> '…/mid…' >> '…/end' #=> URITemplate::Draft7::Section.new('/prefix/mid/end')
801
- #
802
- # @example
803
- # sect = URITemplate::Draft7::Section.new('/prefix…')
804
- # sect >> '……' #=> sect
805
- #
806
- # @example Just ellipsis has a special syntax to allow empty matches.
807
- # sect = URITemplate::Draft7::Section.new('…')
808
- # combo = sect >> '…'
809
- # combo.pattern #=> ""
810
- # combo === "" #=> true
811
- # combo === "/foo" #=> false
812
- #
813
- # @return Section
814
- def >>(other)
815
- o = self.class.try_convert(other)
816
- if o.kind_of? Section
817
- if !self.right_bound?
818
- if o.pattern == ELLIPSIS
819
- return self.class.new("", o.options)
820
- elsif !o.left_bound?
821
- #if self.tokens.size == 1
822
- # return o
823
- #end
824
- tkns = self.tokens[0..-2] + o.tokens[1..-1]
825
- unless tkns.first.kind_of? Terminal
826
- tkns.unshift(LeftBound.new)
827
- end
828
- unless tkns.last.kind_of? Terminal
829
- tkns.push(RightBound.new)
830
- end
831
- return self.class.new(tkns, o.options)
823
+ # Merge!
824
+ # Analyze the last token of this an the first token of the next and try to merge them
825
+ if self.tokens.last.kind_of?(Literal)
826
+ if self.tokens.last.string[-1] == '/' # the last token ends with an /
827
+ if other.tokens.first.kind_of? Literal
828
+ # both seems to be paths, merge them!
829
+ if other.tokens.first.string[0] == '/'
830
+ # strip one '/'
831
+ return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string[1..-1]) ] + other.tokens[1..-1] )
832
+ else
833
+ # no problem, but we can merge them
834
+ return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string) ] + other.tokens[1..-1] )
832
835
  end
836
+ elsif other.tokens.first.kind_of? Expression::Path
837
+ # this will automatically insert '/'
838
+ # so we can strip one '/'
839
+ return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string[0..-2]) ] + other.tokens )
833
840
  end
834
- raise ArgumentError, "Expected something that could be converted to a URITemplate section, but got #{other.inspect}"
835
841
  end
836
842
  end
843
+ if other.tokens.first.kind_of?(Expression::Path) or (other.tokens.first.kind_of?(Literal) and other.tokens.first.string[0] == '/')
844
+ return self.class.new( self.tokens + other.tokens )
845
+ else
846
+ return self.class.new( self.tokens + [Literal.new('/')] + other.tokens )
847
+ end
848
+ end
849
+
850
+
851
+ #
852
+ # should be relative:
853
+ # xxx ...
854
+ # {xxx}x ...
855
+ #
856
+ # should not be relative:
857
+ # {proto}:// ...
858
+ # http:// ...
859
+ # http{ssl}:// ...
860
+ #
861
+ def absolute?
862
+ read_chars = ""
837
863
 
838
- protected
839
- # @private
840
- def tokenize!
841
- pat = pattern
842
- if pat == ELLIPSIS # just ellipsis
843
- return [LeftBound.new, Open.new]
844
- else
845
- lb = (pat[0] != ELLIPSIS)
846
- rb = (pat[-1] != ELLIPSIS)
864
+ tokens.each do |token|
865
+ if token.kind_of? Expression
866
+ if token.class::OPERATOR == ''
867
+ read_chars << "x"
868
+ else
869
+ return false
870
+ end
871
+ elsif token.kind_of? Literal
872
+ read_chars << token.string
873
+ end
874
+ if read_chars =~ /^[a-z]+:\/\//i
875
+ return true
876
+ elsif read_chars =~ /(?<!:|\/)\/(?!\/)/
877
+ return false
847
878
  end
848
- pat = pat[ (lb ? 0 : 1)..(rb ? -1 : -2) ]
849
- [lb ? LeftBound.new : Open.new] + Tokenizer.new(pat).to_a + [rb ? RightBound.new : Open.new]
850
879
  end
851
-
880
+
881
+ return false
852
882
  end
853
883
 
854
- # Compares two template patterns.
855
- def ==(tpl)
856
- return false if self.class != tpl.class
857
- return self.pattern == tpl.pattern
884
+ # Returns the number of static characters in this template.
885
+ # This method is useful for routing, since it's often pointful to use the url with fewer variable characters.
886
+ # For example 'static' and 'sta{var}' both match 'static', but in most cases 'static' should be prefered over 'sta{var}' since it's more specific.
887
+ #
888
+ # @example
889
+ # URITemplate::Draft7.new('/xy/').static_characters #=> 4
890
+ # URITemplate::Draft7.new('{foo}').static_characters #=> 0
891
+ # URITemplate::Draft7.new('a{foo}b').static_characters #=> 2
892
+ #
893
+ # @return Numeric
894
+ def static_characters
895
+ @static_characters ||= tokens.select{|t| t.kind_of?(Literal) }.map{|t| t.string.size }.inject(0,:+)
858
896
  end
859
-
860
- # @method ===(uri)
861
- # Alias for to_r.=== . Tests whether this template matches a given uri.
862
- # @return TrueClass, FalseClass
863
- def_delegators :to_r, :===
864
-
865
- # @method match(uri)
866
- # Alias for to_r.match . Matches this template against the given uri.
867
- # @yield MatchData
868
- # @return MatchData, Object
869
- def_delegators :to_r, :match
870
897
 
871
898
  protected
872
899
  # @private
873
900
  def tokenize!
874
- [LeftBound.new] + Tokenizer.new(pattern).to_a + [RightBound.new]
901
+ Tokenizer.new(pattern).to_a
875
902
  end
876
903
 
877
904
  def tokens
data/uri_template.gemspec CHANGED
@@ -1,12 +1,12 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'uri_template'
3
- s.version = '0.0.2'
4
- s.date = '2011-11-01'
3
+ s.version = '0.1.0'
4
+ s.date = '2011-11-02'
5
5
  s.authors = ["HannesG"]
6
6
  s.email = %q{hannes.georg@googlemail.com}
7
7
  s.summary = 'A templating system for URIs.'
8
8
  s.homepage = 'http://github.com/hannesg/uri_template'
9
- s.description = 'A templating system for URIs, which implements http://tools.ietf.org/html/draft-gregorio-uritemplate-07 . An implementation of an older version of that spec is known as addressable. This system however is intended to be extended when newer specs evolve. For now only draft 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma regexp.'
9
+ s.description = 'A templating system for URIs, which implements http://tools.ietf.org/html/draft-gregorio-uritemplate-07 . An implementation of an older version of that spec is known as addressable. This gem however is intended to be extended when newer specs evolve. For now only draft 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma regexp.'
10
10
 
11
11
  s.require_paths = ['lib']
12
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uri_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-01 00:00:00.000000000Z
12
+ date: 2011-11-02 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &22417520 !ruby/object:Gem::Requirement
16
+ requirement: &14484700 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *22417520
24
+ version_requirements: *14484700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: yard
27
- requirement: &22415680 !ruby/object:Gem::Requirement
27
+ requirement: &14482080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,18 +32,19 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *22415680
35
+ version_requirements: *14482080
36
36
  description: ! 'A templating system for URIs, which implements http://tools.ietf.org/html/draft-gregorio-uritemplate-07
37
37
  . An implementation of an older version of that spec is known as addressable. This
38
- system however is intended to be extended when newer specs evolve. For now only
39
- draft 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma
40
- regexp.'
38
+ gem however is intended to be extended when newer specs evolve. For now only draft
39
+ 7 is supported. Downside: only for 1.9 compatible since it uses Oniguruma regexp.'
41
40
  email: hannes.georg@googlemail.com
42
41
  executables: []
43
42
  extensions: []
44
43
  extra_rdoc_files: []
45
44
  files:
46
45
  - lib/uri_template.rb
46
+ - lib/uri_template/colon.rb
47
+ - lib/uri_template/draft2.rb
47
48
  - lib/uri_template/draft7.rb
48
49
  - lib/uri_template/utils.rb
49
50
  - uri_template.gemspec