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 +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
|