mustermann-uri-template 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8d99fdcae417aa135df269a93872d1abec9ba1f3
4
+ data.tar.gz: 33eaef99d2e9442396c2456d16a5df1c40960079
5
+ SHA512:
6
+ metadata.gz: 83b7c599cd1a1a8c599f28e1a99ff9958739ae989688c8b666b3f621282d1f1035adf527ec4aba76e3912b520b13a30c597505d0db88235b09a537667aadccbe
7
+ data.tar.gz: 306ebd2de32b51d42eb0fad5893e5bb87def3f40213b3198da50fc73c09726b421b2d4c0030b237a3a418fb35ed0ff0290d07aa2a8c65ac072033420fe7401f7
@@ -0,0 +1,84 @@
1
+ # URI Template Syntax for Mustermann
2
+
3
+ This gem implements the `uri-template` (or `template`) pattern type for Mustermann. It is compatible with [RFC 6570](https://tools.ietf.org/html/rfc6570) (level 4), [JSON API](http://jsonapi.org/), [JSON Home Documents](http://tools.ietf.org/html/draft-nottingham-json-home-02) and [many more](https://code.google.com/p/uri-templates/wiki/Implementations)
4
+
5
+ ## Overview
6
+
7
+ **Supported options:**
8
+ `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`.
9
+
10
+ Please keep the following in mind:
11
+
12
+ > "Some URI Templates can be used in reverse for the purpose of variable matching: comparing the template to a fully formed URI in order to extract the variable parts from that URI and assign them to the named variables. Variable matching only works well if the template expressions are delimited by the beginning or end of the URI or by characters that cannot be part of the expansion, such as reserved characters surrounding a simple string expression. In general, regular expression languages are better suited for variable matching."
13
+ > — *RFC 6570, Sec 1.5: "Limitations"*
14
+
15
+ ``` ruby
16
+ require 'mustermann'
17
+
18
+ pattern = Mustermann.new('/{example}', type: :template)
19
+ pattern === "/foo.bar" # => true
20
+ pattern === "/foo/bar" # => false
21
+ pattern.params("/foo.bar") # => { "example" => "foo.bar" }
22
+ pattern.params("/foo/bar") # => nil
23
+
24
+ pattern = Mustermann.new("{/segments*}/{page}{.ext,cmpr:2}", type: :template)
25
+ pattern.params("/a/b/c.tar.gz") # => {"segments"=>["a","b"], "page"=>"c", "ext"=>"tar", "cmpr"=>"gz"}
26
+ ```
27
+
28
+ ## Generating URI Templates
29
+
30
+ You do not need to use URI templates (and this gem) if all you want is reusing them for hypermedia links. Most other pattern types support generating these (via `#to_pattern`):
31
+
32
+ ``` ruby
33
+ require 'mustermann'
34
+
35
+ Mustermann.new('/:name').to_templates # => ['/{name}']
36
+ ```
37
+
38
+ Moreover, Mustermann's default pattern type implements a subset of URI templates (`{capture}` and `{+capture}`) and can therefore also be used for simple templates/
39
+
40
+ ``` ruby
41
+ require 'mustermann'
42
+
43
+ Mustermann.new('/{name}').expand(name: "example") # => "/example"
44
+ ```
45
+
46
+ ## Syntax
47
+
48
+ <table>
49
+ <thead>
50
+ <tr>
51
+ <th>Syntax Element</th>
52
+ <th>Description</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ <tr>
57
+ <td><b>&#123;</b><i>o</i> <i>var</i> <i>m</i><b>,</b> <i>var</i> <i>m</i><b>,</b> ...<b>&#125;</b></td>
58
+ <td>
59
+ Captures expansion.
60
+ Operator <i>o</i>: <code>+ # . / ; ? &amp;</tt> or none.
61
+ Modifier <i>m</i>: <code>:num *</tt> or none.
62
+ </td>
63
+ </tr>
64
+ <tr>
65
+ <td><b>/</b></td>
66
+ <td>
67
+ Matches forward slash. Does not match URI encoded version of forward slash.
68
+ </td>
69
+ </tr>
70
+ <tr>
71
+ <td><i>any other character</i></td>
72
+ <td>Matches exactly that character or a URI encoded version of it.</td>
73
+ </tr>
74
+ </tbody>
75
+ </table>
76
+
77
+ The operators `+` and `#` will always match non-greedy, whereas all other operators match semi-greedy by default.
78
+ All modifiers and operators are supported. However, it does not parse lists as single values without the *explode* modifier (aka *star*).
79
+ Parametric operators (`;`, `?` and `&`) currently only match parameters in given order.
80
+
81
+ Note that it differs from URI templates in that it takes the unescaped version of special character instead of the escaped version.
82
+
83
+ If you reuse the exact same templates and expose them via an external API meant for expansion,
84
+ you should set `uri_decode` to `false` in order to conform with the specification.
@@ -0,0 +1,61 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+
4
+ module Mustermann
5
+ # URI template pattern implementation.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/{foo}') === '/bar' # => true
9
+ #
10
+ # @see Mustermann::Pattern
11
+ # @see file:README.md#template Syntax description in the README
12
+ # @see http://tools.ietf.org/html/rfc6570 RFC 6570
13
+ class Template < AST::Pattern
14
+ register :template, :uri_template
15
+
16
+ on ?{ do |char|
17
+ variable = proc do
18
+ start = pos
19
+ match = expect(/(?<name>\w+)(?:\:(?<prefix>\d{1,4})|(?<explode>\*))?/)
20
+ node(:variable, match[:name], prefix: match[:prefix], explode: match[:explode], start: start)
21
+ end
22
+
23
+ operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/)
24
+ expression = node(:expression, [variable[]], operator: operator) { variable[] if scan(?,) }
25
+ expression if expect(?})
26
+ end
27
+
28
+ on(?}) { |c| unexpected(c) }
29
+
30
+ # @!visibility private
31
+ def compile(*args, **options)
32
+ @split_params = {}
33
+ super(*args, split_params: @split_params, **options)
34
+ end
35
+
36
+ # @!visibility private
37
+ def map_param(key, value)
38
+ return super unless variable = @split_params[key]
39
+ value = value.split variable[:separator]
40
+ value.map! { |e| e.sub(/\A#{key}=/, '') } if variable[:parametric]
41
+ value.map! { |e| super(key, e) }
42
+ end
43
+
44
+ # @!visibility private
45
+ def always_array?(key)
46
+ @split_params.include? key
47
+ end
48
+
49
+ # Identity patterns support generating templates (the logic is quite complex, though).
50
+ #
51
+ # @example (see Mustermann::Pattern#to_templates)
52
+ # @param (see Mustermann::Pattern#to_templates)
53
+ # @return (see Mustermann::Pattern#to_templates)
54
+ # @see Mustermann::Pattern#to_templates
55
+ def to_templates
56
+ [to_s]
57
+ end
58
+
59
+ private :compile, :map_param, :always_array?
60
+ end
61
+ end
@@ -0,0 +1 @@
1
+ require 'mustermann/template'
@@ -0,0 +1,18 @@
1
+ $:.unshift File.expand_path("../../mustermann/lib", __FILE__)
2
+ require "mustermann/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mustermann-uri-template"
6
+ s.version = Mustermann::VERSION
7
+ s.author = "Konstantin Haase"
8
+ s.email = "konstantin.mailinglists@googlemail.com"
9
+ s.homepage = "https://github.com/rkh/mustermann"
10
+ s.summary = %q{URI template syntax for Mustermann}
11
+ s.description = %q{Adds URI template style patterns to Mustermman}
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.1.0'
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.add_dependency 'mustermann', Mustermann::VERSION
18
+ end
@@ -0,0 +1,841 @@
1
+ require 'support'
2
+ require 'mustermann/template'
3
+
4
+ describe Mustermann::Template do
5
+ extend Support::Pattern
6
+
7
+ pattern '' do
8
+ it { should match('') }
9
+ it { should_not match('/') }
10
+
11
+ it { should respond_to(:expand) }
12
+ it { should respond_to(:to_templates) }
13
+ end
14
+
15
+ pattern '/' do
16
+ it { should match('/') }
17
+ it { should_not match('/foo') }
18
+ end
19
+
20
+ pattern '/foo' do
21
+ it { should match('/foo') }
22
+ it { should_not match('/bar') }
23
+ it { should_not match('/foo.bar') }
24
+ end
25
+
26
+ pattern '/foo/bar' do
27
+ it { should match('/foo/bar') }
28
+ it { should_not match('/foo%2Fbar') }
29
+ it { should_not match('/foo%2fbar') }
30
+ end
31
+
32
+ pattern '/:foo' do
33
+ it { should match('/:foo') }
34
+ it { should match('/%3Afoo') }
35
+ it { should_not match('/foo') }
36
+ it { should_not match('/foo?') }
37
+ it { should_not match('/foo/bar') }
38
+ it { should_not match('/') }
39
+ it { should_not match('/foo/') }
40
+ end
41
+
42
+ pattern '/föö' do
43
+ it { should match("/f%C3%B6%C3%B6") }
44
+ end
45
+
46
+ pattern '/test$/' do
47
+ it { should match('/test$/') }
48
+ end
49
+
50
+ pattern '/te+st/' do
51
+ it { should match('/te+st/') }
52
+ it { should_not match('/test/') }
53
+ it { should_not match('/teest/') }
54
+ end
55
+
56
+ pattern "/path with spaces" do
57
+ it { should match('/path%20with%20spaces') }
58
+ it { should match('/path%2Bwith%2Bspaces') }
59
+ it { should match('/path+with+spaces') }
60
+ end
61
+
62
+ pattern '/foo&bar' do
63
+ it { should match('/foo&bar') }
64
+ end
65
+
66
+ pattern '/test.bar' do
67
+ it { should match('/test.bar') }
68
+ it { should_not match('/test0bar') }
69
+ end
70
+
71
+ pattern "/path with spaces", space_matches_plus: false do
72
+ it { should match('/path%20with%20spaces') }
73
+ it { should_not match('/path%2Bwith%2Bspaces') }
74
+ it { should_not match('/path+with+spaces') }
75
+ end
76
+
77
+ pattern "/path with spaces", uri_decode: false do
78
+ it { should_not match('/path%20with%20spaces') }
79
+ it { should_not match('/path%2Bwith%2Bspaces') }
80
+ it { should_not match('/path+with+spaces') }
81
+ end
82
+
83
+ context 'level 1' do
84
+ context 'without operator' do
85
+ pattern '/hello/{person}' do
86
+ it { should match('/hello/Frank').capturing person: 'Frank' }
87
+ it { should match('/hello/a_b~c').capturing person: 'a_b~c' }
88
+ it { should match('/hello/a.%20').capturing person: 'a.%20' }
89
+
90
+ it { should_not match('/hello/:') }
91
+ it { should_not match('/hello//') }
92
+ it { should_not match('/hello/?') }
93
+ it { should_not match('/hello/#') }
94
+ it { should_not match('/hello/[') }
95
+ it { should_not match('/hello/]') }
96
+ it { should_not match('/hello/@') }
97
+ it { should_not match('/hello/!') }
98
+ it { should_not match('/hello/*') }
99
+ it { should_not match('/hello/+') }
100
+ it { should_not match('/hello/,') }
101
+ it { should_not match('/hello/;') }
102
+ it { should_not match('/hello/=') }
103
+
104
+ example { pattern.params('/hello/Frank').should be == {'person' => 'Frank'} }
105
+ end
106
+
107
+ pattern "/{foo}/{bar}" do
108
+ it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' }
109
+ it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
110
+ it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' }
111
+ it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' }
112
+
113
+ it { should_not match('/foo%2Fbar') }
114
+ it { should_not match('/foo%2fbar') }
115
+ end
116
+ end
117
+ end
118
+
119
+ context 'level 2' do
120
+ context 'operator +' do
121
+ pattern '/hello/{+person}' do
122
+ it { should match('/hello/Frank') .capturing person: 'Frank' }
123
+ it { should match('/hello/a_b~c') .capturing person: 'a_b~c' }
124
+ it { should match('/hello/a.%20') .capturing person: 'a.%20' }
125
+ it { should match('/hello/a/%20') .capturing person: 'a/%20' }
126
+ it { should match('/hello/:') .capturing person: ?: }
127
+ it { should match('/hello//') .capturing person: ?/ }
128
+ it { should match('/hello/?') .capturing person: ?? }
129
+ it { should match('/hello/#') .capturing person: ?# }
130
+ it { should match('/hello/[') .capturing person: ?[ }
131
+ it { should match('/hello/]') .capturing person: ?] }
132
+ it { should match('/hello/@') .capturing person: ?@ }
133
+ it { should match('/hello/!') .capturing person: ?! }
134
+ it { should match('/hello/*') .capturing person: ?* }
135
+ it { should match('/hello/+') .capturing person: ?+ }
136
+ it { should match('/hello/,') .capturing person: ?, }
137
+ it { should match('/hello/;') .capturing person: ?; }
138
+ it { should match('/hello/=') .capturing person: ?= }
139
+ end
140
+
141
+ pattern "/{+foo}/{bar}" do
142
+ it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' }
143
+ it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
144
+ it { should match('/foo/bar/bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' }
145
+ it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' }
146
+ it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' }
147
+
148
+ it { should_not match('/foo%2Fbar') }
149
+ it { should_not match('/foo%2fbar') }
150
+ end
151
+ end
152
+
153
+ context 'operator #' do
154
+ pattern '/hello/{#person}' do
155
+ it { should match('/hello/#Frank') .capturing person: 'Frank' }
156
+ it { should match('/hello/#a_b~c') .capturing person: 'a_b~c' }
157
+ it { should match('/hello/#a.%20') .capturing person: 'a.%20' }
158
+ it { should match('/hello/#a/%20') .capturing person: 'a/%20' }
159
+ it { should match('/hello/#:') .capturing person: ?: }
160
+ it { should match('/hello/#/') .capturing person: ?/ }
161
+ it { should match('/hello/#?') .capturing person: ?? }
162
+ it { should match('/hello/##') .capturing person: ?# }
163
+ it { should match('/hello/#[') .capturing person: ?[ }
164
+ it { should match('/hello/#]') .capturing person: ?] }
165
+ it { should match('/hello/#@') .capturing person: ?@ }
166
+ it { should match('/hello/#!') .capturing person: ?! }
167
+ it { should match('/hello/#*') .capturing person: ?* }
168
+ it { should match('/hello/#+') .capturing person: ?+ }
169
+ it { should match('/hello/#,') .capturing person: ?, }
170
+ it { should match('/hello/#;') .capturing person: ?; }
171
+ it { should match('/hello/#=') .capturing person: ?= }
172
+
173
+
174
+ it { should_not match('/hello/Frank') }
175
+ it { should_not match('/hello/a_b~c') }
176
+ it { should_not match('/hello/a.%20') }
177
+
178
+ it { should_not match('/hello/:') }
179
+ it { should_not match('/hello//') }
180
+ it { should_not match('/hello/?') }
181
+ it { should_not match('/hello/#') }
182
+ it { should_not match('/hello/[') }
183
+ it { should_not match('/hello/]') }
184
+ it { should_not match('/hello/@') }
185
+ it { should_not match('/hello/!') }
186
+ it { should_not match('/hello/*') }
187
+ it { should_not match('/hello/+') }
188
+ it { should_not match('/hello/,') }
189
+ it { should_not match('/hello/;') }
190
+ it { should_not match('/hello/=') }
191
+
192
+
193
+ example { pattern.params('/hello/#Frank').should be == {'person' => 'Frank'} }
194
+ end
195
+
196
+ pattern "/{+foo}/{#bar}" do
197
+ it { should match('/foo/#bar') .capturing foo: 'foo', bar: 'bar' }
198
+ it { should match('/foo.bar/#bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
199
+ it { should match('/foo/bar/#bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' }
200
+ it { should match('/10.1/#te.st') .capturing foo: '10.1', bar: 'te.st' }
201
+ it { should match('/10.1.2/#te.st') .capturing foo: '10.1.2', bar: 'te.st' }
202
+
203
+ it { should_not match('/foo%2F#bar') }
204
+ it { should_not match('/foo%2f#bar') }
205
+
206
+ example { pattern.params('/hello/#Frank').should be == {'foo' => 'hello', 'bar' => 'Frank'} }
207
+ end
208
+ end
209
+ end
210
+
211
+ context 'level 3' do
212
+ context 'without operator' do
213
+ pattern "{a,b,c}" do
214
+ it { should match("~x,42,_").capturing a: '~x', b: '42', c: '_' }
215
+ it { should_not match("~x,42") }
216
+ it { should_not match("~x/42") }
217
+ it { should_not match("~x#42") }
218
+ it { should_not match("~x,42,_#42") }
219
+
220
+ example { pattern.params('d,f,g').should be == {'a' => 'd', 'b' => 'f', 'c' => 'g'} }
221
+ end
222
+ end
223
+
224
+ context 'operator +' do
225
+ pattern "{+a,b,c}" do
226
+ it { should match("~x,42,_") .capturing a: '~x', b: '42', c: '_' }
227
+ it { should match("~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' }
228
+ it { should match("~/x,42,_/42") .capturing a: '~/x', b: '42', c: '_/42' }
229
+
230
+ it { should_not match("~x,42") }
231
+ it { should_not match("~x/42") }
232
+ it { should_not match("~x#42") }
233
+ end
234
+ end
235
+
236
+ context 'operator #' do
237
+ pattern "{#a,b,c}" do
238
+ it { should match("#~x,42,_") .capturing a: '~x', b: '42', c: '_' }
239
+ it { should match("#~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' }
240
+ it { should match("#~/x,42,_#42") .capturing a: '~/x', b: '42', c: '_#42' }
241
+
242
+ it { should_not match("~x,42,_") }
243
+ it { should_not match("~x,42,_#42") }
244
+ it { should_not match("~/x,42,_#42") }
245
+
246
+ it { should_not match("~x,42") }
247
+ it { should_not match("~x/42") }
248
+ it { should_not match("~x#42") }
249
+ end
250
+ end
251
+
252
+ context 'operator .' do
253
+ pattern '/hello/{.person}' do
254
+ it { should match('/hello/.Frank') .capturing person: 'Frank' }
255
+ it { should match('/hello/.a_b~c') .capturing person: 'a_b~c' }
256
+
257
+ it { should_not match('/hello/.:') }
258
+ it { should_not match('/hello/./') }
259
+ it { should_not match('/hello/.?') }
260
+ it { should_not match('/hello/.#') }
261
+ it { should_not match('/hello/.[') }
262
+ it { should_not match('/hello/.]') }
263
+ it { should_not match('/hello/.@') }
264
+ it { should_not match('/hello/.!') }
265
+ it { should_not match('/hello/.*') }
266
+ it { should_not match('/hello/.+') }
267
+ it { should_not match('/hello/.,') }
268
+ it { should_not match('/hello/.;') }
269
+ it { should_not match('/hello/.=') }
270
+
271
+ it { should_not match('/hello/Frank') }
272
+ it { should_not match('/hello/a_b~c') }
273
+ it { should_not match('/hello/a.%20') }
274
+
275
+ it { should_not match('/hello/:') }
276
+ it { should_not match('/hello//') }
277
+ it { should_not match('/hello/?') }
278
+ it { should_not match('/hello/#') }
279
+ it { should_not match('/hello/[') }
280
+ it { should_not match('/hello/]') }
281
+ it { should_not match('/hello/@') }
282
+ it { should_not match('/hello/!') }
283
+ it { should_not match('/hello/*') }
284
+ it { should_not match('/hello/+') }
285
+ it { should_not match('/hello/,') }
286
+ it { should_not match('/hello/;') }
287
+ it { should_not match('/hello/=') }
288
+ end
289
+
290
+ pattern "{.a,b,c}" do
291
+ it { should match(".~x.42._").capturing a: '~x', b: '42', c: '_' }
292
+ it { should_not match(".~x,42") }
293
+ it { should_not match(".~x/42") }
294
+ it { should_not match(".~x#42") }
295
+ it { should_not match(".~x,42,_") }
296
+ it { should_not match("~x.42._") }
297
+ end
298
+ end
299
+
300
+ context 'operator /' do
301
+ pattern '/hello{/person}' do
302
+ it { should match('/hello/Frank') .capturing person: 'Frank' }
303
+ it { should match('/hello/a_b~c') .capturing person: 'a_b~c' }
304
+
305
+ it { should_not match('/hello//:') }
306
+ it { should_not match('/hello///') }
307
+ it { should_not match('/hello//?') }
308
+ it { should_not match('/hello//#') }
309
+ it { should_not match('/hello//[') }
310
+ it { should_not match('/hello//]') }
311
+ it { should_not match('/hello//@') }
312
+ it { should_not match('/hello//!') }
313
+ it { should_not match('/hello//*') }
314
+ it { should_not match('/hello//+') }
315
+ it { should_not match('/hello//,') }
316
+ it { should_not match('/hello//;') }
317
+ it { should_not match('/hello//=') }
318
+
319
+ it { should_not match('/hello/:') }
320
+ it { should_not match('/hello//') }
321
+ it { should_not match('/hello/?') }
322
+ it { should_not match('/hello/#') }
323
+ it { should_not match('/hello/[') }
324
+ it { should_not match('/hello/]') }
325
+ it { should_not match('/hello/@') }
326
+ it { should_not match('/hello/!') }
327
+ it { should_not match('/hello/*') }
328
+ it { should_not match('/hello/+') }
329
+ it { should_not match('/hello/,') }
330
+ it { should_not match('/hello/;') }
331
+ it { should_not match('/hello/=') }
332
+ end
333
+
334
+ pattern "{/a,b,c}" do
335
+ it { should match("/~x/42/_").capturing a: '~x', b: '42', c: '_' }
336
+ it { should_not match("/~x,42") }
337
+ it { should_not match("/~x.42") }
338
+ it { should_not match("/~x#42") }
339
+ it { should_not match("/~x,42,_") }
340
+ it { should_not match("~x/42/_") }
341
+ end
342
+ end
343
+
344
+ context 'operator ;' do
345
+ pattern '/hello/{;person}' do
346
+ it { should match('/hello/;person=Frank') .capturing person: 'Frank' }
347
+ it { should match('/hello/;person=a_b~c') .capturing person: 'a_b~c' }
348
+ it { should match('/hello/;person') .capturing person: nil }
349
+
350
+ it { should_not match('/hello/;persona=Frank') }
351
+ it { should_not match('/hello/;persona=a_b~c') }
352
+
353
+ it { should_not match('/hello/;person=:') }
354
+ it { should_not match('/hello/;person=/') }
355
+ it { should_not match('/hello/;person=?') }
356
+ it { should_not match('/hello/;person=#') }
357
+ it { should_not match('/hello/;person=[') }
358
+ it { should_not match('/hello/;person=]') }
359
+ it { should_not match('/hello/;person=@') }
360
+ it { should_not match('/hello/;person=!') }
361
+ it { should_not match('/hello/;person=*') }
362
+ it { should_not match('/hello/;person=+') }
363
+ it { should_not match('/hello/;person=,') }
364
+ it { should_not match('/hello/;person=;') }
365
+ it { should_not match('/hello/;person==') }
366
+
367
+ it { should_not match('/hello/;Frank') }
368
+ it { should_not match('/hello/;a_b~c') }
369
+ it { should_not match('/hello/;a.%20') }
370
+
371
+ it { should_not match('/hello/:') }
372
+ it { should_not match('/hello//') }
373
+ it { should_not match('/hello/?') }
374
+ it { should_not match('/hello/#') }
375
+ it { should_not match('/hello/[') }
376
+ it { should_not match('/hello/]') }
377
+ it { should_not match('/hello/@') }
378
+ it { should_not match('/hello/!') }
379
+ it { should_not match('/hello/*') }
380
+ it { should_not match('/hello/+') }
381
+ it { should_not match('/hello/,') }
382
+ it { should_not match('/hello/;') }
383
+ it { should_not match('/hello/=') }
384
+ end
385
+
386
+ pattern "{;a,b,c}" do
387
+ it { should match(";a=~x;b=42;c=_") .capturing a: '~x', b: '42', c: '_' }
388
+ it { should match(";a=~x;b;c=_") .capturing a: '~x', b: nil, c: '_' }
389
+
390
+ it { should_not match(";a=~x;c=_;b=42").capturing a: '~x', b: '42', c: '_' }
391
+
392
+ it { should_not match(";a=~x;b=42") }
393
+ it { should_not match("a=~x;b=42") }
394
+ it { should_not match(";a=~x;b=#42;c") }
395
+ it { should_not match(";a=~x,b=42,c=_") }
396
+ it { should_not match("~x;b=42;c=_") }
397
+ end
398
+ end
399
+
400
+ context 'operator ?' do
401
+ pattern '/hello/{?person}' do
402
+ it { should match('/hello/?person=Frank') .capturing person: 'Frank' }
403
+ it { should match('/hello/?person=a_b~c') .capturing person: 'a_b~c' }
404
+ it { should match('/hello/?person') .capturing person: nil }
405
+
406
+ it { should_not match('/hello/?persona=Frank') }
407
+ it { should_not match('/hello/?persona=a_b~c') }
408
+
409
+ it { should_not match('/hello/?person=:') }
410
+ it { should_not match('/hello/?person=/') }
411
+ it { should_not match('/hello/?person=?') }
412
+ it { should_not match('/hello/?person=#') }
413
+ it { should_not match('/hello/?person=[') }
414
+ it { should_not match('/hello/?person=]') }
415
+ it { should_not match('/hello/?person=@') }
416
+ it { should_not match('/hello/?person=!') }
417
+ it { should_not match('/hello/?person=*') }
418
+ it { should_not match('/hello/?person=+') }
419
+ it { should_not match('/hello/?person=,') }
420
+ it { should_not match('/hello/?person=;') }
421
+ it { should_not match('/hello/?person==') }
422
+
423
+ it { should_not match('/hello/?Frank') }
424
+ it { should_not match('/hello/?a_b~c') }
425
+ it { should_not match('/hello/?a.%20') }
426
+
427
+ it { should_not match('/hello/:') }
428
+ it { should_not match('/hello//') }
429
+ it { should_not match('/hello/?') }
430
+ it { should_not match('/hello/#') }
431
+ it { should_not match('/hello/[') }
432
+ it { should_not match('/hello/]') }
433
+ it { should_not match('/hello/@') }
434
+ it { should_not match('/hello/!') }
435
+ it { should_not match('/hello/*') }
436
+ it { should_not match('/hello/+') }
437
+ it { should_not match('/hello/,') }
438
+ it { should_not match('/hello/;') }
439
+ it { should_not match('/hello/=') }
440
+ end
441
+
442
+ pattern "{?a,b,c}" do
443
+ it { should match("?a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' }
444
+ it { should match("?a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' }
445
+
446
+ it { should_not match("?a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' }
447
+
448
+ it { should_not match("?a=~x&b=42") }
449
+ it { should_not match("a=~x&b=42") }
450
+ it { should_not match("?a=~x&b=#42&c") }
451
+ it { should_not match("?a=~x,b=42,c=_") }
452
+ it { should_not match("~x&b=42&c=_") }
453
+ end
454
+ end
455
+
456
+ context 'operator &' do
457
+ pattern '/hello/{&person}' do
458
+ it { should match('/hello/&person=Frank') .capturing person: 'Frank' }
459
+ it { should match('/hello/&person=a_b~c') .capturing person: 'a_b~c' }
460
+ it { should match('/hello/&person') .capturing person: nil }
461
+
462
+ it { should_not match('/hello/&persona=Frank') }
463
+ it { should_not match('/hello/&persona=a_b~c') }
464
+
465
+ it { should_not match('/hello/&person=:') }
466
+ it { should_not match('/hello/&person=/') }
467
+ it { should_not match('/hello/&person=?') }
468
+ it { should_not match('/hello/&person=#') }
469
+ it { should_not match('/hello/&person=[') }
470
+ it { should_not match('/hello/&person=]') }
471
+ it { should_not match('/hello/&person=@') }
472
+ it { should_not match('/hello/&person=!') }
473
+ it { should_not match('/hello/&person=*') }
474
+ it { should_not match('/hello/&person=+') }
475
+ it { should_not match('/hello/&person=,') }
476
+ it { should_not match('/hello/&person=;') }
477
+ it { should_not match('/hello/&person==') }
478
+
479
+ it { should_not match('/hello/&Frank') }
480
+ it { should_not match('/hello/&a_b~c') }
481
+ it { should_not match('/hello/&a.%20') }
482
+
483
+ it { should_not match('/hello/:') }
484
+ it { should_not match('/hello//') }
485
+ it { should_not match('/hello/?') }
486
+ it { should_not match('/hello/#') }
487
+ it { should_not match('/hello/[') }
488
+ it { should_not match('/hello/]') }
489
+ it { should_not match('/hello/@') }
490
+ it { should_not match('/hello/!') }
491
+ it { should_not match('/hello/*') }
492
+ it { should_not match('/hello/+') }
493
+ it { should_not match('/hello/,') }
494
+ it { should_not match('/hello/;') }
495
+ it { should_not match('/hello/=') }
496
+ end
497
+
498
+ pattern "{&a,b,c}" do
499
+ it { should match("&a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' }
500
+ it { should match("&a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' }
501
+
502
+ it { should_not match("&a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' }
503
+
504
+ it { should_not match("&a=~x&b=42") }
505
+ it { should_not match("a=~x&b=42") }
506
+ it { should_not match("&a=~x&b=#42&c") }
507
+ it { should_not match("&a=~x,b=42,c=_") }
508
+ it { should_not match("~x&b=42&c=_") }
509
+ end
510
+ end
511
+ end
512
+
513
+ context 'level 4' do
514
+ context 'without operator' do
515
+ context 'prefix' do
516
+ pattern '{a:3}/bar' do
517
+ it { should match('foo/bar') .capturing a: 'foo' }
518
+ it { should match('fo/bar') .capturing a: 'fo' }
519
+ it { should match('f/bar') .capturing a: 'f' }
520
+ it { should_not match('fooo/bar') }
521
+ end
522
+
523
+ pattern '{a:3}{b}' do
524
+ it { should match('foobar') .capturing a: 'foo', b: 'bar' }
525
+ end
526
+ end
527
+
528
+ context 'expand' do
529
+ pattern '{a*}' do
530
+ it { should match('a') .capturing a: 'a' }
531
+ it { should match('a,b') .capturing a: 'a,b' }
532
+ it { should match('a,b,c') .capturing a: 'a,b,c' }
533
+ it { should_not match('a,b/c') }
534
+ it { should_not match('a,') }
535
+
536
+ example { pattern.params('a').should be == { 'a' => ['a'] }}
537
+ example { pattern.params('a,b').should be == { 'a' => ['a', 'b'] }}
538
+ end
539
+
540
+ pattern '{a*},{b}' do
541
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
542
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
543
+ it { should_not match('a,b/c') }
544
+ it { should_not match('a,') }
545
+
546
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
547
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
548
+ end
549
+
550
+ pattern '{a*,b}' do
551
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
552
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
553
+ it { should_not match('a,b/c') }
554
+ it { should_not match('a,') }
555
+
556
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
557
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
558
+ end
559
+ end
560
+ end
561
+
562
+ context 'operator +' do
563
+ pattern '/{a}/{+b}' do
564
+ it { should match('/foo/bar/baz').capturing(a: 'foo', b: 'bar/baz') }
565
+ it { should expand(a: 'foo/bar', b: 'foo/bar').to('/foo%2Fbar/foo/bar') }
566
+ end
567
+
568
+ context 'prefix' do
569
+ pattern '{+a:3}/bar' do
570
+ it { should match('foo/bar') .capturing a: 'foo' }
571
+ it { should match('fo/bar') .capturing a: 'fo' }
572
+ it { should match('f/bar') .capturing a: 'f' }
573
+ it { should_not match('fooo/bar') }
574
+ end
575
+
576
+ pattern '{+a:3}{b}' do
577
+ it { should match('foobar') .capturing a: 'foo', b: 'bar' }
578
+ end
579
+ end
580
+
581
+ context 'expand' do
582
+ pattern '{+a*}' do
583
+ it { should match('a') .capturing a: 'a' }
584
+ it { should match('a,b') .capturing a: 'a,b' }
585
+ it { should match('a,b,c') .capturing a: 'a,b,c' }
586
+ it { should match('a,b/c') .capturing a: 'a,b/c' }
587
+ end
588
+
589
+ pattern '{+a*},{b}' do
590
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
591
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
592
+ it { should_not match('a,b/c') }
593
+ it { should_not match('a,') }
594
+
595
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
596
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
597
+ end
598
+ end
599
+ end
600
+
601
+ context 'operator #' do
602
+ context 'prefix' do
603
+ pattern '{#a:3}/bar' do
604
+ it { should match('#foo/bar') .capturing a: 'foo' }
605
+ it { should match('#fo/bar') .capturing a: 'fo' }
606
+ it { should match('#f/bar') .capturing a: 'f' }
607
+ it { should_not match('#fooo/bar') }
608
+ end
609
+
610
+ pattern '{#a:3}{b}' do
611
+ it { should match('#foobar') .capturing a: 'foo', b: 'bar' }
612
+ end
613
+ end
614
+
615
+ context 'expand' do
616
+ pattern '{#a*}' do
617
+ it { should match('#a') .capturing a: 'a' }
618
+ it { should match('#a,b') .capturing a: 'a,b' }
619
+ it { should match('#a,b,c') .capturing a: 'a,b,c' }
620
+ it { should match('#a,b/c') .capturing a: 'a,b/c' }
621
+
622
+ example { pattern.params('#a,b').should be == { 'a' => ['a', 'b'] }}
623
+ example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b', 'c'] }}
624
+ end
625
+
626
+ pattern '{#a*,b}' do
627
+ it { should match('#a,b') .capturing a: 'a', b: 'b' }
628
+ it { should match('#a,b,c') .capturing a: 'a,b', b: 'c' }
629
+ it { should_not match('#a,') }
630
+
631
+ example { pattern.params('#a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
632
+ example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
633
+ end
634
+ end
635
+ end
636
+
637
+ context 'operator .' do
638
+ context 'prefix' do
639
+ pattern '{.a:3}/bar' do
640
+ it { should match('.foo/bar') .capturing a: 'foo' }
641
+ it { should match('.fo/bar') .capturing a: 'fo' }
642
+ it { should match('.f/bar') .capturing a: 'f' }
643
+ it { should_not match('.fooo/bar') }
644
+ end
645
+
646
+ pattern '{.a:3}{b}' do
647
+ it { should match('.foobar') .capturing a: 'foo', b: 'bar' }
648
+ end
649
+ end
650
+
651
+ context 'expand' do
652
+ pattern '{.a*}' do
653
+ it { should match('.a') .capturing a: 'a' }
654
+ it { should match('.a.b') .capturing a: 'a.b' }
655
+ it { should match('.a.b.c') .capturing a: 'a.b.c' }
656
+ it { should_not match('.a.b,c') }
657
+ it { should_not match('.a,') }
658
+ end
659
+
660
+ pattern '{.a*,b}' do
661
+ it { should match('.a.b') .capturing a: 'a', b: 'b' }
662
+ it { should match('.a.b.c') .capturing a: 'a.b', b: 'c' }
663
+ it { should_not match('.a.b/c') }
664
+ it { should_not match('.a.') }
665
+
666
+ example { pattern.params('.a.b').should be == { 'a' => ['a'], 'b' => 'b' }}
667
+ example { pattern.params('.a.b.c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
668
+ end
669
+ end
670
+ end
671
+
672
+ context 'operator /' do
673
+ context 'prefix' do
674
+ pattern '{/a:3}/bar' do
675
+ it { should match('/foo/bar') .capturing a: 'foo' }
676
+ it { should match('/fo/bar') .capturing a: 'fo' }
677
+ it { should match('/f/bar') .capturing a: 'f' }
678
+ it { should_not match('/fooo/bar') }
679
+ end
680
+
681
+ pattern '{/a:3}{b}' do
682
+ it { should match('/foobar') .capturing a: 'foo', b: 'bar' }
683
+ end
684
+ end
685
+
686
+ context 'expand' do
687
+ pattern '{/a*}' do
688
+ it { should match('/a') .capturing a: 'a' }
689
+ it { should match('/a/b') .capturing a: 'a/b' }
690
+ it { should match('/a/b/c') .capturing a: 'a/b/c' }
691
+ it { should_not match('/a/b,c') }
692
+ it { should_not match('/a,') }
693
+ end
694
+
695
+ pattern '{/a*,b}' do
696
+ it { should match('/a/b') .capturing a: 'a', b: 'b' }
697
+ it { should match('/a/b/c') .capturing a: 'a/b', b: 'c' }
698
+ it { should_not match('/a/b,c') }
699
+ it { should_not match('/a/') }
700
+
701
+ example { pattern.params('/a/b').should be == { 'a' => ['a'], 'b' => 'b' }}
702
+ example { pattern.params('/a/b/c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
703
+ end
704
+ end
705
+ end
706
+
707
+ context 'operator ;' do
708
+ context 'prefix' do
709
+ pattern '{;a:3}/bar' do
710
+ it { should match(';a=foo/bar') .capturing a: 'foo' }
711
+ it { should match(';a=fo/bar') .capturing a: 'fo' }
712
+ it { should match(';a=f/bar') .capturing a: 'f' }
713
+ it { should_not match(';a=fooo/bar') }
714
+ end
715
+
716
+ pattern '{;a:3}{b}' do
717
+ it { should match(';a=foobar') .capturing a: 'foo', b: 'bar' }
718
+ end
719
+ end
720
+
721
+ context 'expand' do
722
+ pattern '{;a*}' do
723
+ it { should match(';a=1') .capturing a: 'a=1' }
724
+ it { should match(';a=1;a=2') .capturing a: 'a=1;a=2' }
725
+ it { should match(';a=1;a=2;a=3') .capturing a: 'a=1;a=2;a=3' }
726
+ it { should_not match(';a=1;a=2;b=3') }
727
+ it { should_not match(';a=1;a=2;a=3,') }
728
+ end
729
+
730
+ pattern '{;a*,b}' do
731
+ it { should match(';a=1;b') .capturing a: 'a=1', b: nil }
732
+ it { should match(';a=2;a=2;b=1') .capturing a: 'a=2;a=2', b: '1' }
733
+ it { should_not match(';a;b;c') }
734
+ it { should_not match(';a;') }
735
+
736
+ example { pattern.params(';a=2;a=2;b').should be == { 'a' => ['2', '2'], 'b' => nil }}
737
+ end
738
+ end
739
+ end
740
+
741
+ context 'operator ?' do
742
+ context 'prefix' do
743
+ pattern '{?a:3}/bar' do
744
+ it { should match('?a=foo/bar') .capturing a: 'foo' }
745
+ it { should match('?a=fo/bar') .capturing a: 'fo' }
746
+ it { should match('?a=f/bar') .capturing a: 'f' }
747
+ it { should_not match('?a=fooo/bar') }
748
+ end
749
+
750
+ pattern '{?a:3}{b}' do
751
+ it { should match('?a=foobar') .capturing a: 'foo', b: 'bar' }
752
+ end
753
+ end
754
+
755
+ context 'expand' do
756
+ pattern '{?a*}' do
757
+ it { should match('?a=1') .capturing a: 'a=1' }
758
+ it { should match('?a=1&a=2') .capturing a: 'a=1&a=2' }
759
+ it { should match('?a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' }
760
+ it { should_not match('?a=1&a=2&b=3') }
761
+ it { should_not match('?a=1&a=2&a=3,') }
762
+ end
763
+
764
+ pattern '{?a*,b}' do
765
+ it { should match('?a=1&b') .capturing a: 'a=1', b: nil }
766
+ it { should match('?a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' }
767
+ it { should_not match('?a&b&c') }
768
+ it { should_not match('?a&') }
769
+
770
+ example { pattern.params('?a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }}
771
+ end
772
+ end
773
+ end
774
+
775
+ context 'operator &' do
776
+ context 'prefix' do
777
+ pattern '{&a:3}/bar' do
778
+ it { should match('&a=foo/bar') .capturing a: 'foo' }
779
+ it { should match('&a=fo/bar') .capturing a: 'fo' }
780
+ it { should match('&a=f/bar') .capturing a: 'f' }
781
+ it { should_not match('&a=fooo/bar') }
782
+ end
783
+
784
+ pattern '{&a:3}{b}' do
785
+ it { should match('&a=foobar') .capturing a: 'foo', b: 'bar' }
786
+ end
787
+ end
788
+
789
+ context 'expand' do
790
+ pattern '{&a*}' do
791
+ it { should match('&a=1') .capturing a: 'a=1' }
792
+ it { should match('&a=1&a=2') .capturing a: 'a=1&a=2' }
793
+ it { should match('&a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' }
794
+ it { should_not match('&a=1&a=2&b=3') }
795
+ it { should_not match('&a=1&a=2&a=3,') }
796
+ end
797
+
798
+ pattern '{&a*,b}' do
799
+ it { should match('&a=1&b') .capturing a: 'a=1', b: nil }
800
+ it { should match('&a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' }
801
+ it { should_not match('&a&b&c') }
802
+ it { should_not match('&a&') }
803
+
804
+ example { pattern.params('&a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }}
805
+ example { pattern.params('&a=2&a=%20&b').should be == { 'a' => ['2', ' '], 'b' => nil }}
806
+ end
807
+ end
808
+ end
809
+ end
810
+
811
+ context 'invalid syntax' do
812
+ example 'unexpected closing bracket' do
813
+ expect { Mustermann::Template.new('foo}bar') }.
814
+ to raise_error(Mustermann::ParseError, 'unexpected } while parsing "foo}bar"')
815
+ end
816
+
817
+ example 'missing closing bracket' do
818
+ expect { Mustermann::Template.new('foo{bar') }.
819
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo{bar"')
820
+ end
821
+ end
822
+
823
+ context "peeking" do
824
+ subject(:pattern) { Mustermann::Template.new("{name}bar") }
825
+
826
+ describe :peek_size do
827
+ example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size }
828
+ example { pattern.peek_size("/foo bar") .should be_nil }
829
+ end
830
+
831
+ describe :peek_match do
832
+ example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" }
833
+ example { pattern.peek_match("/foo bar") .should be_nil }
834
+ end
835
+
836
+ describe :peek_params do
837
+ example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo "}, "foo%20bar".size] }
838
+ example { pattern.peek_params("/foo bar") .should be_nil }
839
+ end
840
+ end
841
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mustermann-uri-template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Haase
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mustermann
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ description: Adds URI template style patterns to Mustermman
28
+ email: konstantin.mailinglists@googlemail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/mustermann/template.rb
35
+ - lib/mustermann/uri_template.rb
36
+ - mustermann-uri-template.gemspec
37
+ - spec/template_spec.rb
38
+ homepage: https://github.com/rkh/mustermann
39
+ licenses:
40
+ - MIT
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.1.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 2.4.3
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: URI template syntax for Mustermann
62
+ test_files:
63
+ - spec/template_spec.rb
64
+ has_rdoc: