uri_template 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/README +29 -27
- data/lib/uri_template.rb +17 -4
- data/lib/uri_template/colon.rb +2 -2
- data/lib/uri_template/draft7.rb +55 -699
- data/lib/uri_template/rfc6570.rb +992 -0
- data/lib/uri_template/utils.rb +22 -4
- data/uri_template.gemspec +5 -4
- metadata +29 -17
data/CHANGELOG
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
# 0.3.0 - 24.05.2012
|
2
|
+
* Implemented the final version. Default implementation is now RFC 6570
|
3
|
+
* BUGFIX: variables with terminal dots were allowed
|
4
|
+
* BUGFIX: lists of commas were parsed incorrectly
|
5
|
+
|
1
6
|
# 0.2.1 - 30.12.2011
|
2
7
|
* Compatibility: Works now with MRI 1.8.7 and REE
|
3
8
|
|
data/README
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
= URITemplate - a uri template library
|
2
2
|
|
3
|
-
With URITemplate you can generate URI based on simple templates. The
|
3
|
+
With URITemplate you can generate URI based on simple templates. The syntax if defined by RFC 6570 ( http://tools.ietf.org/html/rfc6570 ).
|
4
4
|
|
5
|
-
|
5
|
+
For backward compatibility, there is an implementation based on draft 7 of the uri template spec ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ), too. For the syntax read that spec. Draft 2 of that specification is implemented as addressable gem.
|
6
6
|
|
7
|
-
|
7
|
+
It's currently planed to add newer versions of that spec when they emerge. Therefore the module URITemplate just defines one abstract method ( expand ) and some methods to access the actual implementations.
|
8
8
|
|
9
9
|
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:
|
10
10
|
|
@@ -34,30 +34,32 @@ From version 0.2.0, it will use escape_utils if available. This will significant
|
|
34
34
|
|
35
35
|
== Benchmarks
|
36
36
|
|
37
|
-
* System: Core 2 Duo T9300, 4 gb ram,
|
38
|
-
* Implementation:
|
37
|
+
* System: Core 2 Duo T9300, 4 gb ram, ubuntu 12.04 64 bit, ruby 1.9.3, 100_000 repetitions
|
38
|
+
* Implementation: RFC6570 ( version 0.3.0 ) vs. Addressable ( 2.2.8 )
|
39
39
|
* Results marked with * means that the template object is reused.
|
40
40
|
|
41
|
-
Addressable | Addressable* |
|
42
|
-
--Expansion
|
43
|
-
Empty string 8.
|
44
|
-
One simple variable 19.
|
45
|
-
One escaped variable
|
46
|
-
One missing variable 9.
|
47
|
-
Path segments
|
48
|
-
Arguments
|
49
|
-
Full URI
|
50
|
-
Segments and Arguments
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
One
|
55
|
-
One
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
Segments and Arguments
|
61
|
-
|
62
|
-
|
41
|
+
Addressable | Addressable* | RFC6570 | RFC6570* |
|
42
|
+
--Expansion-----------------------------------------------------------------------------------------
|
43
|
+
Empty string 8.505 | 8.439 | 0.539 | 0.064 |
|
44
|
+
One simple variable 19.717 | 19.721 | 3.031 | 1.169 |
|
45
|
+
One escaped variable 21.873 | 22.017 | 3.573 | 1.705 |
|
46
|
+
One missing variable 9.676 | 11.981 | 2.633 | 0.352 |
|
47
|
+
Path segments 29.901 | 31.698 | 4.929 | 3.051 |
|
48
|
+
Arguments 36.584 | 36.531 | 9.540 | 5.982 |
|
49
|
+
Full URI 109.102 | 116.458 | 19.806 | 10.548 |
|
50
|
+
Segments and Arguments 108.103 | 107.750 | 14.059 | 7.593 |
|
51
|
+
total 343.461 | 354.596 | 58.109 | 30.464 |
|
52
|
+
--Extraction----------------------------------------------------------------------------------------
|
53
|
+
Empty string 21.422 | 21.843 | 3.122 | 0.804 |
|
54
|
+
One simple variable 39.860 | 43.840 | 10.671 | 2.874 |
|
55
|
+
One escaped variable 51.321 | 50.790 | 10.963 | 2.040 |
|
56
|
+
One missing variable 26.125 | 26.320 | 9.400 | 1.847 |
|
57
|
+
Path segments 62.712 | 64.191 | 18.502 | 4.518 |
|
58
|
+
Arguments 81.350 | 81.258 | 20.762 | 5.401 |
|
59
|
+
Full URI 145.339 | 141.795 | 45.306 | 11.096 |
|
60
|
+
Segments and Arguments 124.431 | 122.885 | 34.208 | 8.126 |
|
61
|
+
Segments and Arguments ( not extractable ) 34.183 | 34.200 | 26.542 | 0.410 |
|
62
|
+
total 586.743 | 587.122 | 179.476 | 37.116 |
|
63
|
+
|
64
|
+
SUCCESS - URITemplate was faster in every test!
|
63
65
|
|
data/lib/uri_template.rb
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
# You should have received a copy of the GNU General Public License
|
13
13
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
14
|
#
|
15
|
-
# (c) 2011 by Hannes Georg
|
15
|
+
# (c) 2011 - 2012 by Hannes Georg
|
16
16
|
#
|
17
17
|
|
18
18
|
# A base module for all implementations of a uri template.
|
@@ -21,8 +21,13 @@ module URITemplate
|
|
21
21
|
# @private
|
22
22
|
# Should we use \u.... or \x.. in regexps?
|
23
23
|
SUPPORTS_UNICODE_CHARS = begin
|
24
|
-
|
25
|
-
|
24
|
+
if "string".respond_to? :encoding
|
25
|
+
rx = eval('Regexp.compile("\u0020")')
|
26
|
+
!!(rx =~ " ")
|
27
|
+
else
|
28
|
+
rx = eval('/\u0020/')
|
29
|
+
!!(rx =~ " ")
|
30
|
+
end
|
26
31
|
rescue SyntaxError
|
27
32
|
false
|
28
33
|
end
|
@@ -84,6 +89,7 @@ module URITemplate
|
|
84
89
|
|
85
90
|
autoload :Utils, 'uri_template/utils'
|
86
91
|
autoload :Draft7, 'uri_template/draft7'
|
92
|
+
autoload :RFC6570, 'uri_template/rfc6570'
|
87
93
|
autoload :Colon, 'uri_template/colon'
|
88
94
|
|
89
95
|
# A hash with all available implementations.
|
@@ -91,9 +97,10 @@ module URITemplate
|
|
91
97
|
# @see resolve_class
|
92
98
|
VERSIONS = {
|
93
99
|
:draft7 => :Draft7,
|
100
|
+
:rfc6570 => :RFC6570,
|
94
101
|
:default => :Draft7,
|
95
102
|
:colon => :Colon,
|
96
|
-
:latest => :
|
103
|
+
:latest => :RFC6570
|
97
104
|
}
|
98
105
|
|
99
106
|
# Looks up which implementation to use.
|
@@ -207,12 +214,18 @@ module URITemplate
|
|
207
214
|
module Invalid
|
208
215
|
end
|
209
216
|
|
217
|
+
# A base class for all errors which will be raised when a variable value
|
218
|
+
# is not allowed for a certain expansion.
|
219
|
+
module InvalidValue
|
220
|
+
end
|
221
|
+
|
210
222
|
# Expands this uri template with the given variables.
|
211
223
|
# The variables should be converted to strings using {Utils#object_to_param}.
|
212
224
|
# @raise {Unconvertable} if a variable could not be converted to a string.
|
213
225
|
# @param variables Hash
|
214
226
|
# @return String
|
215
227
|
def expand(variables = {})
|
228
|
+
raise ArgumentError, "Expected something that returns to :[], but got: #{variables.inspect}" unless variables.respond_to? :[]
|
216
229
|
tokens.map{|part|
|
217
230
|
part.expand(variables)
|
218
231
|
}.join
|
data/lib/uri_template/colon.rb
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
# You should have received a copy of the GNU General Public License
|
13
13
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
14
|
#
|
15
|
-
# (c) 2011 by Hannes Georg
|
15
|
+
# (c) 2011 - 2012 by Hannes Georg
|
16
16
|
#
|
17
17
|
|
18
18
|
require 'forwardable'
|
@@ -97,7 +97,7 @@ class Colon
|
|
97
97
|
|
98
98
|
# Extracts variables from an uri.
|
99
99
|
#
|
100
|
-
# @param String
|
100
|
+
# @param uri [String]
|
101
101
|
# @return nil,Hash
|
102
102
|
def extract(uri)
|
103
103
|
md = self.to_r.match(uri)
|
data/lib/uri_template/draft7.rb
CHANGED
@@ -12,190 +12,66 @@
|
|
12
12
|
# You should have received a copy of the GNU General Public License
|
13
13
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
14
14
|
#
|
15
|
-
# (c) 2011 by Hannes Georg
|
15
|
+
# (c) 2011 - 2012 by Hannes Georg
|
16
16
|
#
|
17
17
|
|
18
|
-
require 'strscan'
|
19
|
-
require 'set'
|
20
|
-
require 'forwardable'
|
21
|
-
|
22
|
-
require 'uri_template'
|
23
|
-
require 'uri_template/utils'
|
24
18
|
|
25
19
|
# A uri template which should comply with the uri template spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
|
26
|
-
#
|
27
|
-
|
28
|
-
class URITemplate::Draft7
|
29
|
-
|
30
|
-
include URITemplate
|
31
|
-
extend Forwardable
|
32
|
-
|
33
|
-
# @private
|
34
|
-
Utils = URITemplate::Utils
|
35
|
-
|
36
|
-
if SUPPORTS_UNICODE_CHARS
|
37
|
-
# @private
|
38
|
-
# \/ - unicode ctrl-chars
|
39
|
-
LITERAL = /([^"'%<>\\^`{|}\u0000-\u001F\u007F-\u009F\s]|%[0-9a-fA-F]{2})+/u
|
40
|
-
else
|
41
|
-
# @private
|
42
|
-
LITERAL = Regexp.compile('([^"\'%<>\\\\^`{|}\x00-\x1F\x7F-\x9F\s]|%[0-9a-fA-F]{2})+',Utils::KCODE_UTF8)
|
43
|
-
end
|
44
|
-
|
45
|
-
# @private
|
46
|
-
CHARACTER_CLASSES = {
|
47
|
-
|
48
|
-
:unreserved => {
|
49
|
-
:class => '(?:[A-Za-z0-9\-\._]|%[0-9a-fA-F]{2})',
|
50
|
-
:grabs_comma => false
|
51
|
-
},
|
52
|
-
:unreserved_reserved_pct => {
|
53
|
-
:class => '(?:[A-Za-z0-9\-\._:\/?#\[\]@!\$%\'\(\)*+,;=]|%[0-9a-fA-F]{2})',
|
54
|
-
:grabs_comma => true
|
55
|
-
},
|
56
|
-
|
57
|
-
:varname => {
|
58
|
-
:class => '(?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*?',
|
59
|
-
:class_name => 'c_vn_'
|
60
|
-
}
|
61
|
-
|
62
|
-
}
|
63
|
-
|
64
|
-
# Specifies that no processing should be done upon extraction.
|
65
|
-
# @see #extract
|
66
|
-
NO_PROCESSING = []
|
67
|
-
|
68
|
-
# Specifies that the extracted values should be processed.
|
69
|
-
# @see #extract
|
70
|
-
CONVERT_VALUES = [:convert_values]
|
71
|
-
|
72
|
-
# Specifies that the extracted variable list should be processed.
|
73
|
-
# @see #extract
|
74
|
-
CONVERT_RESULT = [:convert_result]
|
75
|
-
|
76
|
-
# Default processing. Means: convert values and the list itself.
|
77
|
-
# @see #extract
|
78
|
-
DEFAULT_PROCESSING = CONVERT_VALUES + CONVERT_RESULT
|
79
|
-
|
80
|
-
# @private
|
81
|
-
VAR = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
|
82
|
-
((?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*)(\*)?(?::(\d+))?
|
83
|
-
__REGEXP__
|
84
|
-
|
85
|
-
# @private
|
86
|
-
EXPRESSION = Regexp.compile(<<'__REGEXP__'.strip, Utils::KCODE_UTF8)
|
87
|
-
\{([+#\./;?&]?)((?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*\*?(?::\d+)?(?:,(?:[a-zA-Z_]|%[0-9a-fA-F]{2})(?:[a-zA-Z_\.]|%[0-9a-fA-F]{2})*\*?(?::\d+)?)*)\}
|
88
|
-
__REGEXP__
|
89
|
-
|
90
|
-
# @private
|
91
|
-
URI = Regexp.compile(<<__REGEXP__.strip, Utils::KCODE_UTF8)
|
92
|
-
\\A(#{LITERAL.source}|#{EXPRESSION.source})*\\z
|
93
|
-
__REGEXP__
|
94
|
-
|
95
|
-
SLASH = ?/
|
96
|
-
|
97
|
-
# @private
|
98
|
-
class Token
|
99
|
-
end
|
100
|
-
|
101
|
-
# @private
|
102
|
-
class Literal < Token
|
20
|
+
# This class is here for backward compatibility. There is already a newer draft of the spec and an rfc.
|
21
|
+
class URITemplate::Draft7 < URITemplate::RFC6570
|
103
22
|
|
104
|
-
|
23
|
+
TYPE = :draft7
|
105
24
|
|
106
|
-
|
107
|
-
@string = string
|
108
|
-
end
|
109
|
-
|
110
|
-
def level
|
111
|
-
1
|
112
|
-
end
|
113
|
-
|
114
|
-
def arity
|
115
|
-
0
|
116
|
-
end
|
117
|
-
|
118
|
-
def to_r_source(*_)
|
119
|
-
Regexp.escape(@string)
|
120
|
-
end
|
121
|
-
|
122
|
-
def to_s
|
123
|
-
@string
|
124
|
-
end
|
25
|
+
CHARACTER_CLASSES = URITemplate::RFC6570::CHARACTER_CLASSES
|
125
26
|
|
126
|
-
|
127
|
-
|
128
|
-
# @private
|
129
|
-
class Expression < Token
|
130
|
-
|
131
|
-
include URITemplate::Expression
|
132
|
-
|
133
|
-
attr_reader :variables, :max_length
|
134
|
-
|
135
|
-
def initialize(vars)
|
136
|
-
@variable_specs = vars
|
137
|
-
@variables = vars.map(&:first)
|
138
|
-
@variables.uniq!
|
139
|
-
end
|
140
|
-
|
141
|
-
PREFIX = ''.freeze
|
142
|
-
SEPARATOR = ','.freeze
|
143
|
-
PAIR_CONNECTOR = '='.freeze
|
144
|
-
PAIR_IF_EMPTY = true
|
145
|
-
LIST_CONNECTOR = ','.freeze
|
146
|
-
BASE_LEVEL = 1
|
147
|
-
|
148
|
-
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
|
27
|
+
Utils = URITemplate::Utils
|
149
28
|
|
150
|
-
|
151
|
-
OPERATOR = ''
|
29
|
+
class Expression < URITemplate::RFC6570::Expression
|
152
30
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
else
|
158
|
-
return 3
|
159
|
-
end
|
160
|
-
else
|
161
|
-
return 4
|
31
|
+
def extract(position,matched)
|
32
|
+
name, expand, max_length = @variable_specs[position]
|
33
|
+
if matched.nil?
|
34
|
+
return [[ name , matched ]]
|
162
35
|
end
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
36
|
+
if expand
|
37
|
+
#TODO: do we really need this? - this could be stolen from rack
|
38
|
+
ex = self.class.hash_extractor(max_length)
|
39
|
+
rest = matched
|
40
|
+
splitted = []
|
41
|
+
found_value = false
|
42
|
+
# 1 = name
|
43
|
+
# 2 = value
|
44
|
+
# 3 = rest
|
45
|
+
until rest.size == 0
|
46
|
+
match = ex.match(rest)
|
47
|
+
if match.nil?
|
48
|
+
raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
|
49
|
+
end
|
50
|
+
if match.post_match.size == 0
|
51
|
+
rest = match[3].to_s
|
177
52
|
else
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
53
|
+
rest = ''
|
54
|
+
end
|
55
|
+
if match[1]
|
56
|
+
found_value = true
|
57
|
+
splitted << [ match[1][0..-2], decode(match[2] + rest , false) ]
|
58
|
+
else
|
59
|
+
splitted << [ match[2] + rest, nil ]
|
183
60
|
end
|
61
|
+
rest = match.post_match
|
184
62
|
end
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
63
|
+
if !found_value
|
64
|
+
return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
|
65
|
+
else
|
66
|
+
return [ [ name, splitted ] ]
|
67
|
+
end
|
68
|
+
elsif self.class::NAMED
|
69
|
+
return [ [ name, decode( matched[1..-1] ) ] ]
|
190
70
|
end
|
191
|
-
end
|
192
71
|
|
193
|
-
|
194
|
-
return '{' + self.class::OPERATOR + @variable_specs.map{|name,expand,max_length| name + (expand ? '*': '') + (max_length > 0 ? (':' + max_length.to_s) : '') }.join(',') + '}'
|
72
|
+
return [ [ name, decode( matched ) ] ]
|
195
73
|
end
|
196
74
|
|
197
|
-
#TODO: certain things after a slurpy variable will never get matched. therefore, it's pointless to add expressions for them
|
198
|
-
#TODO: variables, which appear twice could be compacted, don't they?
|
199
75
|
def to_r_source
|
200
76
|
source = []
|
201
77
|
first = true
|
@@ -262,52 +138,18 @@ __REGEXP__
|
|
262
138
|
return '(?:' + Regexp.escape(self.class::PREFIX) + source.join + ')?'
|
263
139
|
end
|
264
140
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
return [[ name , matched ]]
|
269
|
-
end
|
141
|
+
protected
|
142
|
+
|
143
|
+
def transform_array(name, ary, expand , max_length)
|
270
144
|
if expand
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
# 1 = name
|
277
|
-
# 2 = value
|
278
|
-
# 3 = rest
|
279
|
-
until rest.size == 0
|
280
|
-
match = ex.match(rest)
|
281
|
-
if match.nil?
|
282
|
-
raise "Couldn't match #{rest.inspect} againts the hash extractor. This is definitly a Bug. Please report this ASAP!"
|
283
|
-
end
|
284
|
-
if match.post_match.size == 0
|
285
|
-
rest = match[3].to_s
|
286
|
-
else
|
287
|
-
rest = ''
|
288
|
-
end
|
289
|
-
if match[1]
|
290
|
-
found_value = true
|
291
|
-
splitted << [ match[1][0..-2], decode(match[2] + rest , false) ]
|
292
|
-
else
|
293
|
-
splitted << [ match[2] + rest, nil ]
|
294
|
-
end
|
295
|
-
rest = match.post_match
|
296
|
-
end
|
297
|
-
if !found_value
|
298
|
-
return [ [ name, splitted.map{|n,v| decode(n , false) } ] ]
|
299
|
-
else
|
300
|
-
return [ [ name, splitted ] ]
|
301
|
-
end
|
302
|
-
elsif self.class::NAMED
|
303
|
-
return [ [ name, decode( matched[1..-1] ) ] ]
|
145
|
+
ary.map{|value| escape(value) }
|
146
|
+
elsif ary.none?
|
147
|
+
[]
|
148
|
+
else
|
149
|
+
[ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| escape(value) }.join(self.class::LIST_CONNECTOR) ]
|
304
150
|
end
|
305
|
-
|
306
|
-
return [ [ name, decode( matched ) ] ]
|
307
151
|
end
|
308
152
|
|
309
|
-
protected
|
310
|
-
|
311
153
|
module ClassMethods
|
312
154
|
|
313
155
|
def hash_extractor(max_length)
|
@@ -324,88 +166,9 @@ __REGEXP__
|
|
324
166
|
|
325
167
|
extend ClassMethods
|
326
168
|
|
327
|
-
def escape(x)
|
328
|
-
Utils.escape_url(Utils.object_to_param(x))
|
329
|
-
end
|
330
|
-
|
331
|
-
def unescape(x)
|
332
|
-
Utils.unescape_url(x)
|
333
|
-
end
|
334
|
-
|
335
|
-
SPLITTER = /^(?:,(,*)|([^,]+))/
|
336
|
-
|
337
|
-
def decode(x, split = true)
|
338
|
-
if x.nil?
|
339
|
-
if self.class::PAIR_IF_EMPTY
|
340
|
-
return x
|
341
|
-
else
|
342
|
-
return ''
|
343
|
-
end
|
344
|
-
elsif split
|
345
|
-
r = []
|
346
|
-
v = x
|
347
|
-
until v.size == 0
|
348
|
-
m = SPLITTER.match(v)
|
349
|
-
if m[1] and m[1].size > 0
|
350
|
-
r << m[1]
|
351
|
-
elsif m[2]
|
352
|
-
r << unescape(m[2])
|
353
|
-
end
|
354
|
-
v = m.post_match
|
355
|
-
end
|
356
|
-
case(r.size)
|
357
|
-
when 0 then ''
|
358
|
-
when 1 then r.first
|
359
|
-
else r
|
360
|
-
end
|
361
|
-
else
|
362
|
-
unescape(x)
|
363
|
-
end
|
364
|
-
end
|
365
|
-
|
366
|
-
def cut(str,chars)
|
367
|
-
if chars > 0
|
368
|
-
md = Regexp.compile("\\A#{self.class::CHARACTER_CLASS[:class]}{0,#{chars.to_s}}", Utils::KCODE_UTF8).match(str)
|
369
|
-
#TODO: handle invalid matches
|
370
|
-
return md[0]
|
371
|
-
else
|
372
|
-
return str
|
373
|
-
end
|
374
|
-
end
|
375
|
-
|
376
|
-
def pair(key, value, max_length = 0)
|
377
|
-
ek = escape(key)
|
378
|
-
ev = escape(value)
|
379
|
-
if !self.class::PAIR_IF_EMPTY and ev.size == 0
|
380
|
-
return ek
|
381
|
-
else
|
382
|
-
return ek + self.class::PAIR_CONNECTOR + cut( ev, max_length )
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
def transform_hash(name, hsh, expand , max_length)
|
387
|
-
if expand
|
388
|
-
hsh.map{|key,value| pair(key,value) }
|
389
|
-
elsif hsh.none?
|
390
|
-
[]
|
391
|
-
else
|
392
|
-
[ (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) ]
|
393
|
-
end
|
394
|
-
end
|
395
|
-
|
396
|
-
def transform_array(name, ary, expand , max_length)
|
397
|
-
if expand
|
398
|
-
ary.map{|value| escape(value) }
|
399
|
-
elsif ary.none?
|
400
|
-
[]
|
401
|
-
else
|
402
|
-
[ (self.class::NAMED ? escape(name)+self.class::PAIR_CONNECTOR : '' ) + ary.map{|value| escape(value) }.join(self.class::LIST_CONNECTOR) ]
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
169
|
class Reserved < self
|
407
170
|
|
408
|
-
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
171
|
+
CHARACTER_CLASS = URITemplate::RFC6570::CHARACTER_CLASSES[:unreserved_reserved_pct]
|
409
172
|
OPERATOR = '+'.freeze
|
410
173
|
BASE_LEVEL = 2
|
411
174
|
|
@@ -421,7 +184,7 @@ __REGEXP__
|
|
421
184
|
|
422
185
|
class Fragment < self
|
423
186
|
|
424
|
-
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
187
|
+
CHARACTER_CLASS = URITemplate::RFC6570::CHARACTER_CLASSES[:unreserved_reserved_pct]
|
425
188
|
PREFIX = '#'.freeze
|
426
189
|
OPERATOR = '#'.freeze
|
427
190
|
BASE_LEVEL = 2
|
@@ -483,7 +246,8 @@ __REGEXP__
|
|
483
246
|
OPERATOR = '&'.freeze
|
484
247
|
BASE_LEVEL = 3
|
485
248
|
|
486
|
-
end
|
249
|
+
end
|
250
|
+
|
487
251
|
|
488
252
|
end
|
489
253
|
|
@@ -499,412 +263,4 @@ __REGEXP__
|
|
499
263
|
'&' => Expression::FormQueryContinuation
|
500
264
|
}
|
501
265
|
|
502
|
-
# This error is raised when an invalid pattern was given.
|
503
|
-
class Invalid < StandardError
|
504
|
-
|
505
|
-
include URITemplate::Invalid
|
506
|
-
|
507
|
-
attr_reader :pattern, :position
|
508
|
-
|
509
|
-
def initialize(source, position)
|
510
|
-
@pattern = pattern
|
511
|
-
@position = position
|
512
|
-
super("Invalid expression found in #{source.inspect} at #{position}: '#{source[position..-1]}'")
|
513
|
-
end
|
514
|
-
|
515
|
-
end
|
516
|
-
|
517
|
-
# @private
|
518
|
-
class Tokenizer
|
519
|
-
|
520
|
-
include Enumerable
|
521
|
-
|
522
|
-
attr_reader :source
|
523
|
-
|
524
|
-
def initialize(source)
|
525
|
-
@source = source
|
526
|
-
end
|
527
|
-
|
528
|
-
def each
|
529
|
-
if !block_given?
|
530
|
-
return Enumerator.new(self)
|
531
|
-
end
|
532
|
-
scanner = StringScanner.new(@source)
|
533
|
-
until scanner.eos?
|
534
|
-
expression = scanner.scan(EXPRESSION)
|
535
|
-
if expression
|
536
|
-
vars = scanner[2].split(',').map{|name|
|
537
|
-
match = VAR.match(name)
|
538
|
-
# 1 = varname
|
539
|
-
# 2 = explode
|
540
|
-
# 3 = length
|
541
|
-
[ match[1], match[2] == '*', match[3].to_i ]
|
542
|
-
}
|
543
|
-
yield OPERATORS[scanner[1]].new(vars)
|
544
|
-
else
|
545
|
-
literal = scanner.scan(LITERAL)
|
546
|
-
if literal
|
547
|
-
yield(Literal.new(literal))
|
548
|
-
else
|
549
|
-
raise Invalid.new(@source,scanner.pos)
|
550
|
-
end
|
551
|
-
end
|
552
|
-
end
|
553
|
-
end
|
554
|
-
|
555
|
-
end
|
556
|
-
|
557
|
-
# The class methods for all draft7 templates.
|
558
|
-
module ClassMethods
|
559
|
-
|
560
|
-
# Tries to convert the given param in to a instance of {Draft7}
|
561
|
-
# It basically passes thru instances of that class, parses strings and return nil on everything else.
|
562
|
-
#
|
563
|
-
# @example
|
564
|
-
# URITemplate::Draft7.try_convert( Object.new ) #=> nil
|
565
|
-
# tpl = URITemplate::Draft7.new('{foo}')
|
566
|
-
# URITemplate::Draft7.try_convert( tpl ) #=> tpl
|
567
|
-
# URITemplate::Draft7.try_convert('{foo}') #=> tpl
|
568
|
-
# URITemplate::Draft7.try_convert(URITemplate.new(:colon, ':foo')) #=> tpl
|
569
|
-
# # This pattern is invalid, so it wont be parsed:
|
570
|
-
# URITemplate::Draft7.try_convert('{foo') #=> nil
|
571
|
-
#
|
572
|
-
def try_convert(x)
|
573
|
-
if x.kind_of? self
|
574
|
-
return x
|
575
|
-
elsif x.kind_of? String and valid? x
|
576
|
-
return new(x)
|
577
|
-
elsif x.kind_of? URITemplate::Colon
|
578
|
-
return new( x.tokens.map{|tk|
|
579
|
-
if tk.literal?
|
580
|
-
Literal.new(tk.string)
|
581
|
-
else
|
582
|
-
Expression.new([[tk.variables.first, false, 0]])
|
583
|
-
end
|
584
|
-
})
|
585
|
-
else
|
586
|
-
return nil
|
587
|
-
end
|
588
|
-
end
|
589
|
-
|
590
|
-
# Like {.try_convert}, but raises an ArgumentError, when the conversion failed.
|
591
|
-
#
|
592
|
-
# @raise ArgumentError
|
593
|
-
def convert(x)
|
594
|
-
o = self.try_convert(x)
|
595
|
-
if o.nil?
|
596
|
-
raise ArgumentError, "Expected to receive something that can be converted to an #{self.class}, but got: #{x.inspect}."
|
597
|
-
else
|
598
|
-
return o
|
599
|
-
end
|
600
|
-
end
|
601
|
-
|
602
|
-
# Tests whether a given pattern is a valid template pattern.
|
603
|
-
# @example
|
604
|
-
# URITemplate::Draft7.valid? 'foo' #=> true
|
605
|
-
# URITemplate::Draft7.valid? '{foo}' #=> true
|
606
|
-
# URITemplate::Draft7.valid? '{foo' #=> false
|
607
|
-
def valid?(pattern)
|
608
|
-
URI === pattern
|
609
|
-
end
|
610
|
-
|
611
|
-
end
|
612
|
-
|
613
|
-
extend ClassMethods
|
614
|
-
|
615
|
-
attr_reader :options
|
616
|
-
|
617
|
-
# @param String,Array either a pattern as String or an Array of tokens
|
618
|
-
# @param Hash some options
|
619
|
-
# @option :lazy If true the pattern will be parsed on first access, this also means that syntax errors will not be detected unless accessed.
|
620
|
-
def initialize(pattern_or_tokens,options={})
|
621
|
-
@options = options.dup.freeze
|
622
|
-
if pattern_or_tokens.kind_of? String
|
623
|
-
@pattern = pattern_or_tokens.dup
|
624
|
-
@pattern.freeze
|
625
|
-
unless @options[:lazy]
|
626
|
-
self.tokens
|
627
|
-
end
|
628
|
-
elsif pattern_or_tokens.kind_of? Array
|
629
|
-
@tokens = pattern_or_tokens.dup
|
630
|
-
@tokens.freeze
|
631
|
-
else
|
632
|
-
raise ArgumentError, "Expected to receive a pattern string, but got #{pattern_or_tokens.inspect}"
|
633
|
-
end
|
634
|
-
end
|
635
|
-
|
636
|
-
# @method expand(variables = {})
|
637
|
-
# Expands the template with the given variables.
|
638
|
-
# The expansion should be compatible to uritemplate spec draft 7 ( http://tools.ietf.org/html/draft-gregorio-uritemplate-07 ).
|
639
|
-
# @note
|
640
|
-
# All keys of the supplied hash should be strings as anything else won't be recognised.
|
641
|
-
# @note
|
642
|
-
# 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.
|
643
|
-
# @example
|
644
|
-
# URITemplate::Draft7.new('{foo}').expand('foo'=>'bar') #=> 'bar'
|
645
|
-
# URITemplate::Draft7.new('{?args*}').expand('args'=>{'key'=>'value'}) #=> '?key=value'
|
646
|
-
# URITemplate::Draft7.new('{undef}').expand() #=> ''
|
647
|
-
#
|
648
|
-
# @param variables Hash
|
649
|
-
# @return String
|
650
|
-
|
651
|
-
# 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 {#===}.
|
652
|
-
#
|
653
|
-
# @example
|
654
|
-
# tpl = URITemplate::Draft7.new('/foo/{bar}/')
|
655
|
-
# regex = tpl.to_r
|
656
|
-
# regex === '/foo/baz/' #=> true
|
657
|
-
# regex === '/foz/baz/' #=> false
|
658
|
-
#
|
659
|
-
# @return Regexp
|
660
|
-
def to_r
|
661
|
-
@regexp ||= begin
|
662
|
-
source = tokens.map(&:to_r_source)
|
663
|
-
source.unshift('\A')
|
664
|
-
source.push('\z')
|
665
|
-
Regexp.new( source.join, Utils::KCODE_UTF8)
|
666
|
-
end
|
667
|
-
end
|
668
|
-
|
669
|
-
# Extracts variables from a uri ( given as string ) or an instance of MatchData ( which was matched by the regexp of this template.
|
670
|
-
# The actual result depends on the value of post_processing.
|
671
|
-
# This argument specifies whether pair arrays should be converted to hashes.
|
672
|
-
#
|
673
|
-
# @example Default Processing
|
674
|
-
# URITemplate::Draft7.new('{var}').extract('value') #=> {'var'=>'value'}
|
675
|
-
# URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2') #=> {'args'=>{'a'=>'1','b'=>'2'}}
|
676
|
-
# URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2') #=> {'arg'=>'2'}
|
677
|
-
#
|
678
|
-
# @example No Processing
|
679
|
-
# URITemplate::Draft7.new('{var}').extract('value', URITemplate::Draft7::NO_PROCESSING) #=> [['var','value']]
|
680
|
-
# URITemplate::Draft7.new('{&args*}').extract('&a=1&b=2', URITemplate::Draft7::NO_PROCESSING) #=> [['args',[['a','1'],['b','2']]]]
|
681
|
-
# URITemplate::Draft7.new('{&arg,arg}').extract('&arg=1&arg=2', URITemplate::Draft7::NO_PROCESSING) #=> [['arg','1'],['arg','2']]
|
682
|
-
#
|
683
|
-
# @raise Encoding::InvalidByteSequenceError when the given uri was not properly encoded.
|
684
|
-
# @raise Encoding::UndefinedConversionError when the given uri could not be converted to utf-8.
|
685
|
-
# @raise Encoding::CompatibilityError when the given uri could not be converted to utf-8.
|
686
|
-
#
|
687
|
-
# @param [String,MatchData] Uri_or_MatchData A uri or a matchdata from which the variables should be extracted.
|
688
|
-
# @param [Array] Processing Specifies which processing should be done.
|
689
|
-
#
|
690
|
-
# @note
|
691
|
-
# 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:
|
692
|
-
# a_tpl.expand( a_tpl.extract( an_uri ) ) == an_uri
|
693
|
-
#
|
694
|
-
# @example Extraction cruces
|
695
|
-
# two_lists = URITemplate::Draft7.new('{listA*,listB*}')
|
696
|
-
# uri = two_lists.expand('listA'=>[1,2],'listB'=>[3,4]) #=> "1,2,3,4"
|
697
|
-
# variables = two_lists.extract( uri ) #=> {'listA'=>["1","2","3","4"],'listB'=>nil}
|
698
|
-
# # However, like said in the note:
|
699
|
-
# two_lists.expand( variables ) == uri #=> true
|
700
|
-
#
|
701
|
-
# @note
|
702
|
-
# The current implementation drops duplicated variables instead of checking them.
|
703
|
-
#
|
704
|
-
#
|
705
|
-
def extract(uri_or_match, post_processing = DEFAULT_PROCESSING )
|
706
|
-
if uri_or_match.kind_of? String
|
707
|
-
m = self.to_r.match(uri_or_match)
|
708
|
-
elsif uri_or_match.kind_of?(MatchData)
|
709
|
-
if uri_or_match.respond_to?(:regexp) and uri_or_match.regexp != self.to_r
|
710
|
-
raise ArgumentError, "Trying to extract variables from MatchData which was not generated by this template."
|
711
|
-
end
|
712
|
-
m = uri_or_match
|
713
|
-
elsif uri_or_match.nil?
|
714
|
-
return nil
|
715
|
-
else
|
716
|
-
raise ArgumentError, "Expected to receive a String or a MatchData, but got #{uri_or_match.inspect}."
|
717
|
-
end
|
718
|
-
if m.nil?
|
719
|
-
return nil
|
720
|
-
else
|
721
|
-
result = extract_matchdata(m, post_processing)
|
722
|
-
if block_given?
|
723
|
-
return yield result
|
724
|
-
end
|
725
|
-
|
726
|
-
return result
|
727
|
-
end
|
728
|
-
end
|
729
|
-
|
730
|
-
# Extracts variables without any proccessing.
|
731
|
-
# This is equivalent to {#extract} with options {NO_PROCESSING}.
|
732
|
-
# @see #extract
|
733
|
-
def extract_simple(uri_or_match)
|
734
|
-
extract( uri_or_match, NO_PROCESSING )
|
735
|
-
end
|
736
|
-
|
737
|
-
# Returns the pattern for this template.
|
738
|
-
def pattern
|
739
|
-
@pattern ||= tokens.map(&:to_s).join
|
740
|
-
end
|
741
|
-
|
742
|
-
alias to_s pattern
|
743
|
-
|
744
|
-
# Compares two template patterns.
|
745
|
-
def ==(o)
|
746
|
-
this, other, this_converted, _ = URITemplate.coerce( self, o )
|
747
|
-
if this_converted
|
748
|
-
return this == other
|
749
|
-
end
|
750
|
-
return this.pattern == other.pattern
|
751
|
-
end
|
752
|
-
|
753
|
-
# @method ===(uri)
|
754
|
-
# Alias for to_r.=== . Tests whether this template matches a given uri.
|
755
|
-
# @return TrueClass, FalseClass
|
756
|
-
def_delegators :to_r, :===
|
757
|
-
|
758
|
-
# @method match(uri)
|
759
|
-
# Alias for to_r.match . Matches this template against the given uri.
|
760
|
-
# @yield MatchData
|
761
|
-
# @return MatchData, Object
|
762
|
-
def_delegators :to_r, :match
|
763
|
-
|
764
|
-
# The type of this template.
|
765
|
-
#
|
766
|
-
# @example
|
767
|
-
# tpl1 = URITemplate::Draft7.new('/foo')
|
768
|
-
# tpl2 = URITemplate.new( tpl1.pattern, tpl1.type )
|
769
|
-
# tpl1 == tpl2 #=> true
|
770
|
-
#
|
771
|
-
# @see {URITemplate#type}
|
772
|
-
def type
|
773
|
-
:draft7
|
774
|
-
end
|
775
|
-
|
776
|
-
# 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.
|
777
|
-
# Basically this is defined as:
|
778
|
-
#
|
779
|
-
# * Level 1: no operators, one variable per expansion, no variable modifiers
|
780
|
-
# * Level 2: '+' and '#' operators, one variable per expansion, no variable modifiers
|
781
|
-
# * Level 3: all operators, multiple variables per expansion, no variable modifiers
|
782
|
-
# * Level 4: all operators, multiple variables per expansion, all variable modifiers
|
783
|
-
#
|
784
|
-
# @example
|
785
|
-
# URITemplate::Draft7.new('/foo/').level #=> 1
|
786
|
-
# URITemplate::Draft7.new('/foo{bar}').level #=> 1
|
787
|
-
# URITemplate::Draft7.new('/foo{#bar}').level #=> 2
|
788
|
-
# URITemplate::Draft7.new('/foo{.bar}').level #=> 3
|
789
|
-
# URITemplate::Draft7.new('/foo{bar,baz}').level #=> 3
|
790
|
-
# URITemplate::Draft7.new('/foo{bar:20}').level #=> 4
|
791
|
-
# URITemplate::Draft7.new('/foo{bar*}').level #=> 4
|
792
|
-
#
|
793
|
-
# 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.
|
794
|
-
#
|
795
|
-
def level
|
796
|
-
tokens.map(&:level).max
|
797
|
-
end
|
798
|
-
|
799
|
-
# Tries to concatenate two templates, as if they were path segments.
|
800
|
-
# Removes double slashes or insert one if they are missing.
|
801
|
-
#
|
802
|
-
# @example
|
803
|
-
# tpl = URITemplate::Draft7.new('/xy/')
|
804
|
-
# (tpl / '/z/' ).pattern #=> '/xy/z/'
|
805
|
-
# (tpl / 'z/' ).pattern #=> '/xy/z/'
|
806
|
-
# (tpl / '{/z}' ).pattern #=> '/xy{/z}'
|
807
|
-
# (tpl / 'a' / 'b' ).pattern #=> '/xy/a/b'
|
808
|
-
#
|
809
|
-
def /(o)
|
810
|
-
this, other, this_converted, _ = URITemplate.coerce( self, o )
|
811
|
-
if this_converted
|
812
|
-
return this / other
|
813
|
-
end
|
814
|
-
|
815
|
-
if other.absolute?
|
816
|
-
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."
|
817
|
-
end
|
818
|
-
|
819
|
-
if other.pattern == ''
|
820
|
-
return self
|
821
|
-
end
|
822
|
-
# Merge!
|
823
|
-
# Analyze the last token of this an the first token of the next and try to merge them
|
824
|
-
if self.tokens.last.kind_of?(Literal)
|
825
|
-
if self.tokens.last.string[-1] == SLASH # the last token ends with an /
|
826
|
-
if other.tokens.first.kind_of? Literal
|
827
|
-
# both seems to be paths, merge them!
|
828
|
-
if other.tokens.first.string[0] == SLASH
|
829
|
-
# strip one '/'
|
830
|
-
return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string[1..-1]) ] + other.tokens[1..-1] )
|
831
|
-
else
|
832
|
-
# no problem, but we can merge them
|
833
|
-
return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string + other.tokens.first.string) ] + other.tokens[1..-1] )
|
834
|
-
end
|
835
|
-
elsif other.tokens.first.kind_of? Expression::Path
|
836
|
-
# this will automatically insert '/'
|
837
|
-
# so we can strip one '/'
|
838
|
-
return self.class.new( self.tokens[0..-2] + [ Literal.new(self.tokens.last.string[0..-2]) ] + other.tokens )
|
839
|
-
end
|
840
|
-
elsif other.tokens.first.kind_of? Literal
|
841
|
-
# okay, this template does not end with /, but the next starts with a literal => merge them!
|
842
|
-
if other.tokens.first.string[0] == SLASH
|
843
|
-
return self.class.new( self.tokens[0..-2] + [Literal.new(self.tokens.last.string + other.tokens.first.string)] + other.tokens[1..-1] )
|
844
|
-
else
|
845
|
-
return self.class.new( self.tokens[0..-2] + [Literal.new(self.tokens.last.string + '/' + other.tokens.first.string)] + other.tokens[1..-1] )
|
846
|
-
end
|
847
|
-
end
|
848
|
-
end
|
849
|
-
|
850
|
-
if other.tokens.first.kind_of?(Literal)
|
851
|
-
if other.tokens.first.string[0] == SLASH
|
852
|
-
return self.class.new( self.tokens + other.tokens )
|
853
|
-
else
|
854
|
-
return self.class.new( self.tokens + [Literal.new('/' + other.tokens.first.string)]+ other.tokens[1..-1] )
|
855
|
-
end
|
856
|
-
elsif other.tokens.first.kind_of?(Expression::Path)
|
857
|
-
return self.class.new( self.tokens + other.tokens )
|
858
|
-
else
|
859
|
-
return self.class.new( self.tokens + [Literal.new('/')] + other.tokens )
|
860
|
-
end
|
861
|
-
end
|
862
|
-
|
863
|
-
# Returns an array containing a the template tokens.
|
864
|
-
def tokens
|
865
|
-
@tokens ||= tokenize!
|
866
|
-
end
|
867
|
-
|
868
|
-
protected
|
869
|
-
# @private
|
870
|
-
def tokenize!
|
871
|
-
Tokenizer.new(pattern).to_a
|
872
|
-
end
|
873
|
-
|
874
|
-
def arity
|
875
|
-
@arity ||= tokens.inject(0){|a,t| a + t.arity }
|
876
|
-
end
|
877
|
-
|
878
|
-
# @private
|
879
|
-
def extract_matchdata(matchdata, post_processing)
|
880
|
-
bc = 1
|
881
|
-
vars = []
|
882
|
-
tokens.each{|part|
|
883
|
-
next if part.literal?
|
884
|
-
i = 0
|
885
|
-
pa = part.arity
|
886
|
-
while i < pa
|
887
|
-
vars << part.extract(i, matchdata[bc])
|
888
|
-
bc += 1
|
889
|
-
i += 1
|
890
|
-
end
|
891
|
-
}
|
892
|
-
if post_processing.include? :convert_result
|
893
|
-
if post_processing.include? :convert_values
|
894
|
-
vars.flatten!(1)
|
895
|
-
return Hash[*vars.map!{|k,v| [k,Utils.pair_array_to_hash(v)] }.flatten(1) ]
|
896
|
-
else
|
897
|
-
vars.flatten!(2)
|
898
|
-
return Hash[*vars]
|
899
|
-
end
|
900
|
-
else
|
901
|
-
if post_processing.include? :convert_value
|
902
|
-
vars.flatten!(1)
|
903
|
-
return vars.collect{|k,v| [k,Utils.pair_array_to_hash(v)] }
|
904
|
-
else
|
905
|
-
return vars.flatten(1)
|
906
|
-
end
|
907
|
-
end
|
908
|
-
end
|
909
|
-
|
910
266
|
end
|