uri_template 0.1.1 → 0.1.2

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.2 - 10.11.2011
2
+ + added a new template-type: Colon
3
+ this should allow (some day) to rails-like routing tables
4
+ + made the tokens-method mandatory and added two interfaces for tokens.
5
+ this allows cross-type features like variable anaylisis
6
+
1
7
  # 0.1.1 - 4.11.2011
2
8
  + added a bunch of useful helper methods
3
9
 
@@ -0,0 +1,153 @@
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 by Hannes Georg
16
+ #
17
+
18
+ require 'forwardable'
19
+
20
+ require 'uri_template'
21
+ require 'uri_template/utils'
22
+
23
+ # A colon based template denotes variables with a colon.
24
+ # This template type is realy basic but having just on template type was a bit weird.
25
+ module URITemplate
26
+
27
+ class Colon
28
+
29
+ include URITemplate
30
+
31
+ VAR = /(?:\{:(?<name>[a-z]+)\}|:(?<name>[a-z]+)(?![a-z]))/
32
+
33
+ class Token
34
+
35
+ class Variable < self
36
+
37
+ include URITemplate::Expression
38
+
39
+ attr_reader :name
40
+
41
+ def initialize(name)
42
+ @name = name
43
+ @variables = [name]
44
+ end
45
+
46
+ def expand(vars)
47
+ return Utils.pct(vars[@name])
48
+ end
49
+
50
+ def to_r
51
+ return ['(?<', name, '>[^/]*?)'].join
52
+ end
53
+
54
+ end
55
+
56
+ class Static < self
57
+
58
+ include URITemplate::Literal
59
+
60
+ def initialize(str)
61
+ @string = str
62
+ end
63
+
64
+ def expand(_)
65
+ return @string
66
+ end
67
+
68
+ def to_r
69
+ return Regexp.escape(@string)
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ attr_reader :pattern
77
+
78
+ # Tries to convert the value into a colon-template.
79
+ # @example
80
+ # URITemplate::Colon.try_convert('/foo/:bar/').pattern #=> '/foo/:bar/'
81
+ # URITemplate::Colon.try_convert(URITemplate::Draft7.new('/foo/{bar}/')).pattern #=> '/foo/{:bar}/'
82
+ def self.try_convert(x)
83
+ if x.kind_of? String
84
+ return new(x)
85
+ elsif x.kind_of? self
86
+ return x
87
+ elsif x.kind_of? URITemplate::Draft7 and x.level == 1
88
+ return new( x.pattern.gsub(/\{(.*?)\}/){ "{:#{$1}}" } )
89
+ else
90
+ return nil
91
+ end
92
+ end
93
+
94
+ def initialize(pattern)
95
+ @pattern = pattern
96
+ end
97
+
98
+ # Extracts variables from an uri.
99
+ #
100
+ # @param String uri
101
+ # @return nil,Hash
102
+ def extract(uri)
103
+ return self.to_r.match(uri) do |md|
104
+ Hash[ *self.variables.map{|v|
105
+ [v, Utils.dpct(md[v])]
106
+ }.flatten(1) ]
107
+ end
108
+ end
109
+
110
+ def type
111
+ :colon
112
+ end
113
+
114
+ def to_r
115
+ @regexp ||= Regexp.new('\A' + tokens.map(&:to_r).join + '\z')
116
+ end
117
+
118
+ def tokens
119
+ @tokens ||= tokenize!
120
+ end
121
+
122
+ # Tries to concatenate two templates, as if they were path segments.
123
+ # Removes double slashes or inserts one if they are missing.
124
+ #
125
+ # @example
126
+ # tpl = URITemplate::Colon.new('/xy/')
127
+ # (tpl / '/z/' ).pattern #=> '/xy/z/'
128
+ # (tpl / 'z/' ).pattern #=> '/xy/z/'
129
+ # (tpl / ':z' ).pattern #=> '/xy/:z'
130
+ # (tpl / ':a' / 'b' ).pattern #=> '/xy/:a/b'
131
+ #
132
+ def /(o)
133
+ this, other, this_converted, other_converted = URITemplate.coerce( self, o )
134
+ if this_converted
135
+ return this / other
136
+ end
137
+ return self.class.new( File.join( this.pattern, other.pattern ) )
138
+ end
139
+
140
+ protected
141
+
142
+ def tokenize!
143
+ RegexpEnumerator.new(VAR).each(@pattern).map{|x|
144
+ if x.kind_of? String
145
+ Token::Static.new(x)
146
+ else
147
+ Token::Variable.new(x['name'])
148
+ end
149
+ }.to_a
150
+ end
151
+
152
+ end
153
+ end
@@ -109,22 +109,18 @@ __REGEXP__
109
109
  # @private
110
110
  class Literal < Token
111
111
 
112
- attr_reader :string
113
-
112
+ include URITemplate::Literal
113
+
114
114
  def initialize(string)
115
115
  @string = string
116
116
  end
117
117
 
118
- def size
119
- 0
120
- end
121
-
122
118
  def level
123
119
  1
124
120
  end
125
121
 
126
- def expand(*_)
127
- return @string
122
+ def arity
123
+ 0
128
124
  end
129
125
 
130
126
  def to_r_source(*_)
@@ -139,11 +135,15 @@ __REGEXP__
139
135
 
140
136
  # @private
141
137
  class Expression < Token
138
+
139
+ include URITemplate::Expression
142
140
 
143
141
  attr_reader :variables, :max_length
144
142
 
145
143
  def initialize(vars)
146
- @variables = vars
144
+ @variable_specs = vars
145
+ @variables = vars.map(&:first)
146
+ @variables.uniq!
147
147
  end
148
148
 
149
149
  PREFIX = ''.freeze
@@ -158,13 +158,9 @@ __REGEXP__
158
158
  NAMED = false
159
159
  OPERATOR = ''
160
160
 
161
- def size
162
- @variables.size
163
- end
164
-
165
161
  def level
166
- if @variables.none?{|_,expand,ml| expand || (ml > 0) }
167
- if @variables.size == 1
162
+ if @variable_specs.none?{|_,expand,ml| expand || (ml > 0) }
163
+ if @variable_specs.size == 1
168
164
  return self.class::BASE_LEVEL
169
165
  else
170
166
  return 3
@@ -174,9 +170,13 @@ __REGEXP__
174
170
  end
175
171
  end
176
172
 
177
- def expand( vars, options )
173
+ def arity
174
+ @variable_specs.size
175
+ end
176
+
177
+ def expand( vars )
178
178
  result = []
179
- variables.each{| var, expand , max_length |
179
+ @variable_specs.each{| var, expand , max_length |
180
180
  unless vars[var].nil?
181
181
  if vars[var].kind_of? Hash
182
182
  result.push( *transform_hash(var, vars[var], expand, max_length) )
@@ -199,7 +199,7 @@ __REGEXP__
199
199
  end
200
200
 
201
201
  def to_s
202
- '{' + self.class::OPERATOR + @variables.map{|name,expand,max_length| name +(expand ? '*': '') + (max_length > 0 ? ':'+max_length.to_s : '') }.join(',') + '}'
202
+ '{' + self.class::OPERATOR + @variable_specs.map{|name,expand,max_length| name +(expand ? '*': '') + (max_length > 0 ? ':'+max_length.to_s : '') }.join(',') + '}'
203
203
  end
204
204
 
205
205
  #TODO: certain things after a slurpy variable will never get matched. therefore, it's pointless to add expressions for them
@@ -207,10 +207,10 @@ __REGEXP__
207
207
  def to_r_source(base_counter = 0)
208
208
  source = []
209
209
  first = true
210
- vs = variables.size - 1
210
+ vs = @variable_specs.size - 1
211
211
  i = 0
212
212
  if self.class::NAMED
213
- variables.each{| var, expand , max_length |
213
+ @variable_specs.each{| var, expand , max_length |
214
214
  last = (vs == i)
215
215
  value = "(?:\\g<#{self.class::CHARACTER_CLASS[:class_name]}>|,)#{(max_length > 0)?'{,'+max_length.to_s+'}':'*'}"
216
216
  if expand
@@ -240,7 +240,7 @@ __REGEXP__
240
240
  i = i+1
241
241
  }
242
242
  else
243
- variables.each{| var, expand , max_length |
243
+ @variable_specs.each{| var, expand , max_length |
244
244
  last = (vs == i)
245
245
  if expand
246
246
  # could be list or map, too
@@ -272,12 +272,12 @@ __REGEXP__
272
272
  end
273
273
 
274
274
  def extract(position,matched)
275
- name, expand, max_length = @variables[position]
275
+ name, expand, max_length = @variable_specs[position]
276
276
  if matched.nil?
277
277
  return [[ name , matched ]]
278
278
  end
279
279
  if expand
280
- ex = self.hash_extractor(max_length)
280
+ ex = self.class.hash_extractor(max_length)
281
281
  rest = matched
282
282
  splitted = []
283
283
  found_value = false
@@ -310,22 +310,25 @@ __REGEXP__
310
310
 
311
311
  return [ [ name, decode( matched ) ] ]
312
312
  end
313
-
314
- def variable_names
315
- @variables.collect(&:first)
316
- end
317
313
 
318
314
  protected
319
315
 
320
- def hash_extractor(max_length)
321
- value = "\\g<#{self.class::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
322
-
323
- pair = "(?<name>\\g<c_vn_>#{Regexp.escape(self.class::PAIR_CONNECTOR)})?(?<value>#{value})"
316
+ module ClassMethods
324
317
 
325
- return Regexp.new( CHARACTER_CLASSES[:varname][:class] + "{0}\n" + self.class::CHARACTER_CLASS[:class] + "{0}\n" + "^#{Regexp.escape(self.class::SEPARATOR)}?" + pair + "(?<rest>$|#{Regexp.escape(self.class::SEPARATOR)}(?!#{Regexp.escape(self.class::SEPARATOR)}))" ,Regexp::EXTENDED)
318
+ def hash_extractor(max_length)
319
+ @hash_extractors ||= {}
320
+ @hash_extractors[max_length] ||= begin
321
+ value = "\\g<#{self::CHARACTER_CLASS[:class_name]}>#{(max_length > 0)?'{,'+max_length.to_s+'}':'*?'}"
322
+ pair = "(?<name>\\g<c_vn_>#{Regexp.escape(self::PAIR_CONNECTOR)})?(?<value>#{value})"
323
+
324
+ Regexp.new( CHARACTER_CLASSES[:varname][:class] + "{0}\n" + self::CHARACTER_CLASS[:class] + "{0}\n" + "^#{Regexp.escape(self::SEPARATOR)}?" + pair + "(?<rest>$|#{Regexp.escape(self::SEPARATOR)}(?!#{Regexp.escape(self::SEPARATOR)}))" ,Regexp::EXTENDED)
325
+ end
326
+ end
326
327
 
327
328
  end
328
329
 
330
+ extend ClassMethods
331
+
329
332
  def encode(x)
330
333
  Utils.pct(Utils.object_to_param(x), self.class::CHARACTER_CLASS[:unencoded])
331
334
  end
@@ -544,6 +547,7 @@ __REGEXP__
544
547
  # tpl = URITemplate::Draft7.new('{foo}')
545
548
  # URITemplate::Draft7.try_convert( tpl ) #=> tpl
546
549
  # URITemplate::Draft7.try_convert('{foo}') #=> tpl
550
+ # URITemplate::Draft7.try_convert(URITemplate.new(:colon, ':foo')) #=> tpl
547
551
  # # This pattern is invalid, so it wont be parsed:
548
552
  # URITemplate::Draft7.try_convert('{foo') #=> nil
549
553
  #
@@ -552,6 +556,14 @@ __REGEXP__
552
556
  return x
553
557
  elsif x.kind_of? String and valid? x
554
558
  return new(x)
559
+ elsif x.kind_of? URITemplate::Colon
560
+ return new( x.tokens.map{|tk|
561
+ if tk.literal?
562
+ Literal.new(tk.string)
563
+ else
564
+ Expression.new([[tk.variables.first, false, 0]])
565
+ end
566
+ })
555
567
  else
556
568
  return nil
557
569
  end
@@ -606,6 +618,7 @@ __REGEXP__
606
618
  end
607
619
  end
608
620
 
621
+ # @method expand(variables = {})
609
622
  # Expands the template with the given variables.
610
623
  # The expansion should be compatible to uritemplate spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
611
624
  # @note
@@ -619,31 +632,6 @@ __REGEXP__
619
632
  #
620
633
  # @param variables Hash
621
634
  # @return String
622
- def expand(variables = {})
623
- tokens.map{|part|
624
- part.expand(variables, {})
625
- }.join
626
- end
627
-
628
- # Returns an array containing all variables. Repeated variables are ignored, but the order will be kept intact.
629
- # @example
630
- # URITemplate::Draft7.new('{foo}{bar}{baz}').variables #=> ['foo','bar','baz']
631
- # URITemplate::Draft7.new('{a}{c}{a}{b}').variables #=> ['c','a','b']
632
- #
633
- # @return Array
634
- def variables
635
- @variables ||= begin
636
- vars = []
637
- tokens.each{|token|
638
- if token.respond_to? :variable_names
639
- vn = token.variable_names.uniq
640
- vars -= vn
641
- vars.push(*vn)
642
- end
643
- }
644
- vars
645
- end
646
- end
647
635
 
648
636
  # 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 {#===}.
649
637
  #
@@ -659,7 +647,7 @@ __REGEXP__
659
647
  bc = 0
660
648
  @regexp ||= Regexp.new(classes.join + '\A' + tokens.map{|part|
661
649
  r = part.to_r_source(bc)
662
- bc += part.size
650
+ bc += part.arity
663
651
  r
664
652
  }.join + '\z' , Regexp::EXTENDED)
665
653
  end
@@ -803,7 +791,7 @@ __REGEXP__
803
791
  tokens.map(&:level).max
804
792
  end
805
793
 
806
- # Tries to conatenate two templates, as if they were path segments.
794
+ # Tries to concatenate two templates, as if they were path segments.
807
795
  # Removes double slashes or insert one if they are missing.
808
796
  #
809
797
  # @example
@@ -886,20 +874,6 @@ __REGEXP__
886
874
 
887
875
  return false
888
876
  end
889
-
890
- # Returns the number of static characters in this template.
891
- # This method is useful for routing, since it's often pointful to use the url with fewer variable characters.
892
- # 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.
893
- #
894
- # @example
895
- # URITemplate::Draft7.new('/xy/').static_characters #=> 4
896
- # URITemplate::Draft7.new('{foo}').static_characters #=> 0
897
- # URITemplate::Draft7.new('a{foo}b').static_characters #=> 2
898
- #
899
- # @return Numeric
900
- def static_characters
901
- @static_characters ||= tokens.select{|t| t.kind_of?(Literal) }.map{|t| t.string.size }.inject(0,:+)
902
- end
903
877
 
904
878
  protected
905
879
  # @private
@@ -917,7 +891,7 @@ protected
917
891
  vars = []
918
892
  tokens.each{|part|
919
893
  i = 0
920
- while i < part.size
894
+ while i < part.arity
921
895
  vars.push(*part.extract(i, matchdata["v#{bc}"]))
922
896
  bc += 1
923
897
  i += 1
@@ -17,6 +17,41 @@
17
17
 
18
18
  module URITemplate
19
19
 
20
+ # An awesome little helper which helps iterating over a string.
21
+ # Initialize with a regexp and pass a string to :each.
22
+ # It will yield a string or a MatchData
23
+ class RegexpEnumerator
24
+
25
+ include Enumerable
26
+
27
+ def initialize(regexp)
28
+ @regexp = regexp
29
+ end
30
+
31
+ def each(str)
32
+ return Enumerator.new(self,:each,str) unless block_given?
33
+ rest = str
34
+ loop do
35
+ m = @regexp.match(rest)
36
+ if m.nil?
37
+ yield rest
38
+ break
39
+ end
40
+ yield m.pre_match if m.pre_match.size > 0
41
+ yield m
42
+ if m[0].size == 0
43
+ # obviously matches empty string, so post_match will equal rest
44
+ # terminate or this will loop forever
45
+ yield m.post_match
46
+ break
47
+ end
48
+ rest = m.post_match
49
+ end
50
+ return self
51
+ end
52
+
53
+ end
54
+
20
55
  # This error will be raised whenever an object could not be converted to a param string.
21
56
  class Unconvertable < StandardError
22
57
 
data/lib/uri_template.rb CHANGED
@@ -18,8 +18,65 @@
18
18
  # A base module for all implementations of a uri template.
19
19
  module URITemplate
20
20
 
21
+ # This should make it possible to do basic analysis independently from the concrete type.
22
+ module Token
23
+
24
+ def size
25
+ variables.size
26
+ end
27
+
28
+ end
29
+
30
+ # A module which all literal tokens should include.
31
+ module Literal
32
+
33
+ include Token
34
+
35
+ attr_reader :string
36
+
37
+ def literal?
38
+ true
39
+ end
40
+
41
+ def expression?
42
+ false
43
+ end
44
+
45
+ def variables
46
+ []
47
+ end
48
+
49
+ def size
50
+ 0
51
+ end
52
+
53
+ def expand(*_)
54
+ return string
55
+ end
56
+
57
+ end
58
+
59
+
60
+ # A module which all non-literal tokens should include.
61
+ module Expression
62
+
63
+ include Token
64
+
65
+ attr_reader :variables
66
+
67
+ def literal?
68
+ false
69
+ end
70
+
71
+ def expression?
72
+ true
73
+ end
74
+
75
+ end
76
+
21
77
  autoload :Utils, 'uri_template/utils'
22
78
  autoload :Draft7, 'uri_template/draft7'
79
+ autoload :Colon, 'uri_template/colon'
23
80
 
24
81
  # A hash with all available implementations.
25
82
  # Currently the only implementation is :draft7. But there also aliases :default and :latest available. This should make it possible to add newer specs later.
@@ -27,6 +84,7 @@ module URITemplate
27
84
  VERSIONS = {
28
85
  :draft7 => :Draft7,
29
86
  :default => :Draft7,
87
+ :colon => :Colon,
30
88
  :latest => :Draft7
31
89
  }
32
90
 
@@ -95,6 +153,7 @@ module URITemplate
95
153
  #
96
154
  # @example
97
155
  # URITemplate.coerce( URITemplate.new(:draft7,'{x}'), '{y}' ) #=> [URITemplate.new(:draft7,'{x}'), URITemplate.new(:draft7,'{y}'), false, true]
156
+ # URITemplate.coerce( '{y}', URITemplate.new(:draft7,'{x}') ) #=> [URITemplate.new(:draft7,'{y}'), URITemplate.new(:draft7,'{x}'), true, false]
98
157
  def self.coerce(a,b)
99
158
  if a.kind_of? URITemplate
100
159
  if a.class == b.class
@@ -140,12 +199,15 @@ module URITemplate
140
199
  module Invalid
141
200
  end
142
201
 
143
- # @abstract
144
202
  # Expands this uri template with the given variables.
145
203
  # The variables should be converted to strings using {Utils#object_to_param}.
146
204
  # @raise {Unconvertable} if a variable could not be converted to a string.
147
- def expand(variables={})
148
- raise "Please implement #expand on #{self.class.inspect}."
205
+ # @param variables Hash
206
+ # @return String
207
+ def expand(variables = {})
208
+ tokens.map{|part|
209
+ part.expand(variables)
210
+ }.join
149
211
  end
150
212
 
151
213
  # @abstract
@@ -153,5 +215,35 @@ module URITemplate
153
215
  def type
154
216
  raise "Please implement #type on #{self.class.inspect}."
155
217
  end
218
+
219
+ # @abstract
220
+ # Returns the tokens of this templates. Tokens should include either {Static} or {Expression}.
221
+ def tokens
222
+ raise "Please implement #tokens on #{self.class.inspect}."
223
+ end
224
+
225
+ # Returns an array containing all variables. Repeated variables are ignored. The concrete order of the variables may change.
226
+ # @example
227
+ # URITemplate.new('{foo}{bar}{baz}').variables #=> ['foo','bar','baz']
228
+ # URITemplate.new('{a}{c}{a}{b}').variables #=> ['a','c','b']
229
+ #
230
+ # @return Array
231
+ def variables
232
+ @variables ||= tokens.select(&:expression?).map(&:variables).flatten.uniq
233
+ end
234
+
235
+ # Returns the number of static characters in this template.
236
+ # This method is useful for routing, since it's often pointful to use the url with fewer variable characters.
237
+ # 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.
238
+ #
239
+ # @example
240
+ # URITemplate.new('/xy/').static_characters #=> 4
241
+ # URITemplate.new('{foo}').static_characters #=> 0
242
+ # URITemplate.new('a{foo}b').static_characters #=> 2
243
+ #
244
+ # @return Numeric
245
+ def static_characters
246
+ @static_characters ||= tokens.select(&:literal?).map{|t| t.string.size }.inject(0,:+)
247
+ end
156
248
 
157
249
  end
data/uri_template.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'uri_template'
3
- s.version = '0.1.1'
4
- s.date = '2011-11-04'
3
+ s.version = '0.1.2'
4
+ s.date = '2011-11-10'
5
5
  s.authors = ["HannesG"]
6
6
  s.email = %q{hannes.georg@googlemail.com}
7
7
  s.summary = 'A templating system for URIs.'
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.1.1
4
+ version: 0.1.2
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-04 00:00:00.000000000Z
12
+ date: 2011-11-10 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &17225100 !ruby/object:Gem::Requirement
16
+ requirement: &14750900 !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: *17225100
24
+ version_requirements: *14750900
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: yard
27
- requirement: &17224520 !ruby/object:Gem::Requirement
27
+ requirement: &14750460 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *17224520
35
+ version_requirements: *14750460
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
38
  gem however is intended to be extended when newer specs evolve. For now only draft
@@ -43,6 +43,7 @@ extensions: []
43
43
  extra_rdoc_files: []
44
44
  files:
45
45
  - lib/uri_template.rb
46
+ - lib/uri_template/colon.rb
46
47
  - lib/uri_template/draft7.rb
47
48
  - lib/uri_template/utils.rb
48
49
  - uri_template.gemspec