uri_template 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # 0.5.0 - 23.09.2012
2
+ - - Removed draft7
3
+ - - splitted absoulte? method into host? and scheme?
4
+ - + the URITemplate interface is now much stronger
5
+ - * code quality _significantly_ improved
6
+ - + concat method
7
+
8
+ # 0.4.0 - 06.07.2012
9
+ - + expand now accepts symbols as keys ( thanks to @peterhellber )
10
+ - + expand now accepts arrays of pairs ( thanks to @peterhellber )
11
+ - * fixed some testing bugs
12
+
13
+ # 0.3.0 - 24.05.2012
14
+ - + Implemented the final version. Default implementation is now RFC 6570
15
+ - * BUGFIX: variables with terminal dots were allowed
16
+ - * BUGFIX: lists of commas were parsed incorrectly
17
+
18
+ # 0.2.1 - 30.12.2011
19
+ - * Compatibility: Works now with MRI 1.8.7 and REE
20
+
21
+ # 0.2.0 - 03.12.2011
22
+ - * Reworked the escaping mechanism
23
+ - + escape_utils can now be used to boost escape/unescape performance
24
+
25
+ # 0.1.4 - 19.11.2011
26
+ - * Compatiblity: Works now with MRI 1.9.3, Rubinius and JRuby
27
+ - * Various (significant!) performance improvements
28
+
29
+ # 0.1.3 - 15.11.2011
30
+ - * BUGFIX: Draft7./ now concatenates literals correctly
31
+ - * BUGFIX: Draft7.tokens is now public
32
+
33
+ # 0.1.2 - 10.11.2011
34
+ - + added a new template-type: Colon
35
+ this should allow (some day) to rails-like routing tables
36
+ - + made the tokens-method mandatory and added two interfaces for tokens.
37
+ this allows cross-type features like variable anaylisis
38
+
39
+ # 0.1.1 - 4.11.2011
40
+ - + added a bunch of useful helper methods
41
+
42
+ # 0.1.0 - 2.11.2011
43
+ - - Removed Sections. They made too many headaches.
44
+ - + Made draft7 template concatenateable. This should replace sections.
45
+ - * BUGFIX: multiline uris were matched
46
+ - * BUGFIX: variablenames were decoded when this was not appreciated
47
+
48
+ # 0.0.2 - 1.11.2011
49
+ - * BUGFIX: Concatenating empty sections no more leads to catch-all templates, when an emtpy template was appreciated.
50
+ - + The extracted variables now contains the keys :suffix and :prefix if the match didn't consume the whole uri.
51
+
52
+ # 0.0.1 - 30.10.2011
53
+ - Initial version
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ URITemplate - a uri template library
2
+ ========================
3
+
4
+ [![Build Status](https://secure.travis-ci.org/hannesg/uri_template.png)](http://travis-ci.org/hannesg/uri_template)
5
+ [![Dependency Status](https://gemnasium.com/hannesg/uri_template.png)](https://gemnasium.com/hannesg/uri_template)
6
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/hannesg/uri_template)
7
+
8
+ With URITemplate you can generate URIs based on simple templates and extract variables from URIs using the same templates. There are currently two syntaxes defined. Namely the one defined in [RFC 6570]( http://tools.ietf.org/html/rfc6570 ) and a colon based syntax, similiar to the one used by sinatra.
9
+
10
+ From version 0.2.0, it will use escape_utils if available. This will significantly boost uri-escape/unescape performance if more characters need to be escaped ( may be slightly slower in trivial cases. working on that ... ), but does not run everywhere. To enable this, do the following:
11
+
12
+ # escape_utils has to be loaded when uri_templates is loaded
13
+ gem 'escape_utils'
14
+ require 'escape_utils'
15
+
16
+ gem 'uri_template'
17
+ require 'uri_template'
18
+
19
+ UriTemplate::Utils.using_escape_utils? #=> true
20
+
21
+
22
+ Examples
23
+ -------------------
24
+
25
+ require 'uri_template'
26
+
27
+ tpl = URITemplate.new('http://{host}{/segments*}/{file}{.extensions*}')
28
+
29
+ # This will give: http://www.host.com/path/to/a/file.x.y
30
+ tpl.expand('host'=>'www.host.com','segments'=>['path','to','a'],'file'=>'file','extensions'=>['x','y'])
31
+
32
+ # This will give: { 'host'=>'www.host.com','segments'=>['path','to','a'],'file'=>'file','extensions'=>['x','y']}
33
+ tpl.extract('http://www.host.com/path/to/a/file.x.y')
34
+
35
+ # If you like colon templates more:
36
+ tpl2 = URITemplate.new(:colon, '/:x/y')
37
+
38
+ # This will give: {'x' => 'z'}
39
+ tpl2.extract('/z/y')
40
+
41
+
42
+ RFC 6570 Syntax
43
+ --------------------
44
+
45
+ The syntax defined by [RFC 6570]( http://tools.ietf.org/html/rfc6570 ) is pretty straight forward. Basically anything surrounded by curly brackets is interpreted as variable.
46
+
47
+ URITemplate.new('{variable}').expand('variable' => 'value') #=> "value"
48
+
49
+ The way variables are inserted can be modified using operators. The operator is the first character between the curly brackets. There are seven operators defined `#`, `+`, `;`, `?`, `&`, `/` and `.`. So if you want to create a form-style query do this:
50
+
51
+ URITemplate.new('{?variable}').expand('variable' => 'value') #=> "?variable=value"
52
+
53
+ Benchmarks
54
+ -----------------------
55
+
56
+ I have assembled one benchmark based on the uritemplate-test examples. You can find them in the "benchmarks" folder. The short result: uri_template is 2-10x faster than addressable on ruby 1.9.3.
@@ -20,15 +20,45 @@ require 'forwardable'
20
20
  require 'uri_template'
21
21
  require 'uri_template/utils'
22
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
23
  module URITemplate
26
24
 
25
+ # A colon based template denotes variables with a colon.
26
+ #
27
+ # This template type is somewhat compatible with sinatra.
28
+ #
29
+ # @example
30
+ # tpl = URITemplate::Colon.new('/foo/:bar')
31
+ # tpl.extract('/foo/baz') #=> {'bar'=>'baz'}
32
+ # tpl.expand('bar'=>'boom') #=> '/foo/boom'
33
+ #
27
34
  class Colon
28
35
 
29
36
  include URITemplate
30
37
 
31
- VAR = /(?:\{:([a-z]+)\}|:([a-z]+)(?![a-z]))/u
38
+ VAR = /(?:\{:([a-z]+)\}|:([a-z]+)(?![a-z])|\*)/u
39
+
40
+ class InvalidValue < StandardError
41
+
42
+ include URITemplate::InvalidValue
43
+
44
+ attr_reader :variable, :value
45
+
46
+ def initialize(variable, value)
47
+ @variable = variable
48
+ @value = value
49
+ super(generate_message())
50
+ end
51
+
52
+ class SplatIsNotAnArray < self
53
+ end
54
+
55
+ protected
56
+
57
+ def generate_message()
58
+ return "The template variable " + variable.inspect + " cannot expand the given value "+ value.inspect
59
+ end
60
+
61
+ end
32
62
 
33
63
  class Token
34
64
 
@@ -44,11 +74,41 @@ class Colon
44
74
  end
45
75
 
46
76
  def expand(vars)
47
- return Utils.escape_url(Utils.object_to_param(vars[@name]))
77
+ return Utils.escape_url(Utils.object_to_param(vars[name]))
78
+ end
79
+
80
+ def to_r
81
+ return '([^/]*?)'
82
+ end
83
+
84
+ def to_s
85
+ return ":#{name}"
86
+ end
87
+
88
+ end
89
+
90
+ class Splat < Variable
91
+
92
+ SPLAT = 'splat'.freeze
93
+
94
+ attr_reader :index
95
+
96
+ def initialize(index)
97
+ @index = index
98
+ super(SPLAT)
99
+ end
100
+
101
+ def expand(vars)
102
+ var = vars[name]
103
+ if Array === var
104
+ return Utils.escape_uri(Utils.object_to_param(var[index]))
105
+ else
106
+ raise InvalidValue::SplatIsNotAnArray.new(name,var)
107
+ end
48
108
  end
49
109
 
50
110
  def to_r
51
- return ['([^/]*?)'].join
111
+ return '(.+?)'
52
112
  end
53
113
 
54
114
  end
@@ -78,13 +138,13 @@ class Colon
78
138
  # Tries to convert the value into a colon-template.
79
139
  # @example
80
140
  # URITemplate::Colon.try_convert('/foo/:bar/').pattern #=> '/foo/:bar/'
81
- # URITemplate::Colon.try_convert(URITemplate::Draft7.new('/foo/{bar}/')).pattern #=> '/foo/{:bar}/'
141
+ # URITemplate::Colon.try_convert(URITemplate.new(:rfc6570, '/foo/{bar}/')).pattern #=> '/foo/{:bar}/'
82
142
  def self.try_convert(x)
83
143
  if x.kind_of? String
84
144
  return new(x)
85
145
  elsif x.kind_of? self
86
146
  return x
87
- elsif x.kind_of? URITemplate::Draft7 and x.level == 1
147
+ elsif x.kind_of? URITemplate::RFC6570 and x.level == 1
88
148
  return new( x.pattern.gsub(/\{(.*?)\}/u){ "{:#{$1}}" } )
89
149
  else
90
150
  return nil
@@ -92,6 +152,7 @@ class Colon
92
152
  end
93
153
 
94
154
  def initialize(pattern)
155
+ raise ArgumentError,"Expected a String but got #{pattern.inspect}" unless pattern.kind_of? String
95
156
  @pattern = pattern
96
157
  end
97
158
 
@@ -102,9 +163,20 @@ class Colon
102
163
  def extract(uri)
103
164
  md = self.to_r.match(uri)
104
165
  return nil unless md
105
- return Hash[ *self.variables.each_with_index.map{|v,i|
106
- [v, Utils.unescape_url(md[i+1])]
107
- }.flatten(1) ]
166
+ result = {}
167
+ splat = []
168
+ self.tokens.select{|tk| tk.kind_of? URITemplate::Expression }.each_with_index do |tk,i|
169
+ if tk.kind_of? Token::Splat
170
+ splat << md[i+1]
171
+ result['splat'] = splat unless result.key? 'splat'
172
+ else
173
+ result[tk.name] = Utils.unescape_url( md[i+1] )
174
+ end
175
+ end
176
+ if block_given?
177
+ return yield(result)
178
+ end
179
+ return result
108
180
  end
109
181
 
110
182
  def type
@@ -119,30 +191,17 @@ class Colon
119
191
  @tokens ||= tokenize!
120
192
  end
121
193
 
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
194
  protected
141
195
 
142
196
  def tokenize!
197
+ number_of_splats = 0
143
198
  RegexpEnumerator.new(VAR).each(@pattern).map{|x|
144
199
  if x.kind_of? String
145
- Token::Static.new(x)
200
+ Token::Static.new(Utils.escape_uri(x))
201
+ elsif x[0] == '*'
202
+ n = number_of_splats
203
+ number_of_splats = number_of_splats + 1
204
+ Token::Splat.new(n)
146
205
  else
147
206
  # TODO: when rubinius supports ambigious names this could be replaced with x['name'] *sigh*
148
207
  Token::Variable.new(x[1] || x[2])
@@ -0,0 +1,33 @@
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
+ # A module which all non-literal tokens should include.
19
+ module URITemplate::Expression
20
+
21
+ include URITemplate::Token
22
+
23
+ attr_reader :variables
24
+
25
+ def literal?
26
+ false
27
+ end
28
+
29
+ def expression?
30
+ true
31
+ end
32
+
33
+ end
@@ -0,0 +1,53 @@
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
+ # A module which all literal tokens should include.
19
+ module URITemplate::Literal
20
+
21
+ include URITemplate::Token
22
+
23
+ SLASH = ?/
24
+
25
+ attr_reader :string
26
+
27
+ def literal?
28
+ true
29
+ end
30
+
31
+ def expression?
32
+ false
33
+ end
34
+
35
+ def size
36
+ 0
37
+ end
38
+
39
+ def expand(_)
40
+ return string
41
+ end
42
+
43
+ def starts_with_slash?
44
+ string[0] == SLASH
45
+ end
46
+
47
+ def ends_with_slash?
48
+ string[-1] == SLASH
49
+ end
50
+
51
+ alias to_s string
52
+
53
+ end
@@ -0,0 +1,72 @@
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 'uri_template/rfc6570'
19
+
20
+ class URITemplate::RFC6570::Expression::Named < URITemplate::RFC6570::Expression
21
+
22
+ alias self_pair pair
23
+
24
+ def to_r_source
25
+ source = regex_builder
26
+ source.group do
27
+ source.escaped_prefix
28
+ first = true
29
+ @variable_specs.each do | var, expand , max_length |
30
+ if expand
31
+ source.capture do
32
+ source.separated_list(first) do
33
+ source.character_class('+')\
34
+ .escaped_pair_connector\
35
+ .character_class_with_comma(max_length)
36
+ end
37
+ end
38
+ else
39
+ source.group do
40
+ source.escaped_separator unless first
41
+ source << Regexp.escape(var)
42
+ source.group do
43
+ source.escaped_pair_connector
44
+ source.capture do
45
+ source.character_class_with_comma(max_length)
46
+ end
47
+ source << '|' unless self.class::PAIR_IF_EMPTY
48
+ end
49
+ end.length('?')
50
+ end
51
+ first = false
52
+ end
53
+ end.length('?')
54
+ return source.join
55
+ end
56
+
57
+ private
58
+
59
+ def extracted_nil
60
+ self.class::PAIR_IF_EMPTY ? nil : ""
61
+ end
62
+
63
+ def after_expand(name, splitted)
64
+ result = URITemplate::Utils.pair_array_to_hash2( splitted )
65
+ if result.size == 1 && result[0][0] == name
66
+ return result
67
+ else
68
+ return [ [ name , result ] ]
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,76 @@
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 'uri_template/rfc6570'
19
+
20
+ class URITemplate::RFC6570::Expression::Unnamed < URITemplate::RFC6570::Expression
21
+
22
+ def self_pair(_, value, max_length = 0,&block)
23
+ if block
24
+ ev = value.map(&block).join(self.class::LIST_CONNECTOR)
25
+ else
26
+ ev = escape(value)
27
+ end
28
+ cut( ev, max_length ,&block)
29
+ end
30
+
31
+ def to_r_source
32
+ vs = @variable_specs.size - 1
33
+ i = 0
34
+ source = regex_builder
35
+ source.group do
36
+ source.escaped_prefix
37
+ @variable_specs.each do | var, expand , max_length |
38
+ last = (vs == i)
39
+ first = (i == 0)
40
+ if expand
41
+ source.group(true) do
42
+ source.separated_list(first) do
43
+ source.group do
44
+ source.character_class('+').reluctant
45
+ source.escaped_pair_connector
46
+ end.length('?')
47
+ source.character_class(max_length)
48
+ end
49
+ end
50
+ else
51
+ source.escaped_separator unless first
52
+ source.group(true) do
53
+ if last
54
+ source.character_class_with_comma(max_length)
55
+ else
56
+ source.character_class(max_length)
57
+ end
58
+ end
59
+ end
60
+ i = i+1
61
+ end
62
+ end.length('?')
63
+ return source.join
64
+ end
65
+
66
+ private
67
+
68
+ def after_expand(name, splitted)
69
+ if splitted.none?{|_,b| b }
70
+ return [ [ name, splitted.map{|a,_| a } ] ]
71
+ else
72
+ return [ [ name, splitted ] ]
73
+ end
74
+ end
75
+
76
+ end