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.
@@ -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