mustermann-flask 0.4.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3711353dec9fd3af110d0e77f5bec7a991b17a9b
4
+ data.tar.gz: 7ac3a0d796f83314f7ec0306594e411639db6493
5
+ SHA512:
6
+ metadata.gz: c04ab10704006935895cfeccb302867731393f794c53e130f1b9ab85ee57df4cf3d37bf0e29c0715a24d508636017f108436cce7909d2d7e77abe74a8b845ef6
7
+ data.tar.gz: acaa0a0b96dabb337e96284a3503fab35e1c0d8fb44ed6e2834fae3698a8f704dd9c6309322a5ba5ee36de3b23e545f508be34c5a1c5b4ea28dd559283e36555
@@ -0,0 +1,246 @@
1
+ # Flask Syntax for Mustermann
2
+
3
+ This gem implements the `flask` pattern type for Mustermann. It is compatible with [Flask](http://flask.pocoo.org/) and [Werkzeug](http://werkzeug.pocoo.org/).
4
+
5
+ ## Overview
6
+
7
+ **Supported options:**
8
+ `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, `converters` and `ignore_unknown_options`
9
+
10
+ **External documentation:**
11
+ [Werkzeug: URL Routing](http://werkzeug.pocoo.org/docs/0.9/routing/)
12
+
13
+ ``` ruby
14
+ require 'mustermann/flask'
15
+
16
+ Mustermann.new('/<prefix>/<path:page>', type: :flask).params('/a/b/c') # => { prefix: 'a', page: 'b/c' }
17
+
18
+ pattern = Mustermann.new('/<name>', type: :flask)
19
+
20
+ pattern.respond_to? :expand # => true
21
+ pattern.expand(name: 'foo') # => '/foo'
22
+
23
+ pattern.respond_to? :to_templates # => true
24
+ pattern.to_templates # => ['/{name}']
25
+ ```
26
+
27
+ ## Syntax
28
+
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>Syntax Element</th>
33
+ <th>Description</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ <tr>
38
+ <td><b>&lt;</b><i>name</i><b>&gt;</b></td>
39
+ <td>
40
+ Captures anything but a forward slash in a semi-greedy fashion. Capture is named <i>name</i>.
41
+ Capture behavior can be modified with <tt>capture</tt> and <tt>greedy</tt> option.
42
+ </td>
43
+ </tr>
44
+ <tr>
45
+ <td><b>&lt;</b><i>converter</i><b>:</b><i>name</i><b>&gt;</b></td>
46
+ <td>
47
+ Captures depending on the converter constraint. Capture is named <i>name</i>.
48
+ Capture behavior can be modified with <tt>capture</tt> and <tt>greedy</tt> option.
49
+ See below.
50
+ </td>
51
+ </tr>
52
+ <tr>
53
+ <td><b>&lt;</b><i>converter</i><b>(</b><i>arguments</i><b>):</b><i>name</i><b>&gt;</b></td>
54
+ <td>
55
+ Captures depending on the converter constraint. Capture is named <i>name</i>.
56
+ Capture behavior can be modified with <tt>capture</tt> and <tt>greedy</tt> option.
57
+ Arguments are separated by comma. An argument can be a simple string, a string enclosed
58
+ in single or double quotes, or a key value pair (keys and values being separated by an
59
+ equal sign). See below.
60
+ </td>
61
+ </tr>
62
+ <tr>
63
+ <td><b>/</b></td>
64
+ <td>
65
+ Matches forward slash. Does not match URI encoded version of forward slash.
66
+ </td>
67
+ </tr>
68
+ <tr>
69
+ <td><i>any other character</i></td>
70
+ <td>Matches exactly that character or a URI encoded version of it.</td>
71
+ </tr>
72
+ </tbody>
73
+ </table>
74
+
75
+ ## Converters
76
+
77
+ ### Builtin Converters
78
+
79
+ #### `string`
80
+
81
+ Possible arguments: `minlength`, `maxlength`, `length`
82
+
83
+ Captures anything but a forward slash in a semi-greedy fashion.
84
+ Capture behavior can be modified with <tt>capture</tt> and <tt>greedy</tt> option.
85
+
86
+ This is also the default converter.
87
+
88
+ Examples:
89
+
90
+ ```
91
+ <name>
92
+ <string:name>
93
+ <string(minlength=3,maxlength=10):slug>
94
+ <string(length=10):slug>
95
+ ```
96
+
97
+ #### `int`
98
+
99
+ Possible arguments: `min`, `max`, `fixed_digits`
100
+
101
+ Captures digits.
102
+ Captured value will be converted to an Integer.
103
+
104
+ Examples:
105
+
106
+ ```
107
+ <int:id>
108
+ <int(min=1,max=5):page>
109
+ <int(fixed_digits=16):uuid>
110
+ ```
111
+
112
+ #### `float`
113
+
114
+ Possible arguments: `min`, `max`
115
+
116
+ Captures digits with a dot.
117
+ Captured value will be converted to an Float.
118
+
119
+ Examples:
120
+
121
+ ```
122
+ <float:precision>
123
+ <float(min=0,max=1):precision>
124
+ ```
125
+
126
+ #### `path`
127
+
128
+ Captures anything in a non-greedy fashion.
129
+
130
+ Example:
131
+
132
+ ```
133
+ <path:rest>
134
+ ```
135
+
136
+ #### `any`
137
+
138
+ Possible arguments: List of accepted strings.
139
+
140
+ Captures anything that matches one of the arguments exactly.
141
+
142
+ Example:
143
+
144
+ ```
145
+ <any(home,search,contact):page>
146
+ ```
147
+
148
+ ### Custom Converters
149
+
150
+ [Flask patterns](#-pattern-details-flask) support registering custom converters.
151
+
152
+ A converter object may implement any of the following methods:
153
+
154
+ * `convert`: Should return a block converting a string value to whatever value should end up in the `params` hash.
155
+ * `constraint`: Should return a regular expression limiting which input string will match the capture.
156
+ * `new`: Returns an object that may respond to `convert` and/or `constraint` as described above. Any arguments used for the converter inside the pattern will be passed to `new`.
157
+
158
+ ``` ruby
159
+ require 'mustermann/flask'
160
+
161
+ SimpleConverter = Struct.new(:constraint, :convert)
162
+ id_converter = SimpleConverter.new(/\d/, -> s { s.to_i })
163
+
164
+ class NumConverter
165
+ def initialize(base: 10)
166
+ @base = Integer(base)
167
+ end
168
+
169
+ def convert
170
+ -> s { s.to_i(@base) }
171
+ end
172
+
173
+ def constraint
174
+ @base > 10 ? /[\da-#{(@base-1).to_s(@base)}]/ : /[0-#{@base-1}]/
175
+ end
176
+ end
177
+
178
+ pattern = Mustermann.new('/<id:id>/<num(base=8):oct>/<num(base=16):hex>',
179
+ type: :flask, converters: { id: id_converter, num: NumConverter})
180
+
181
+ pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241}
182
+ ```
183
+
184
+ ### Global Converters
185
+
186
+ It is also possible to register a converter for all flask patterns, using `register_converter`:
187
+
188
+ ``` ruby
189
+ Mustermann::Flask.register_converter(:id, id_converter)
190
+ Mustermann::Flask.register_converter(:num, NumConverter)
191
+
192
+ pattern = Mustermann.new('/<id:id>/<num(base=8):oct>/<num(base=16):hex>', type: :flask)
193
+ pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241}
194
+ ```
195
+
196
+ There is a handy syntax for quickly creating new converter classes: When you pass a block instead of a converter object, it will yield a generic converter with setters and getters for `convert` and `constraint`, and any arguments passed to the converter.
197
+
198
+ ``` ruby
199
+ require 'mustermann/flask'
200
+
201
+ Mustermann::Flask.register_converter(:id) do |converter|
202
+ converter.constraint = /\d/
203
+ converter.convert = -> s { s.to_i }
204
+ end
205
+
206
+ Mustermann::Flask.register_converter(:num) do |converter, base: 10|
207
+ converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/
208
+ converter.convert = -> s { s.to_i(base) }
209
+ end
210
+
211
+ pattern = Mustermann.new('/<id:id>/<num(base=8):oct>/<num(base=16):hex>', type: :flask)
212
+ pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241}
213
+ ```
214
+
215
+ ### Subclassing
216
+
217
+ Registering global converters will make these available for all Flask patterns. It might even override already registered converters. This global state might break unrelated code.
218
+
219
+ It is therefore recommended that, if you don't want to pass in the converters option for every pattern, you create your own subclass of `Mustermann::Flask`.
220
+
221
+ ``` ruby
222
+ require 'mustermann/flask'
223
+
224
+ MyFlask = Class.new(Mustermann::Flask)
225
+
226
+ MyFlask.register_converter(:id) do |converter|
227
+ converter.constraint = /\d/
228
+ converter.convert = -> s { s.to_i }
229
+ end
230
+
231
+ MyFlask.register_converter(:num) do |converter, base: 10|
232
+ converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/
233
+ converter.convert = -> s { s.to_i(base) }
234
+ end
235
+
236
+ pattern = MyFlask.new('/<id:id>/<num(base=8):oct>/<num(base=16):hex>')
237
+ pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241}
238
+ ```
239
+
240
+ You can even register this type for usage with `Mustermann.new`:
241
+
242
+ ``` ruby
243
+ Mustermann.register(:my_flask, MyFlask)
244
+ pattern = Mustermann.new('/<id:id>/<num(base=8):oct>/<num(base=16):hex>', type: :my_flask)
245
+ pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241}
246
+ ```
@@ -0,0 +1,197 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+
4
+ module Mustermann
5
+ # Flask style pattern implementation.
6
+ #
7
+ # @example
8
+ # Mustermann.new('/<foo>', type: :flask) === '/bar' # => true
9
+ #
10
+ # @see Mustermann::Pattern
11
+ # @see file:README.md#flask Syntax description in the README
12
+ class Flask < AST::Pattern
13
+ register :flask
14
+
15
+ on(nil, ?>, ?:) { |c| unexpected(c) }
16
+
17
+ on(?<) do |char|
18
+ converter_name = expect(/\w+/, char: char)
19
+ args, opts = scan(?() ? read_args(?=, ?)) : [[], {}]
20
+
21
+ if scan(?:)
22
+ name = read_escaped(?>)
23
+ else
24
+ converter_name, name = 'default', converter_name
25
+ expect(?>)
26
+ end
27
+
28
+ converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) }
29
+ converter = converter.new(*args, **opts) if converter.respond_to? :new
30
+ constraint = converter.constraint if converter.respond_to? :constraint
31
+ convert = converter.convert if converter.respond_to? :convert
32
+ qualifier = converter.qualifier if converter.respond_to? :qualifier
33
+ node_type = converter.node_type if converter.respond_to? :node_type
34
+ node_type ||= :capture
35
+
36
+ node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier)
37
+ end
38
+
39
+ # A class for easy creating of converters.
40
+ # @see Mustermann::Flask#register_converter
41
+ class Converter
42
+ # Constraint on the format used for the capture.
43
+ # Should be a regexp (or a string corresponding to a regexp)
44
+ # @see Mustermann::Flask#register_converter
45
+ attr_accessor :constraint
46
+
47
+ # Callback
48
+ # Should be a Proc.
49
+ # @see Mustermann::Flask#register_converter
50
+ attr_accessor :convert
51
+
52
+ # Constraint on the format used for the capture.
53
+ # Should be a regexp (or a string corresponding to a regexp)
54
+ # @see Mustermann::Flask#register_converter
55
+ # @!visibility private
56
+ attr_accessor :node_type
57
+
58
+ # Constraint on the format used for the capture.
59
+ # Should be a regexp (or a string corresponding to a regexp)
60
+ # @see Mustermann::Flask#register_converter
61
+ # @!visibility private
62
+ attr_accessor :qualifier
63
+
64
+ # @!visibility private
65
+ def self.create(&block)
66
+ Class.new(self) do
67
+ define_method(:initialize) { |*a, **o| block[self, *a, **o] }
68
+ end
69
+ end
70
+
71
+ # Makes sure a given value falls inbetween a min and a max.
72
+ # Uses the passed block to convert the value from a string to whatever
73
+ # format you'd expect.
74
+ #
75
+ # @example
76
+ # require 'mustermann/flask'
77
+ #
78
+ # class MyPattern < Mustermann::Flask
79
+ # register_converter(:x) { between(5, 15, &:to_i) }
80
+ # end
81
+ #
82
+ # pattern = MyPattern.new('<x:id>')
83
+ # pattern.params('/12') # => { 'id' => 12 }
84
+ # pattern.params('/16') # => { 'id' => 15 }
85
+ #
86
+ # @see Mustermann::Flask#register_converter
87
+ def between(min, max)
88
+ self.convert = proc do |input|
89
+ value = yield(input)
90
+ value = yield(min) if min and value < yield(min)
91
+ value = yield(max) if max and value > yield(max)
92
+ value
93
+ end
94
+ end
95
+ end
96
+
97
+ # Generally available converters.
98
+ # @!visibility private
99
+ def self.converters(inherited = true)
100
+ return @converters ||= {} unless inherited
101
+ defaults = superclass.respond_to?(:converters) ? superclass.converters : {}
102
+ defaults.merge(converters(false))
103
+ end
104
+
105
+ # Allows you to register your own converters.
106
+ #
107
+ # It is reommended to use this on a subclass, so to not influence other subsystems
108
+ # using flask templates.
109
+ #
110
+ # The object passed in as converter can implement #convert and/or #constraint.
111
+ #
112
+ # It can also instead implement #new, which will then return an object responding
113
+ # to some of these methods. Arguments from the flask pattern will be passed to #new.
114
+ #
115
+ # If passed a block, it will be yielded to with a {Mustermann::Flask::Converter}
116
+ # instance and any arguments in the flask pattern.
117
+ #
118
+ # @example with simple object
119
+ # require 'mustermann/flask'
120
+ #
121
+ # MyPattern = Class.new(Mustermann::Flask)
122
+ # up_converter = Struct.new(:convert).new(:upcase.to_proc)
123
+ # MyPattern.register_converter(:upper, up_converter)
124
+ #
125
+ # MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
126
+ #
127
+ # @example with block
128
+ # require 'mustermann/flask'
129
+ #
130
+ # MyPattern = Class.new(Mustermann::Flask)
131
+ # MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc }
132
+ #
133
+ # MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
134
+ #
135
+ # @example with converter class
136
+ # require 'mustermann/flasl'
137
+ #
138
+ # class MyPattern < Mustermann::Flask
139
+ # class Converter
140
+ # attr_reader :convert
141
+ # def initialize(send: :to_s)
142
+ # @convert = send.to_sym.to_proc
143
+ # end
144
+ # end
145
+ #
146
+ # register_converter(:t, Converter)
147
+ # end
148
+ #
149
+ # MyPattern.new("/<t(send=upcase):name>").params('/Foo') # => { "name" => "FOO" }
150
+ # MyPattern.new("/<t(send=downcase):name>").params('/Foo') # => { "name" => "foo" }
151
+ #
152
+ # @param [#to_s] name converter name
153
+ # @param [#new, #convert, #constraint, nil] converter
154
+ def self.register_converter(name, converter = nil, &block)
155
+ converter ||= Converter.create(&block)
156
+ converters(false)[name.to_s] = converter
157
+ end
158
+
159
+ register_converter(:string) do |converter, minlength: nil, maxlength: nil, length: nil|
160
+ converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength
161
+ converter.qualifier = "{%s}" % length if length
162
+ end
163
+
164
+ register_converter(:int) do |converter, min: nil, max: nil, fixed_digits: false|
165
+ converter.constraint = /\d/
166
+ converter.qualifier = "{#{fixed_digits}}" if fixed_digits
167
+ converter.between(min, max) { |string| Integer(string) }
168
+ end
169
+
170
+ register_converter(:float) do |converter, min: nil, max: nil|
171
+ converter.constraint = /\d*\.?\d+/
172
+ converter.qualifier = ""
173
+ converter.between(min, max) { |string| Float(string) }
174
+ end
175
+
176
+ register_converter(:path) do |converter|
177
+ converter.node_type = :named_splat
178
+ end
179
+
180
+ register_converter(:any) do |converter, *strings|
181
+ strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact
182
+ converter.qualifier = ""
183
+ converter.constraint = Regexp.union(*strings)
184
+ end
185
+
186
+ register_converter(:default, converters['string'])
187
+
188
+ supported_options :converters
189
+ attr_reader :converters
190
+
191
+ def initialize(input, converters: {}, **options)
192
+ @converters = self.class.converters.dup
193
+ converters.each { |k,v| @converters[k.to_s] = v } if converters
194
+ super(input, **options)
195
+ end
196
+ end
197
+ end
@@ -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-flask"
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{Flask syntax for Mustermann}
11
+ s.description = %q{Adds Flask 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,361 @@
1
+ require 'support'
2
+ require 'mustermann/flask'
3
+
4
+ describe Mustermann::Flask do
5
+ extend Support::Pattern
6
+
7
+ pattern '' do
8
+ it { should match('') }
9
+ it { should_not match('/') }
10
+
11
+ it { should expand.to('') }
12
+ it { should_not expand(a: 1) }
13
+
14
+ it { should generate_template('') }
15
+
16
+ it { should respond_to(:expand) }
17
+ it { should respond_to(:to_templates) }
18
+ end
19
+
20
+ pattern '/' do
21
+ it { should match('/') }
22
+ it { should_not match('/foo') }
23
+
24
+ it { should expand.to('/') }
25
+ it { should_not expand(a: 1) }
26
+ end
27
+
28
+ pattern '/foo' do
29
+ it { should match('/foo') }
30
+ it { should_not match('/bar') }
31
+ it { should_not match('/foo.bar') }
32
+
33
+ it { should expand.to('/foo') }
34
+ it { should_not expand(a: 1) }
35
+ end
36
+
37
+ pattern '/foo/bar' do
38
+ it { should match('/foo/bar') }
39
+ it { should_not match('/foo%2Fbar') }
40
+ it { should_not match('/foo%2fbar') }
41
+
42
+ it { should expand.to('/foo/bar') }
43
+ it { should_not expand(a: 1) }
44
+ end
45
+
46
+ pattern '/<foo>' do
47
+ it { should match('/foo') .capturing foo: 'foo' }
48
+ it { should match('/bar') .capturing foo: 'bar' }
49
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
50
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
51
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
52
+
53
+ it { should_not match('/foo?') }
54
+ it { should_not match('/foo/bar') }
55
+ it { should_not match('/') }
56
+ it { should_not match('/foo/') }
57
+
58
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
59
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
60
+ example { pattern.params('').should be_nil }
61
+
62
+ it { should expand(foo: 'bar') .to('/bar') }
63
+ it { should expand(foo: 'b r') .to('/b%20r') }
64
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
65
+
66
+ it { should_not expand(foo: 'foo', bar: 'bar') }
67
+ it { should_not expand(bar: 'bar') }
68
+ it { should_not expand }
69
+
70
+ it { should generate_template('/{foo}') }
71
+ end
72
+
73
+ pattern '/<string:foo>' do
74
+ it { should match('/foo') .capturing foo: 'foo' }
75
+ it { should match('/bar') .capturing foo: 'bar' }
76
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
77
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
78
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
79
+
80
+ it { should_not match('/foo?') }
81
+ it { should_not match('/foo/bar') }
82
+ it { should_not match('/') }
83
+ it { should_not match('/foo/') }
84
+
85
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
86
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
87
+ example { pattern.params('').should be_nil }
88
+
89
+ it { should expand(foo: 'bar') .to('/bar') }
90
+ it { should expand(foo: 'b r') .to('/b%20r') }
91
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
92
+
93
+ it { should_not expand(foo: 'foo', bar: 'bar') }
94
+ it { should_not expand(bar: 'bar') }
95
+ it { should_not expand }
96
+
97
+ it { should generate_template('/{foo}') }
98
+ end
99
+
100
+ pattern '/<string(minlength=2):foo>' do
101
+ it { should match('/foo') .capturing foo: 'foo' }
102
+ it { should match('/bar') .capturing foo: 'bar' }
103
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
104
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
105
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
106
+
107
+ it { should_not match('/f') }
108
+ it { should_not match('/foo?') }
109
+ it { should_not match('/foo/bar') }
110
+ it { should_not match('/') }
111
+ it { should_not match('/foo/') }
112
+
113
+ it { should generate_template('/{foo}') }
114
+ end
115
+
116
+ pattern '/<string(maxlength=3):foo>' do
117
+ it { should match('/f') .capturing foo: 'f' }
118
+ it { should match('/fo') .capturing foo: 'fo' }
119
+ it { should match('/foo') .capturing foo: 'foo' }
120
+ it { should match('/bar') .capturing foo: 'bar' }
121
+
122
+ it { should_not match('/fooo') }
123
+ it { should_not match('/foo.bar') }
124
+ it { should_not match('/foo?') }
125
+ it { should_not match('/foo/bar') }
126
+ it { should_not match('/') }
127
+ it { should_not match('/foo/') }
128
+
129
+ it { should generate_template('/{foo}') }
130
+ end
131
+
132
+ pattern '/<string(length=3):foo>' do
133
+ it { should match('/foo') .capturing foo: 'foo' }
134
+ it { should match('/bar') .capturing foo: 'bar' }
135
+
136
+ it { should_not match('/f') }
137
+ it { should_not match('/fo') }
138
+ it { should_not match('/fooo') }
139
+ it { should_not match('/foo.bar') }
140
+ it { should_not match('/foo?') }
141
+ it { should_not match('/foo/bar') }
142
+ it { should_not match('/') }
143
+ it { should_not match('/foo/') }
144
+
145
+ it { should generate_template('/{foo}') }
146
+ end
147
+
148
+ pattern '/<int:foo>' do
149
+ it { should match('/42').capturing foo: '42' }
150
+
151
+ it { should_not match('/1.0') }
152
+ it { should_not match('/.5') }
153
+ it { should_not match('/foo') }
154
+ it { should_not match('/bar') }
155
+ it { should_not match('/foo.bar') }
156
+ it { should_not match('/%0Afoo') }
157
+ it { should_not match('/foo%2Fbar') }
158
+
159
+ it { should_not match('/foo?') }
160
+ it { should_not match('/foo/bar') }
161
+ it { should_not match('/') }
162
+ it { should_not match('/foo/') }
163
+
164
+ example { pattern.params('/42').should be == {"foo" => 42} }
165
+ it { should expand(foo: 12).to('/12') }
166
+ it { should generate_template('/{foo}') }
167
+ end
168
+
169
+ pattern '/<int:foo>' do
170
+ it { should match('/42').capturing foo: '42' }
171
+
172
+ it { should_not match('/1.0') }
173
+ it { should_not match('/.5') }
174
+ it { should_not match('/foo') }
175
+ it { should_not match('/bar') }
176
+ it { should_not match('/foo.bar') }
177
+ it { should_not match('/%0Afoo') }
178
+ it { should_not match('/foo%2Fbar') }
179
+
180
+ it { should_not match('/foo?') }
181
+ it { should_not match('/foo/bar') }
182
+ it { should_not match('/') }
183
+ it { should_not match('/foo/') }
184
+
185
+ example { pattern.params('/42').should be == {"foo" => 42} }
186
+ it { should expand(foo: 12).to('/12') }
187
+ it { should generate_template('/{foo}') }
188
+ end
189
+
190
+ pattern '/<any(foo,bar):foo>' do
191
+ it { should match('/foo') .capturing foo: 'foo' }
192
+ it { should match('/bar') .capturing foo: 'bar' }
193
+
194
+ it { should_not match('/f') }
195
+ it { should_not match('/fo') }
196
+ it { should_not match('/fooo') }
197
+ it { should_not match('/foo.bar') }
198
+ it { should_not match('/foo?') }
199
+ it { should_not match('/foo/bar') }
200
+ it { should_not match('/') }
201
+ it { should_not match('/foo/') }
202
+ it { should_not match('/baz') }
203
+
204
+ it { should generate_template('/{foo}') }
205
+ end
206
+
207
+ pattern '/<any( foo, bar ):foo>' do
208
+ it { should match('/foo') .capturing foo: 'foo' }
209
+ it { should match('/bar') .capturing foo: 'bar' }
210
+
211
+ it { should_not match('/f') }
212
+ it { should_not match('/fo') }
213
+ it { should_not match('/fooo') }
214
+ it { should_not match('/foo.bar') }
215
+ it { should_not match('/foo?') }
216
+ it { should_not match('/foo/bar') }
217
+ it { should_not match('/') }
218
+ it { should_not match('/foo/') }
219
+ it { should_not match('/baz') }
220
+
221
+ it { should generate_template('/{foo}') }
222
+ end
223
+
224
+ pattern '/<any(foo, bar, "foo,bar"):foo>' do
225
+ it { should match('/foo') .capturing foo: 'foo' }
226
+ it { should match('/bar') .capturing foo: 'bar' }
227
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
228
+
229
+ it { should_not match('/f') }
230
+ it { should_not match('/fo') }
231
+ it { should_not match('/fooo') }
232
+ it { should_not match('/foo.bar') }
233
+ it { should_not match('/foo?') }
234
+ it { should_not match('/foo/bar') }
235
+ it { should_not match('/') }
236
+ it { should_not match('/foo/') }
237
+ it { should_not match('/baz') }
238
+
239
+ it { should generate_template('/{foo}') }
240
+ end
241
+
242
+ pattern '/<any(foo, bar, foo\,bar):foo>' do
243
+ it { should match('/foo') .capturing foo: 'foo' }
244
+ it { should match('/bar') .capturing foo: 'bar' }
245
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
246
+
247
+ it { should_not match('/f') }
248
+ it { should_not match('/fo') }
249
+ it { should_not match('/fooo') }
250
+ it { should_not match('/foo.bar') }
251
+ it { should_not match('/foo?') }
252
+ it { should_not match('/foo/bar') }
253
+ it { should_not match('/') }
254
+ it { should_not match('/foo/') }
255
+ it { should_not match('/baz') }
256
+
257
+ it { should generate_template('/{foo}') }
258
+ end
259
+
260
+ pattern '/<any(foo, bar, "foo\,bar"):foo>' do
261
+ it { should match('/foo') .capturing foo: 'foo' }
262
+ it { should match('/bar') .capturing foo: 'bar' }
263
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
264
+
265
+ it { should_not match('/f') }
266
+ it { should_not match('/fo') }
267
+ it { should_not match('/fooo') }
268
+ it { should_not match('/foo.bar') }
269
+ it { should_not match('/foo?') }
270
+ it { should_not match('/foo/bar') }
271
+ it { should_not match('/') }
272
+ it { should_not match('/foo/') }
273
+ it { should_not match('/baz') }
274
+
275
+ it { should generate_template('/{foo}') }
276
+ end
277
+
278
+ pattern '/<int(min=5,max=50):foo>' do
279
+ example { pattern.params('/42').should be == {"foo" => 42} }
280
+ example { pattern.params('/52').should be == {"foo" => 50} }
281
+ example { pattern.params('/2').should be == {"foo" => 5} }
282
+ end
283
+
284
+ pattern '/<float(min=5,max=50.5):foo>' do
285
+ example { pattern.params('/42.5').should be == {"foo" => 42.5} }
286
+ example { pattern.params('/52.5').should be == {"foo" => 50.5} }
287
+ example { pattern.params('/2.5').should be == {"foo" => 5.0} }
288
+ end
289
+
290
+ pattern '/<prefix>/<float:foo>/<int:bar>' do
291
+ it { should match('/foo/42/42') .capturing foo: '42', bar: '42' }
292
+ it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' }
293
+ it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' }
294
+
295
+ it { should_not match('/foo/1/1.0') }
296
+ it { should_not match('/foo/1.0/1.0') }
297
+
298
+ it { should generate_template('/{prefix}/{foo}/{bar}') }
299
+
300
+ example do
301
+ pattern.params('/foo/1.0/1').should be == {
302
+ "prefix" => "foo",
303
+ "foo" => 1.0,
304
+ "bar" => 1
305
+ }
306
+ end
307
+ end
308
+
309
+ pattern '/<path:foo>' do
310
+ it { should match('/') .capturing foo: '' }
311
+ it { should match('/foo') .capturing foo: 'foo' }
312
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
313
+
314
+ it { should expand .to('/') }
315
+ it { should expand(foo: nil) .to('/') }
316
+ it { should expand(foo: '') .to('/') }
317
+ it { should expand(foo: 'foo') .to('/foo') }
318
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
319
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
320
+
321
+ it { should generate_template('/{+foo}') }
322
+ end
323
+
324
+ converter = Struct.new(:convert).new(:upcase.to_proc)
325
+ pattern '/<foo:bar>', converters: { foo: converter } do
326
+ it { should match('/foo').capturing bar: 'foo' }
327
+ example { pattern.params('/foo').should be == {"bar" => "FOO"} }
328
+ end
329
+
330
+ context 'invalid syntax' do
331
+ example 'unexpected end of capture' do
332
+ expect { Mustermann::Flask.new('foo>bar') }.
333
+ to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"')
334
+ end
335
+
336
+ example 'missing end of capture' do
337
+ expect { Mustermann::Flask.new('foo<bar') }.
338
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo<bar"')
339
+ end
340
+
341
+ example 'unknown converter' do
342
+ expect { Mustermann::Flask.new('foo<bar:name>') }.
343
+ to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo<bar:name>"')
344
+ end
345
+
346
+ example 'broken argument synax' do
347
+ expect { Mustermann::Flask.new('<string(length=3=2):foo>') }.
348
+ to raise_error(Mustermann::ParseError, 'unexpected = while parsing "<string(length=3=2):foo>"')
349
+ end
350
+
351
+ example 'missing )' do
352
+ expect { Mustermann::Flask.new('<string(foo') }.
353
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "<string(foo"')
354
+ end
355
+
356
+ example 'missing ""' do
357
+ expect { Mustermann::Flask.new('<string("foo') }.
358
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "<string(\\"foo"')
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,368 @@
1
+ require 'support'
2
+ require 'mustermann/flask'
3
+
4
+ FlaskSubclass ||= Class.new(Mustermann::Flask)
5
+ FlaskSubclass.register_converter(:foo) {}
6
+
7
+ describe FlaskSubclass do
8
+ extend Support::Pattern
9
+
10
+ pattern '' do
11
+ it { should match('') }
12
+ it { should_not match('/') }
13
+
14
+ it { should expand.to('') }
15
+ it { should_not expand(a: 1) }
16
+
17
+ it { should generate_template('') }
18
+
19
+ it { should respond_to(:expand) }
20
+ it { should respond_to(:to_templates) }
21
+ end
22
+
23
+ pattern '/' do
24
+ it { should match('/') }
25
+ it { should_not match('/foo') }
26
+
27
+ it { should expand.to('/') }
28
+ it { should_not expand(a: 1) }
29
+ end
30
+
31
+ pattern '/foo' do
32
+ it { should match('/foo') }
33
+ it { should_not match('/bar') }
34
+ it { should_not match('/foo.bar') }
35
+
36
+ it { should expand.to('/foo') }
37
+ it { should_not expand(a: 1) }
38
+ end
39
+
40
+ pattern '/foo/bar' do
41
+ it { should match('/foo/bar') }
42
+ it { should_not match('/foo%2Fbar') }
43
+ it { should_not match('/foo%2fbar') }
44
+
45
+ it { should expand.to('/foo/bar') }
46
+ it { should_not expand(a: 1) }
47
+ end
48
+
49
+ pattern '/<foo>' do
50
+ it { should match('/foo') .capturing foo: 'foo' }
51
+ it { should match('/bar') .capturing foo: 'bar' }
52
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
53
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
54
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
55
+
56
+ it { should_not match('/foo?') }
57
+ it { should_not match('/foo/bar') }
58
+ it { should_not match('/') }
59
+ it { should_not match('/foo/') }
60
+
61
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
62
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
63
+ example { pattern.params('').should be_nil }
64
+
65
+ it { should expand(foo: 'bar') .to('/bar') }
66
+ it { should expand(foo: 'b r') .to('/b%20r') }
67
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
68
+
69
+ it { should_not expand(foo: 'foo', bar: 'bar') }
70
+ it { should_not expand(bar: 'bar') }
71
+ it { should_not expand }
72
+
73
+ it { should generate_template('/{foo}') }
74
+ end
75
+
76
+ pattern '/<string:foo>' do
77
+ it { should match('/foo') .capturing foo: 'foo' }
78
+ it { should match('/bar') .capturing foo: 'bar' }
79
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
80
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
81
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
82
+
83
+ it { should_not match('/foo?') }
84
+ it { should_not match('/foo/bar') }
85
+ it { should_not match('/') }
86
+ it { should_not match('/foo/') }
87
+
88
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
89
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
90
+ example { pattern.params('').should be_nil }
91
+
92
+ it { should expand(foo: 'bar') .to('/bar') }
93
+ it { should expand(foo: 'b r') .to('/b%20r') }
94
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
95
+
96
+ it { should_not expand(foo: 'foo', bar: 'bar') }
97
+ it { should_not expand(bar: 'bar') }
98
+ it { should_not expand }
99
+
100
+ it { should generate_template('/{foo}') }
101
+ end
102
+
103
+ pattern '/<string(minlength=2):foo>' do
104
+ it { should match('/foo') .capturing foo: 'foo' }
105
+ it { should match('/bar') .capturing foo: 'bar' }
106
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
107
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
108
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
109
+
110
+ it { should_not match('/f') }
111
+ it { should_not match('/foo?') }
112
+ it { should_not match('/foo/bar') }
113
+ it { should_not match('/') }
114
+ it { should_not match('/foo/') }
115
+
116
+ it { should generate_template('/{foo}') }
117
+ end
118
+
119
+ pattern '/<string(maxlength=3):foo>' do
120
+ it { should match('/f') .capturing foo: 'f' }
121
+ it { should match('/fo') .capturing foo: 'fo' }
122
+ it { should match('/foo') .capturing foo: 'foo' }
123
+ it { should match('/bar') .capturing foo: 'bar' }
124
+
125
+ it { should_not match('/fooo') }
126
+ it { should_not match('/foo.bar') }
127
+ it { should_not match('/foo?') }
128
+ it { should_not match('/foo/bar') }
129
+ it { should_not match('/') }
130
+ it { should_not match('/foo/') }
131
+
132
+ it { should generate_template('/{foo}') }
133
+ end
134
+
135
+ pattern '/<string(length=3):foo>' do
136
+ it { should match('/foo') .capturing foo: 'foo' }
137
+ it { should match('/bar') .capturing foo: 'bar' }
138
+
139
+ it { should_not match('/f') }
140
+ it { should_not match('/fo') }
141
+ it { should_not match('/fooo') }
142
+ it { should_not match('/foo.bar') }
143
+ it { should_not match('/foo?') }
144
+ it { should_not match('/foo/bar') }
145
+ it { should_not match('/') }
146
+ it { should_not match('/foo/') }
147
+
148
+ it { should generate_template('/{foo}') }
149
+ end
150
+
151
+ pattern '/<int:foo>' do
152
+ it { should match('/42').capturing foo: '42' }
153
+
154
+ it { should_not match('/1.0') }
155
+ it { should_not match('/.5') }
156
+ it { should_not match('/foo') }
157
+ it { should_not match('/bar') }
158
+ it { should_not match('/foo.bar') }
159
+ it { should_not match('/%0Afoo') }
160
+ it { should_not match('/foo%2Fbar') }
161
+
162
+ it { should_not match('/foo?') }
163
+ it { should_not match('/foo/bar') }
164
+ it { should_not match('/') }
165
+ it { should_not match('/foo/') }
166
+
167
+ example { pattern.params('/42').should be == {"foo" => 42} }
168
+ it { should expand(foo: 12).to('/12') }
169
+ it { should generate_template('/{foo}') }
170
+ end
171
+
172
+ pattern '/<int:foo>' do
173
+ it { should match('/42').capturing foo: '42' }
174
+
175
+ it { should_not match('/1.0') }
176
+ it { should_not match('/.5') }
177
+ it { should_not match('/foo') }
178
+ it { should_not match('/bar') }
179
+ it { should_not match('/foo.bar') }
180
+ it { should_not match('/%0Afoo') }
181
+ it { should_not match('/foo%2Fbar') }
182
+
183
+ it { should_not match('/foo?') }
184
+ it { should_not match('/foo/bar') }
185
+ it { should_not match('/') }
186
+ it { should_not match('/foo/') }
187
+
188
+ example { pattern.params('/42').should be == {"foo" => 42} }
189
+ it { should expand(foo: 12).to('/12') }
190
+ it { should generate_template('/{foo}') }
191
+ end
192
+
193
+ pattern '/<any(foo,bar):foo>' do
194
+ it { should match('/foo') .capturing foo: 'foo' }
195
+ it { should match('/bar') .capturing foo: 'bar' }
196
+
197
+ it { should_not match('/f') }
198
+ it { should_not match('/fo') }
199
+ it { should_not match('/fooo') }
200
+ it { should_not match('/foo.bar') }
201
+ it { should_not match('/foo?') }
202
+ it { should_not match('/foo/bar') }
203
+ it { should_not match('/') }
204
+ it { should_not match('/foo/') }
205
+ it { should_not match('/baz') }
206
+
207
+ it { should generate_template('/{foo}') }
208
+ end
209
+
210
+ pattern '/<any( foo, bar ):foo>' do
211
+ it { should match('/foo') .capturing foo: 'foo' }
212
+ it { should match('/bar') .capturing foo: 'bar' }
213
+
214
+ it { should_not match('/f') }
215
+ it { should_not match('/fo') }
216
+ it { should_not match('/fooo') }
217
+ it { should_not match('/foo.bar') }
218
+ it { should_not match('/foo?') }
219
+ it { should_not match('/foo/bar') }
220
+ it { should_not match('/') }
221
+ it { should_not match('/foo/') }
222
+ it { should_not match('/baz') }
223
+
224
+ it { should generate_template('/{foo}') }
225
+ end
226
+
227
+ pattern '/<any(foo, bar, "foo,bar"):foo>' do
228
+ it { should match('/foo') .capturing foo: 'foo' }
229
+ it { should match('/bar') .capturing foo: 'bar' }
230
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
231
+
232
+ it { should_not match('/f') }
233
+ it { should_not match('/fo') }
234
+ it { should_not match('/fooo') }
235
+ it { should_not match('/foo.bar') }
236
+ it { should_not match('/foo?') }
237
+ it { should_not match('/foo/bar') }
238
+ it { should_not match('/') }
239
+ it { should_not match('/foo/') }
240
+ it { should_not match('/baz') }
241
+
242
+ it { should generate_template('/{foo}') }
243
+ end
244
+
245
+ pattern '/<any(foo, bar, foo\,bar):foo>' do
246
+ it { should match('/foo') .capturing foo: 'foo' }
247
+ it { should match('/bar') .capturing foo: 'bar' }
248
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
249
+
250
+ it { should_not match('/f') }
251
+ it { should_not match('/fo') }
252
+ it { should_not match('/fooo') }
253
+ it { should_not match('/foo.bar') }
254
+ it { should_not match('/foo?') }
255
+ it { should_not match('/foo/bar') }
256
+ it { should_not match('/') }
257
+ it { should_not match('/foo/') }
258
+ it { should_not match('/baz') }
259
+
260
+ it { should generate_template('/{foo}') }
261
+ end
262
+
263
+ pattern '/<any(foo, bar, "foo\,bar"):foo>' do
264
+ it { should match('/foo') .capturing foo: 'foo' }
265
+ it { should match('/bar') .capturing foo: 'bar' }
266
+ it { should match('/foo,bar') .capturing foo: 'foo,bar' }
267
+
268
+ it { should_not match('/f') }
269
+ it { should_not match('/fo') }
270
+ it { should_not match('/fooo') }
271
+ it { should_not match('/foo.bar') }
272
+ it { should_not match('/foo?') }
273
+ it { should_not match('/foo/bar') }
274
+ it { should_not match('/') }
275
+ it { should_not match('/foo/') }
276
+ it { should_not match('/baz') }
277
+
278
+ it { should generate_template('/{foo}') }
279
+ end
280
+
281
+ pattern '/<int(min=5,max=50):foo>' do
282
+ example { pattern.params('/42').should be == {"foo" => 42} }
283
+ example { pattern.params('/52').should be == {"foo" => 50} }
284
+ example { pattern.params('/2').should be == {"foo" => 5} }
285
+ end
286
+
287
+ pattern '/<float(min=5,max=50.5):foo>' do
288
+ example { pattern.params('/42.5').should be == {"foo" => 42.5} }
289
+ example { pattern.params('/52.5').should be == {"foo" => 50.5} }
290
+ example { pattern.params('/2.5').should be == {"foo" => 5.0} }
291
+ end
292
+
293
+ pattern '/<prefix>/<float:foo>/<int:bar>' do
294
+ it { should match('/foo/42/42') .capturing foo: '42', bar: '42' }
295
+ it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' }
296
+ it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' }
297
+
298
+ it { should_not match('/foo/1/1.0') }
299
+ it { should_not match('/foo/1.0/1.0') }
300
+
301
+ it { should generate_template('/{prefix}/{foo}/{bar}') }
302
+
303
+ example do
304
+ pattern.params('/foo/1.0/1').should be == {
305
+ "prefix" => "foo",
306
+ "foo" => 1.0,
307
+ "bar" => 1
308
+ }
309
+ end
310
+ end
311
+
312
+ pattern '/<path:foo>' do
313
+ it { should match('/') .capturing foo: '' }
314
+ it { should match('/foo') .capturing foo: 'foo' }
315
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
316
+
317
+ it { should expand .to('/') }
318
+ it { should expand(foo: nil) .to('/') }
319
+ it { should expand(foo: '') .to('/') }
320
+ it { should expand(foo: 'foo') .to('/foo') }
321
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
322
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
323
+
324
+ it { should generate_template('/{+foo}') }
325
+ end
326
+
327
+ pattern '/<foo:bar>' do
328
+ it { should match('/foo').capturing bar: 'foo' }
329
+ it { should generate_template('/{bar}') }
330
+
331
+ example do
332
+ expect { Mustermann::Flask.new('/<foo:bar>') }.to \
333
+ raise_error(Mustermann::ParseError, 'unexpected converter "foo" while parsing "/<foo:bar>"')
334
+ end
335
+ end
336
+
337
+ context 'invalid syntax' do
338
+ example 'unexpected end of capture' do
339
+ expect { FlaskSubclass.new('foo>bar') }.
340
+ to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"')
341
+ end
342
+
343
+ example 'missing end of capture' do
344
+ expect { FlaskSubclass.new('foo<bar') }.
345
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo<bar"')
346
+ end
347
+
348
+ example 'unknown converter' do
349
+ expect { FlaskSubclass.new('foo<bar:name>') }.
350
+ to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo<bar:name>"')
351
+ end
352
+
353
+ example 'broken argument synax' do
354
+ expect { FlaskSubclass.new('<string(length=3=2):foo>') }.
355
+ to raise_error(Mustermann::ParseError, 'unexpected = while parsing "<string(length=3=2):foo>"')
356
+ end
357
+
358
+ example 'missing )' do
359
+ expect { FlaskSubclass.new('<string(foo') }.
360
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "<string(foo"')
361
+ end
362
+
363
+ example 'missing ""' do
364
+ expect { FlaskSubclass.new('<string("foo') }.
365
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "<string(\\"foo"')
366
+ end
367
+ end
368
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mustermann-flask
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 Flask 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/flask.rb
35
+ - mustermann-flask.gemspec
36
+ - spec/flask_spec.rb
37
+ - spec/flask_subclass_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: Flask syntax for Mustermann
62
+ test_files:
63
+ - spec/flask_spec.rb
64
+ - spec/flask_subclass_spec.rb
65
+ has_rdoc: