uri_template 0.2.1 → 0.3.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 +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
|