uri_template 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +53 -0
- data/README.md +56 -0
- data/lib/uri_template/colon.rb +88 -29
- data/lib/uri_template/expression.rb +33 -0
- data/lib/uri_template/literal.rb +53 -0
- data/lib/uri_template/rfc6570/expression/named.rb +72 -0
- data/lib/uri_template/rfc6570/expression/unnamed.rb +76 -0
- data/lib/uri_template/rfc6570/expression.rb +374 -0
- data/lib/uri_template/rfc6570/regex_builder.rb +117 -0
- data/lib/uri_template/rfc6570.rb +18 -536
- data/lib/uri_template/token.rb +64 -0
- data/lib/uri_template/utils.rb +47 -10
- data/lib/uri_template.rb +189 -88
- data/uri_template.gemspec +4 -4
- metadata +14 -10
- data/CHANGELOG +0 -46
- data/README +0 -67
- data/lib/uri_template/draft7.rb +0 -266
@@ -0,0 +1,374 @@
|
|
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
|
21
|
+
|
22
|
+
# @private
|
23
|
+
class Expression < Token
|
24
|
+
|
25
|
+
include URITemplate::Expression
|
26
|
+
|
27
|
+
attr_reader :variables
|
28
|
+
|
29
|
+
def initialize(vars)
|
30
|
+
@variable_specs = vars
|
31
|
+
@variables = vars.map(&:first)
|
32
|
+
@variables.uniq!
|
33
|
+
end
|
34
|
+
|
35
|
+
PREFIX = ''.freeze
|
36
|
+
SEPARATOR = ','.freeze
|
37
|
+
PAIR_CONNECTOR = '='.freeze
|
38
|
+
PAIR_IF_EMPTY = true
|
39
|
+
LIST_CONNECTOR = ','.freeze
|
40
|
+
BASE_LEVEL = 1
|
41
|
+
|
42
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved]
|
43
|
+
|
44
|
+
NAMED = false
|
45
|
+
OPERATOR = ''
|
46
|
+
|
47
|
+
def level
|
48
|
+
if @variable_specs.none?{|_,expand,ml| expand || (ml > 0) }
|
49
|
+
if @variable_specs.size == 1
|
50
|
+
return self.class::BASE_LEVEL
|
51
|
+
else
|
52
|
+
return 3
|
53
|
+
end
|
54
|
+
else
|
55
|
+
return 4
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def expands?
|
60
|
+
@variable_specs.any?{|_,expand,_| expand }
|
61
|
+
end
|
62
|
+
|
63
|
+
def arity
|
64
|
+
@variable_specs.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def expand( vars )
|
68
|
+
result = []
|
69
|
+
@variable_specs.each{| var, expand , max_length |
|
70
|
+
unless vars[var].nil?
|
71
|
+
result.push(*expand_one(var, vars[var], expand, max_length))
|
72
|
+
end
|
73
|
+
}
|
74
|
+
if result.any?
|
75
|
+
return (self.class::PREFIX + result.join(self.class::SEPARATOR))
|
76
|
+
else
|
77
|
+
return ''
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract(position,matched)
|
82
|
+
name, expand, max_length = @variable_specs[position]
|
83
|
+
if matched.nil?
|
84
|
+
return [[ name , extracted_nil ]]
|
85
|
+
end
|
86
|
+
if expand
|
87
|
+
it = URITemplate::RegexpEnumerator.new(self.class.hash_extractor(max_length), :rest => :raise)
|
88
|
+
if position == 0
|
89
|
+
matched = "#{self.class::SEPARATOR}#{matched}"
|
90
|
+
end
|
91
|
+
splitted = it.each(matched)\
|
92
|
+
.map do |match|
|
93
|
+
raise match.inspect if match.kind_of? String
|
94
|
+
[ decode(match[1]), decode(match[2], false) ]
|
95
|
+
end
|
96
|
+
return after_expand(name, splitted)
|
97
|
+
end
|
98
|
+
|
99
|
+
return [ [ name, decode( matched ) ] ]
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
return '{' + self.class::OPERATOR + @variable_specs.map{|name,expand,max_length| name + (expand ? '*': '') + (max_length > 0 ? (':' + max_length.to_s) : '') }.join(',') + '}'
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def expand_one( name, value, expand, max_length)
|
109
|
+
if length_limited?(max_length)
|
110
|
+
if value.kind_of?(Array) or value.kind_of?(Hash)
|
111
|
+
raise InvalidValue::LengthLimitInapplicable.new(name,value)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
if value.kind_of?(Hash) or Utils.pair_array?(value)
|
115
|
+
return transform_hash(name, value, expand, max_length)
|
116
|
+
elsif value.kind_of? Array
|
117
|
+
return transform_array(name, value, expand, max_length)
|
118
|
+
else
|
119
|
+
return self_pair(name, value, max_length)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def length_limited?(max_length)
|
124
|
+
max_length > 0
|
125
|
+
end
|
126
|
+
|
127
|
+
def extracted_nil
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
module ClassMethods
|
134
|
+
|
135
|
+
def hash_extractors
|
136
|
+
@hash_extractors ||= Hash.new{|hsh, key| hsh[key] = generate_hash_extractor(key) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def hash_extractor(max_length)
|
140
|
+
return hash_extractors[max_length]
|
141
|
+
end
|
142
|
+
|
143
|
+
def generate_hash_extractor(max_length)
|
144
|
+
source = regex_builder
|
145
|
+
source.push('\\A')
|
146
|
+
source.escaped_separator
|
147
|
+
source.capture do
|
148
|
+
source.character_class('+').reluctant
|
149
|
+
end
|
150
|
+
source.group do
|
151
|
+
source.escaped_pair_connector
|
152
|
+
source.capture do
|
153
|
+
source.character_class(max_length,0).reluctant
|
154
|
+
end
|
155
|
+
end.length('?')
|
156
|
+
source.lookahead do
|
157
|
+
source.push '\\z'
|
158
|
+
source.push '|'
|
159
|
+
source.escaped_separator
|
160
|
+
source.push '[^'
|
161
|
+
source.escaped_separator
|
162
|
+
source.push ']'
|
163
|
+
end
|
164
|
+
return Regexp.new( source.join , Utils::KCODE_UTF8)
|
165
|
+
end
|
166
|
+
|
167
|
+
def regex_builder
|
168
|
+
RegexBuilder.new(self)
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
extend ClassMethods
|
174
|
+
|
175
|
+
def escape(x)
|
176
|
+
Utils.escape_url(Utils.object_to_param(x))
|
177
|
+
end
|
178
|
+
|
179
|
+
def unescape(x)
|
180
|
+
Utils.unescape_url(x)
|
181
|
+
end
|
182
|
+
|
183
|
+
def regex_builder
|
184
|
+
self.class.regex_builder
|
185
|
+
end
|
186
|
+
|
187
|
+
SPLITTER = /^(,+)|([^,]+)/
|
188
|
+
|
189
|
+
COMMA = ",".freeze
|
190
|
+
|
191
|
+
def decode(x, split = true)
|
192
|
+
if x.nil?
|
193
|
+
return extracted_nil
|
194
|
+
elsif split
|
195
|
+
result = []
|
196
|
+
# Add a comma if the last character is a comma
|
197
|
+
# seems weird but is more compatible than changing
|
198
|
+
# the regex.
|
199
|
+
x += COMMA if x[-1..-1] == COMMA
|
200
|
+
URITemplate::RegexpEnumerator.new(SPLITTER, :rest => :raise).each(x) do |match|
|
201
|
+
if match[1]
|
202
|
+
next if match[1].size == 1
|
203
|
+
result << match[1][0..-3]
|
204
|
+
elsif match[2]
|
205
|
+
result << unescape(match[2])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
case(result.size)
|
209
|
+
when 0 then ''
|
210
|
+
when 1 then result.first
|
211
|
+
else result
|
212
|
+
end
|
213
|
+
else
|
214
|
+
unescape(x)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def cut(str,chars)
|
219
|
+
if chars > 0
|
220
|
+
md = Regexp.compile("\\A#{self.class::CHARACTER_CLASS[:class]}{0,#{chars.to_s}}", Utils::KCODE_UTF8).match(str)
|
221
|
+
return md[0]
|
222
|
+
else
|
223
|
+
return str
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def pair(key, value, max_length = 0, &block)
|
228
|
+
ek = key
|
229
|
+
if block
|
230
|
+
ev = value.map(&block).join(self.class::LIST_CONNECTOR)
|
231
|
+
else
|
232
|
+
ev = escape(value)
|
233
|
+
end
|
234
|
+
if !self.class::PAIR_IF_EMPTY and ev.size == 0
|
235
|
+
return ek
|
236
|
+
else
|
237
|
+
return ek + self.class::PAIR_CONNECTOR + cut( ev, max_length )
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def transform_hash(name, hsh, expand , max_length)
|
242
|
+
if expand
|
243
|
+
hsh.map{|key,value| pair(escape(key),value) }
|
244
|
+
elsif hsh.none? && !self.class::NAMED
|
245
|
+
[]
|
246
|
+
else
|
247
|
+
[ self_pair(name,hsh){|key,value| escape(key)+self.class::LIST_CONNECTOR+escape(value)} ]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def transform_array(name, ary, expand , max_length)
|
252
|
+
if expand
|
253
|
+
ary.map{|value| self_pair(name,value) }
|
254
|
+
elsif ary.none? && !self.class::NAMED
|
255
|
+
[]
|
256
|
+
else
|
257
|
+
[ self_pair(name, ary){|value| escape(value) } ]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
require 'uri_template/rfc6570/expression/named'
|
263
|
+
require 'uri_template/rfc6570/expression/unnamed'
|
264
|
+
|
265
|
+
class Expression::Basic < Expression::Unnamed
|
266
|
+
end
|
267
|
+
|
268
|
+
class Expression::Reserved < Expression::Unnamed
|
269
|
+
|
270
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
271
|
+
OPERATOR = '+'.freeze
|
272
|
+
BASE_LEVEL = 2
|
273
|
+
|
274
|
+
def escape(x)
|
275
|
+
Utils.escape_uri(Utils.object_to_param(x))
|
276
|
+
end
|
277
|
+
|
278
|
+
def unescape(x)
|
279
|
+
Utils.unescape_uri(x)
|
280
|
+
end
|
281
|
+
|
282
|
+
def scheme?
|
283
|
+
true
|
284
|
+
end
|
285
|
+
|
286
|
+
def host?
|
287
|
+
true
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
class Expression::Fragment < Expression::Unnamed
|
293
|
+
|
294
|
+
CHARACTER_CLASS = CHARACTER_CLASSES[:unreserved_reserved_pct]
|
295
|
+
PREFIX = '#'.freeze
|
296
|
+
OPERATOR = '#'.freeze
|
297
|
+
BASE_LEVEL = 2
|
298
|
+
|
299
|
+
def escape(x)
|
300
|
+
Utils.escape_uri(Utils.object_to_param(x))
|
301
|
+
end
|
302
|
+
|
303
|
+
def unescape(x)
|
304
|
+
Utils.unescape_uri(x)
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
308
|
+
|
309
|
+
class Expression::Label < Expression::Unnamed
|
310
|
+
|
311
|
+
SEPARATOR = '.'.freeze
|
312
|
+
PREFIX = '.'.freeze
|
313
|
+
OPERATOR = '.'.freeze
|
314
|
+
BASE_LEVEL = 3
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
class Expression::Path < Expression::Unnamed
|
319
|
+
|
320
|
+
SEPARATOR = '/'.freeze
|
321
|
+
PREFIX = '/'.freeze
|
322
|
+
OPERATOR = '/'.freeze
|
323
|
+
BASE_LEVEL = 3
|
324
|
+
|
325
|
+
def starts_with_slash?
|
326
|
+
true
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
class Expression::PathParameters < Expression::Named
|
332
|
+
|
333
|
+
SEPARATOR = ';'.freeze
|
334
|
+
PREFIX = ';'.freeze
|
335
|
+
NAMED = true
|
336
|
+
PAIR_IF_EMPTY = false
|
337
|
+
OPERATOR = ';'.freeze
|
338
|
+
BASE_LEVEL = 3
|
339
|
+
|
340
|
+
end
|
341
|
+
|
342
|
+
class Expression::FormQuery < Expression::Named
|
343
|
+
|
344
|
+
SEPARATOR = '&'.freeze
|
345
|
+
PREFIX = '?'.freeze
|
346
|
+
NAMED = true
|
347
|
+
OPERATOR = '?'.freeze
|
348
|
+
BASE_LEVEL = 3
|
349
|
+
|
350
|
+
end
|
351
|
+
|
352
|
+
class Expression::FormQueryContinuation < Expression::Named
|
353
|
+
|
354
|
+
SEPARATOR = '&'.freeze
|
355
|
+
PREFIX = '&'.freeze
|
356
|
+
NAMED = true
|
357
|
+
OPERATOR = '&'.freeze
|
358
|
+
BASE_LEVEL = 3
|
359
|
+
|
360
|
+
end
|
361
|
+
|
362
|
+
# @private
|
363
|
+
OPERATORS = {
|
364
|
+
'' => Expression::Basic,
|
365
|
+
'+' => Expression::Reserved,
|
366
|
+
'#' => Expression::Fragment,
|
367
|
+
'.' => Expression::Label,
|
368
|
+
'/' => Expression::Path,
|
369
|
+
';' => Expression::PathParameters,
|
370
|
+
'?' => Expression::FormQuery,
|
371
|
+
'&' => Expression::FormQueryContinuation
|
372
|
+
}
|
373
|
+
|
374
|
+
end
|
@@ -0,0 +1,117 @@
|
|
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
|
21
|
+
|
22
|
+
class RegexBuilder
|
23
|
+
|
24
|
+
def initialize(expression_class)
|
25
|
+
@expression_class = expression_class
|
26
|
+
@source = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def <<(arg)
|
30
|
+
@source << arg
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def push(*args)
|
35
|
+
@source.push(*args)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def escaped_pair_connector
|
40
|
+
self << Regexp.escape(@expression_class::PAIR_CONNECTOR)
|
41
|
+
end
|
42
|
+
|
43
|
+
def escaped_separator
|
44
|
+
self << Regexp.escape(@expression_class::SEPARATOR)
|
45
|
+
end
|
46
|
+
|
47
|
+
def escaped_prefix
|
48
|
+
self << Regexp.escape(@expression_class::PREFIX)
|
49
|
+
end
|
50
|
+
|
51
|
+
def join
|
52
|
+
return @source.join
|
53
|
+
end
|
54
|
+
|
55
|
+
def length(*args)
|
56
|
+
self << format_length(*args)
|
57
|
+
end
|
58
|
+
|
59
|
+
def character_class_with_comma(max_length=0, min = 0)
|
60
|
+
self << @expression_class::CHARACTER_CLASS[:class_with_comma] << format_length(max_length, min)
|
61
|
+
end
|
62
|
+
|
63
|
+
def character_class(max_length=0, min = 0)
|
64
|
+
self << @expression_class::CHARACTER_CLASS[:class] << format_length(max_length, min)
|
65
|
+
end
|
66
|
+
|
67
|
+
def reluctant
|
68
|
+
self << '?'
|
69
|
+
end
|
70
|
+
|
71
|
+
def group(capture = false)
|
72
|
+
self << '('
|
73
|
+
self << '?:' unless capture
|
74
|
+
yield
|
75
|
+
self << ')'
|
76
|
+
end
|
77
|
+
|
78
|
+
def negative_lookahead
|
79
|
+
self << '(?!'
|
80
|
+
yield
|
81
|
+
self << ')'
|
82
|
+
end
|
83
|
+
|
84
|
+
def lookahead
|
85
|
+
self << '(?='
|
86
|
+
yield
|
87
|
+
self << ')'
|
88
|
+
end
|
89
|
+
|
90
|
+
def capture(&block)
|
91
|
+
group(true, &block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def separated_list(first = true, length = 0, min = 1, &block)
|
95
|
+
if first
|
96
|
+
yield
|
97
|
+
min -= 1
|
98
|
+
end
|
99
|
+
self.push('(?:').escaped_separator
|
100
|
+
yield
|
101
|
+
self.push(')').length(length, min)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def format_length(len, min = 0)
|
107
|
+
return len if len.kind_of? String
|
108
|
+
return '{'+min.to_s+','+len.to_s+'}' if len.kind_of?(Numeric) and len > 0
|
109
|
+
return '*' if min == 0
|
110
|
+
return '+' if min == 1
|
111
|
+
return '{'+min.to_s+',}'
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|