mustermann19 0.3.1 → 0.3.1.1
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.
- checksums.yaml +7 -0
- data/.travis.yml +4 -3
- data/README.md +680 -376
- data/lib/mustermann/ast/compiler.rb +13 -7
- data/lib/mustermann/ast/expander.rb +11 -5
- data/lib/mustermann/ast/node.rb +27 -1
- data/lib/mustermann/ast/param_scanner.rb +20 -0
- data/lib/mustermann/ast/parser.rb +131 -12
- data/lib/mustermann/ast/pattern.rb +45 -6
- data/lib/mustermann/ast/template_generator.rb +28 -0
- data/lib/mustermann/ast/validation.rb +5 -3
- data/lib/mustermann/composite.rb +103 -0
- data/lib/mustermann/expander.rb +1 -1
- data/lib/mustermann/express.rb +34 -0
- data/lib/mustermann/flask.rb +204 -0
- data/lib/mustermann/identity.rb +54 -0
- data/lib/mustermann/pattern.rb +186 -12
- data/lib/mustermann/pattern_cache.rb +49 -0
- data/lib/mustermann/pyramid.rb +25 -0
- data/lib/mustermann/regexp_based.rb +18 -1
- data/lib/mustermann/regular.rb +1 -1
- data/lib/mustermann/shell.rb +8 -0
- data/lib/mustermann/simple.rb +1 -1
- data/lib/mustermann/simple_match.rb +5 -0
- data/lib/mustermann/sinatra.rb +19 -5
- data/lib/mustermann/string_scanner.rb +314 -0
- data/lib/mustermann/template.rb +10 -0
- data/lib/mustermann/to_pattern.rb +11 -6
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +52 -3
- data/mustermann.gemspec +1 -1
- data/spec/composite_spec.rb +147 -0
- data/spec/expander_spec.rb +15 -0
- data/spec/express_spec.rb +209 -0
- data/spec/flask_spec.rb +361 -0
- data/spec/flask_subclass_spec.rb +368 -0
- data/spec/identity_spec.rb +44 -0
- data/spec/mustermann_spec.rb +14 -0
- data/spec/pattern_spec.rb +7 -3
- data/spec/pyramid_spec.rb +101 -0
- data/spec/rails_spec.rb +76 -2
- data/spec/regular_spec.rb +25 -0
- data/spec/shell_spec.rb +33 -0
- data/spec/simple_spec.rb +25 -0
- data/spec/sinatra_spec.rb +184 -9
- data/spec/string_scanner_spec.rb +271 -0
- data/spec/support/expand_matcher.rb +7 -5
- data/spec/support/generate_template_matcher.rb +27 -0
- data/spec/support/pattern.rb +3 -0
- data/spec/support/scan_matcher.rb +63 -0
- data/spec/support.rb +2 -1
- data/spec/template_spec.rb +22 -0
- data/spec/to_pattern_spec.rb +49 -0
- metadata +47 -61
- data/internals.md +0 -64
data/lib/mustermann/sinatra.rb
CHANGED
@@ -9,11 +9,25 @@ module Mustermann
|
|
9
9
|
# @see Mustermann::Pattern
|
10
10
|
# @see file:README.md#sinatra Syntax description in the README
|
11
11
|
class Sinatra < AST::Pattern
|
12
|
-
on(nil, ??, ?)) { |c| unexpected(c) }
|
13
|
-
|
14
|
-
on(
|
15
|
-
on(?:)
|
16
|
-
on(?\\)
|
12
|
+
on(nil, ??, ?), ?|) { |c| unexpected(c) }
|
13
|
+
|
14
|
+
on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
|
15
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
16
|
+
on(?\\) { |c| node(:char, expect(/./)) }
|
17
|
+
|
18
|
+
on ?( do |char|
|
19
|
+
groups = []
|
20
|
+
groups << node(:group) { read unless check(?)) or scan(?|) } until scan(?))
|
21
|
+
groups.size == 1 ? groups.first : node(:union, groups)
|
22
|
+
end
|
23
|
+
|
24
|
+
on ?{ do |char|
|
25
|
+
type = scan(?+) ? :named_splat : :capture
|
26
|
+
name = expect(/[\w\.]+/)
|
27
|
+
type = :splat if type == :named_splat and name == 'splat'
|
28
|
+
expect(?})
|
29
|
+
node(type, name)
|
30
|
+
end
|
17
31
|
|
18
32
|
suffix ?? do |char, element|
|
19
33
|
node(:optional, element)
|
@@ -0,0 +1,314 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/pattern_cache'
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# Class inspired by Ruby's StringScanner to scan an input string using multiple patterns.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# require 'mustermann/string_scanner'
|
10
|
+
# scanner = Mustermann::StringScanner.new("here is our example string")
|
11
|
+
#
|
12
|
+
# scanner.scan("here") # => "here"
|
13
|
+
# scanner.getch # => " "
|
14
|
+
#
|
15
|
+
# if scanner.scan(":verb our")
|
16
|
+
# scanner.scan(:noun, capture: :word)
|
17
|
+
# scanner[:verb] # => "is"
|
18
|
+
# scanner[:nound] # => "example"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# scanner.rest # => "string"
|
22
|
+
#
|
23
|
+
# @note
|
24
|
+
# This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently.
|
25
|
+
# Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour.
|
26
|
+
class StringScanner
|
27
|
+
# Exception raised if scan/unscan operation cannot be performed.
|
28
|
+
ScanError = Class.new(::StringScanner::Error)
|
29
|
+
PATTERN_CACHE = PatternCache.new
|
30
|
+
#private_constant :PATTERN_CACHE
|
31
|
+
|
32
|
+
# Patterns created by {#scan} will be globally cached, since we assume that there is a finite number
|
33
|
+
# of different patterns used and that they are more likely to be reused than not.
|
34
|
+
# This method allows clearing the cache.
|
35
|
+
#
|
36
|
+
# @see Mustermann::PatternCache
|
37
|
+
def self.clear_cache
|
38
|
+
PATTERN_CACHE.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Integer] number of cached patterns
|
42
|
+
# @see clear_cache
|
43
|
+
# @api private
|
44
|
+
def self.cache_size
|
45
|
+
PATTERN_CACHE.size
|
46
|
+
end
|
47
|
+
|
48
|
+
# Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends.
|
49
|
+
# Behaves like a String (the substring which matched the pattern), but also exposes its position
|
50
|
+
# in the main string and any params parsed from it.
|
51
|
+
class ScanResult < DelegateClass(String)
|
52
|
+
# The scanner this result came from.
|
53
|
+
# @example
|
54
|
+
# require 'mustermann/string_scanner'
|
55
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
56
|
+
# scanner.scan(:name).scanner == scanner # => true
|
57
|
+
attr_reader :scanner
|
58
|
+
|
59
|
+
# @example
|
60
|
+
# require 'mustermann/string_scanner'
|
61
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
62
|
+
# scanner.scan(:name).position # => 0
|
63
|
+
# scanner.getch.position # => 3
|
64
|
+
# scanner.scan(:name).position # => 4
|
65
|
+
#
|
66
|
+
# @return [Integer] position the substring starts at
|
67
|
+
attr_reader :position
|
68
|
+
alias_method :pos, :position
|
69
|
+
|
70
|
+
# @example
|
71
|
+
# require 'mustermann/string_scanner'
|
72
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
73
|
+
# scanner.scan(:name).length # => 3
|
74
|
+
# scanner.getch.length # => 1
|
75
|
+
# scanner.scan(:name).length # => 3
|
76
|
+
#
|
77
|
+
# @return [Integer] length of the substring
|
78
|
+
attr_reader :length
|
79
|
+
|
80
|
+
# Params parsed from the substring.
|
81
|
+
# Will not include params from previous scan results.
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# require 'mustermann/string_scanner'
|
85
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
86
|
+
# scanner.scan(:name).params # => { "name" => "foo" }
|
87
|
+
# scanner.getch.params # => {}
|
88
|
+
# scanner.scan(:name).params # => { "name" => "bar" }
|
89
|
+
#
|
90
|
+
# @see Mustermann::StringScanner#params
|
91
|
+
# @see Mustermann::StringScanner#[]
|
92
|
+
#
|
93
|
+
# @return [Hash] params parsed from the substring
|
94
|
+
attr_reader :params
|
95
|
+
|
96
|
+
# @api private
|
97
|
+
def initialize(scanner, position, length, params = {})
|
98
|
+
@scanner, @position, @length, @params = scanner, position, length, params
|
99
|
+
end
|
100
|
+
|
101
|
+
# @api private
|
102
|
+
# @!visibility private
|
103
|
+
def __getobj__
|
104
|
+
@__getobj__ ||= scanner.to_s[position, length]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Hash] default pattern options used for {#scan} and similar methods
|
109
|
+
# @see #initialize
|
110
|
+
attr_reader :pattern_options
|
111
|
+
|
112
|
+
# Params from all previous matches from {#scan} and {#scan_until},
|
113
|
+
# but not from {#check} and {#check_until}. Changes can be reverted
|
114
|
+
# with {#unscan} and it can be completely cleared via {#reset}.
|
115
|
+
#
|
116
|
+
# @return [Hash] current params
|
117
|
+
attr_reader :params
|
118
|
+
|
119
|
+
# @return [Integer] current scan position on the input string
|
120
|
+
attr_accessor :position
|
121
|
+
alias_method :pos, :position
|
122
|
+
alias_method :pos=, :position=
|
123
|
+
|
124
|
+
# @example with different default type
|
125
|
+
# require 'mustermann/string_scanner'
|
126
|
+
# scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell)
|
127
|
+
# scanner.scan('*') # => "foo"
|
128
|
+
# scanner.scan('**/*') # => "/bar/baz"
|
129
|
+
#
|
130
|
+
# @param [String] string the string to scan
|
131
|
+
# @param [Hash] pattern_options default options used for {#scan}
|
132
|
+
def initialize(string = "", pattern_options = {})
|
133
|
+
pattern_options, string = string, {} if string.kind_of?(Hash)
|
134
|
+
@pattern_options = pattern_options
|
135
|
+
@string = String(string).dup
|
136
|
+
reset
|
137
|
+
end
|
138
|
+
|
139
|
+
# Resets the {#position} to the start and clears all {#params}.
|
140
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
141
|
+
def reset
|
142
|
+
@position = 0
|
143
|
+
@params = {}
|
144
|
+
@history = []
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Moves the position to the end of the input string.
|
149
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
150
|
+
def terminate
|
151
|
+
track_result ScanResult.new(self, @position, size - @position)
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
# Checks if the given pattern matches any substring starting at the current position.
|
156
|
+
#
|
157
|
+
# If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
|
158
|
+
# from the substring into {#params}.
|
159
|
+
#
|
160
|
+
# @param (see Mustermann.new)
|
161
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
162
|
+
def scan(pattern, options = {})
|
163
|
+
track_result check(pattern, options)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Checks if the given pattern matches any substring starting at any position after the current position.
|
167
|
+
#
|
168
|
+
# If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
|
169
|
+
# from the substring into {#params}.
|
170
|
+
#
|
171
|
+
# @param (see Mustermann.new)
|
172
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
173
|
+
def scan_until(pattern, options = {})
|
174
|
+
result, prefix = check_until_with_prefix(pattern, options)
|
175
|
+
track_result(prefix, result)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Reverts the last operation that advanced the position.
|
179
|
+
#
|
180
|
+
# Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}.
|
181
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
182
|
+
def unscan
|
183
|
+
raise ScanError, 'unscan failed: previous match record not exist' if @history.empty?
|
184
|
+
previous = @history[0..-2]
|
185
|
+
reset
|
186
|
+
previous.each { |r| track_result(*r) }
|
187
|
+
self
|
188
|
+
end
|
189
|
+
|
190
|
+
# Checks if the given pattern matches any substring starting at the current position.
|
191
|
+
#
|
192
|
+
# Does not affect {#position} or {#params}.
|
193
|
+
#
|
194
|
+
# @param (see Mustermann.new)
|
195
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
196
|
+
def check(pattern, options = {})
|
197
|
+
params, length = create_pattern(pattern, options).peek_params(rest)
|
198
|
+
ScanResult.new(self, @position, length, params) if params
|
199
|
+
end
|
200
|
+
|
201
|
+
# Checks if the given pattern matches any substring starting at any position after the current position.
|
202
|
+
#
|
203
|
+
# Does not affect {#position} or {#params}.
|
204
|
+
#
|
205
|
+
# @param (see Mustermann.new)
|
206
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
207
|
+
def check_until(pattern, options = {})
|
208
|
+
check_until_with_prefix(pattern, options).first
|
209
|
+
end
|
210
|
+
|
211
|
+
def check_until_with_prefix(pattern, options = {})
|
212
|
+
start = @position
|
213
|
+
@position += 1 until eos? or result = check(pattern, options)
|
214
|
+
prefix = ScanResult.new(self, start, @position - start) if result
|
215
|
+
[result, prefix]
|
216
|
+
ensure
|
217
|
+
@position = start
|
218
|
+
end
|
219
|
+
|
220
|
+
# Reads a single character and advances the {#position} by one.
|
221
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string
|
222
|
+
def getch
|
223
|
+
track_result ScanResult.new(self, @position, 1) unless eos?
|
224
|
+
end
|
225
|
+
|
226
|
+
# Appends the given string to the string being scanned
|
227
|
+
#
|
228
|
+
# @example
|
229
|
+
# require 'mustermann/string_scanner'
|
230
|
+
# scanner = Mustermann::StringScanner.new
|
231
|
+
# scanner << "foo"
|
232
|
+
# scanner.scan(/.+/) # => "foo"
|
233
|
+
#
|
234
|
+
# @param [String] string will be appended
|
235
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
236
|
+
def <<(string)
|
237
|
+
@string << string
|
238
|
+
self
|
239
|
+
end
|
240
|
+
|
241
|
+
# @return [true, false] whether or not the end of the string has been reached
|
242
|
+
def eos?
|
243
|
+
@position >= @string.size
|
244
|
+
end
|
245
|
+
|
246
|
+
# @return [true, false] whether or not the current position is at the start of a line
|
247
|
+
def beginning_of_line?
|
248
|
+
@position == 0 or @string[@position - 1] == "\n"
|
249
|
+
end
|
250
|
+
|
251
|
+
# @return [String] outstanding string not yet matched, empty string at end of input string
|
252
|
+
def rest
|
253
|
+
@string[@position..-1] || ""
|
254
|
+
end
|
255
|
+
|
256
|
+
# @return [Integer] number of character remaining to be scanned
|
257
|
+
def rest_size
|
258
|
+
@position > size ? 0 : size - @position
|
259
|
+
end
|
260
|
+
|
261
|
+
# Allows to peek at a number of still unscanned characters without advacing the {#position}.
|
262
|
+
#
|
263
|
+
# @param [Integer] length how many characters to look at
|
264
|
+
# @return [String] the substring
|
265
|
+
def peek(length = 1)
|
266
|
+
@string[@position, length]
|
267
|
+
end
|
268
|
+
|
269
|
+
# Shorthand for accessing {#params}. Accepts symbols as keys.
|
270
|
+
def [](key)
|
271
|
+
params[key.to_s]
|
272
|
+
end
|
273
|
+
|
274
|
+
# (see #params)
|
275
|
+
def to_h
|
276
|
+
params.dup
|
277
|
+
end
|
278
|
+
|
279
|
+
# @return [String] the input string
|
280
|
+
# @see #initialize
|
281
|
+
# @see #<<
|
282
|
+
def to_s
|
283
|
+
@string.dup
|
284
|
+
end
|
285
|
+
|
286
|
+
# @return [Integer] size of the input string
|
287
|
+
def size
|
288
|
+
@string.size
|
289
|
+
end
|
290
|
+
|
291
|
+
# @!visibility private
|
292
|
+
def inspect
|
293
|
+
"#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ]
|
294
|
+
end
|
295
|
+
|
296
|
+
# @!visibility private
|
297
|
+
def create_pattern(pattern, options = {})
|
298
|
+
PATTERN_CACHE.create_pattern(pattern, pattern_options.merge(options))
|
299
|
+
end
|
300
|
+
|
301
|
+
# @!visibility private
|
302
|
+
def track_result(*results)
|
303
|
+
results.compact!
|
304
|
+
@history << results if results.any?
|
305
|
+
results.each do |result|
|
306
|
+
@params.merge! result.params
|
307
|
+
@position += result.length
|
308
|
+
end
|
309
|
+
results.last
|
310
|
+
end
|
311
|
+
|
312
|
+
private :create_pattern, :track_result, :check_until_with_prefix
|
313
|
+
end
|
314
|
+
end
|
data/lib/mustermann/template.rb
CHANGED
@@ -43,6 +43,16 @@ module Mustermann
|
|
43
43
|
@split_params.include? key
|
44
44
|
end
|
45
45
|
|
46
|
+
# Identity patterns support generating templates (the logic is quite complex, though).
|
47
|
+
#
|
48
|
+
# @example (see Mustermann::Pattern#to_templates)
|
49
|
+
# @param (see Mustermann::Pattern#to_templates)
|
50
|
+
# @return (see Mustermann::Pattern#to_templates)
|
51
|
+
# @see Mustermann::Pattern#to_templates
|
52
|
+
def to_templates
|
53
|
+
[to_s]
|
54
|
+
end
|
55
|
+
|
46
56
|
private :compile, :map_param, :always_array?
|
47
57
|
end
|
48
58
|
end
|
@@ -16,8 +16,11 @@ module Mustermann
|
|
16
16
|
#
|
17
17
|
# Foo.new.to_pattern # => #<Mustermann::Sinatra:":foo/:bar">
|
18
18
|
#
|
19
|
-
# By default included into
|
19
|
+
# By default included into String, Symbol, Regexp, Array and {Mustermann::Pattern}.
|
20
20
|
module ToPattern
|
21
|
+
PRIMITIVES = [String, Symbol, Array, Regexp, Mustermann::Pattern]
|
22
|
+
#private_constant :PRIMITIVES
|
23
|
+
|
21
24
|
# Converts the object into a {Mustermann::Pattern}.
|
22
25
|
#
|
23
26
|
# @example converting a string
|
@@ -30,16 +33,18 @@ module Mustermann
|
|
30
33
|
# /.*/.to_pattern # => #<Mustermann::Regular:".*">
|
31
34
|
#
|
32
35
|
# @example converting a pattern
|
33
|
-
#
|
36
|
+
# Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
|
34
37
|
#
|
35
38
|
# @param [Hash] options The options hash.
|
36
39
|
# @return [Mustermann::Pattern] pattern corresponding to object.
|
37
40
|
def to_pattern(options = {})
|
38
|
-
|
41
|
+
input = self if PRIMITIVES.any? { |p| self.is_a? p }
|
42
|
+
input ||= __getobj__ if respond_to?(:__getobj__)
|
43
|
+
Mustermann.new(input || to_s, options)
|
39
44
|
end
|
40
45
|
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
PRIMITIVES.each do |klass|
|
47
|
+
append_features(klass)
|
48
|
+
end
|
44
49
|
end
|
45
50
|
end
|
data/lib/mustermann/version.rb
CHANGED
data/lib/mustermann.rb
CHANGED
@@ -1,22 +1,71 @@
|
|
1
1
|
require 'mustermann/pattern'
|
2
|
+
require 'mustermann/composite'
|
2
3
|
|
3
4
|
# Namespace and main entry point for the Mustermann library.
|
4
5
|
#
|
5
6
|
# Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
|
6
7
|
module Mustermann
|
7
|
-
#
|
8
|
+
# Creates a new pattern based on input.
|
9
|
+
#
|
10
|
+
# * From {Mustermann::Pattern}: returns given pattern.
|
11
|
+
# * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra})
|
12
|
+
# * From Regexp: creates a {Mustermann::Regular} pattern.
|
13
|
+
# * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input.
|
14
|
+
# * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}.
|
15
|
+
# * From anything else: Will try to call to_pattern on it or raise a TypeError.
|
16
|
+
#
|
17
|
+
# Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is
|
18
|
+
# called on the object, the type will be handed on but might be ignored by the input object.
|
19
|
+
#
|
20
|
+
# If you want to enforce the pattern type, you should create them via their expected class.
|
21
|
+
#
|
22
|
+
# @example creating patterns
|
23
|
+
# require 'mustermann'
|
24
|
+
#
|
25
|
+
# Mustermann.new("/:name") # => #<Mustermann::Sinatra:"/example">
|
26
|
+
# Mustermann.new("/{name}", type: :template) # => #<Mustermann::Template:"/{name}">
|
27
|
+
# Mustermann.new(/.*/) # => #<Mustermann::Regular:".*">
|
28
|
+
# Mustermann.new(:name, capture: :word) # => #<Mustermann::Sinatra:":name">
|
29
|
+
# Mustermann.new("/", "/*.jpg", type: :shell) # => #<Mustermann::Composite:(shell:"/" | shell:"/*.jpg")>
|
30
|
+
#
|
31
|
+
# @example using custom #to_pattern
|
32
|
+
# require 'mustermann'
|
33
|
+
#
|
34
|
+
# class MyObject
|
35
|
+
# def to_pattern(**options)
|
36
|
+
# Mustermann.new("/:name", **options)
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# Mustermann.new(MyObject.new, type: :rails) # => #<Mustermann::Rails:"/:name">
|
41
|
+
#
|
42
|
+
# @example enforcing type
|
43
|
+
# require 'mustermann/sinatra'
|
44
|
+
#
|
45
|
+
# Mustermann::Sinatra.new("/:name")
|
46
|
+
#
|
47
|
+
# @param [String, Pattern, Regexp, Symbol, #to_pattern, Array<String, Pattern, Regexp, Symbol, #to_pattern>]
|
48
|
+
# input The representation of the pattern
|
8
49
|
# @param [Hash] options The options hash
|
9
50
|
# @return [Mustermann::Pattern] pattern corresponding to string.
|
10
51
|
# @raise (see [])
|
11
52
|
# @raise (see Mustermann::Pattern.new)
|
53
|
+
# @raise [TypeError] if the passed object cannot be converted to a pattern
|
12
54
|
# @see file:README.md#Types_and_Options "Types and Options" in the README
|
13
|
-
def self.new(input
|
55
|
+
def self.new(*input)
|
56
|
+
options = input.last.kind_of?(Hash) ? input.pop : {}
|
14
57
|
type = options.delete(:type) || :sinatra
|
58
|
+
input = input.first if input.size < 2
|
15
59
|
case input
|
16
60
|
when Pattern then input
|
17
61
|
when Regexp then self[:regexp].new(input, options)
|
18
62
|
when String then self[type].new(input, options)
|
19
|
-
|
63
|
+
when Array then Composite.new(input, options.merge(:type => type))
|
64
|
+
when Symbol then self[:sinatra].new(input.inspect, options)
|
65
|
+
else
|
66
|
+
pattern = input.to_pattern(options.merge(:type => type)) if input.respond_to? :to_pattern
|
67
|
+
raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil?
|
68
|
+
pattern
|
20
69
|
end
|
21
70
|
end
|
22
71
|
|
data/mustermann.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.files = `git ls-files`.split("\n")
|
14
14
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
15
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
-
s.extra_rdoc_files = %w[README.md
|
16
|
+
s.extra_rdoc_files = %w[README.md]
|
17
17
|
s.require_path = 'lib'
|
18
18
|
s.required_ruby_version = '>= 1.9.2'
|
19
19
|
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'support'
|
2
|
+
require 'mustermann'
|
3
|
+
|
4
|
+
describe Mustermann::Composite do
|
5
|
+
describe :new do
|
6
|
+
example 'with no argument' do
|
7
|
+
expect { Mustermann::Composite.new }.
|
8
|
+
to raise_error(ArgumentError, 'cannot create empty composite pattern')
|
9
|
+
end
|
10
|
+
|
11
|
+
example 'with one argument' do
|
12
|
+
pattern = Mustermann.new('/foo')
|
13
|
+
Mustermann::Composite.new(pattern).should be == pattern
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context :| do
|
18
|
+
subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second') }
|
19
|
+
|
20
|
+
describe :== do
|
21
|
+
example { subject.should be == subject }
|
22
|
+
example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second') }
|
23
|
+
example { subject.should_not be == Mustermann.new('/foo/:name') }
|
24
|
+
example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe :=== do
|
28
|
+
example { subject.should be === "/foo/bar" }
|
29
|
+
example { subject.should be === "/fox/bar" }
|
30
|
+
example { subject.should_not be === "/foo" }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe :params do
|
34
|
+
example { subject.params("/foo/bar") .should be == { "name" => "bar" } }
|
35
|
+
example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } }
|
36
|
+
example { subject.params("/foo") .should be_nil }
|
37
|
+
end
|
38
|
+
|
39
|
+
describe :=== do
|
40
|
+
example { subject.should match("/foo/bar") }
|
41
|
+
example { subject.should match("/fox/bar") }
|
42
|
+
example { subject.should_not match("/foo") }
|
43
|
+
end
|
44
|
+
|
45
|
+
describe :expand do
|
46
|
+
example { subject.should respond_to(:expand) }
|
47
|
+
example { subject.expand(name: 'bar') .should be == '/foo/bar' }
|
48
|
+
example { subject.expand(first: 'fox', second: 'bar') .should be == '/fox/bar' }
|
49
|
+
|
50
|
+
context "without expandable patterns" do
|
51
|
+
subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) }
|
52
|
+
example { subject.should_not respond_to(:expand) }
|
53
|
+
example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe :to_templates do
|
58
|
+
example { should respond_to(:to_templates) }
|
59
|
+
example { should generate_templates('/foo/{name}', '/{first}/{second}') }
|
60
|
+
|
61
|
+
context "without patterns implementing to_templates" do
|
62
|
+
subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) }
|
63
|
+
example { should_not respond_to(:to_templates) }
|
64
|
+
example { expect { subject.to_templates }.to raise_error(NotImplementedError) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context :& do
|
70
|
+
subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
|
71
|
+
|
72
|
+
describe :== do
|
73
|
+
example { subject.should be == subject }
|
74
|
+
example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
|
75
|
+
example { subject.should_not be == Mustermann.new('/foo/:name') }
|
76
|
+
example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') }
|
77
|
+
end
|
78
|
+
|
79
|
+
describe :=== do
|
80
|
+
example { subject.should be === "/foo/bar" }
|
81
|
+
example { subject.should_not be === "/fox/bar" }
|
82
|
+
example { subject.should_not be === "/foo" }
|
83
|
+
end
|
84
|
+
|
85
|
+
describe :params do
|
86
|
+
example { subject.params("/foo/bar") .should be == { "name" => "bar" } }
|
87
|
+
example { subject.params("/fox/bar") .should be_nil }
|
88
|
+
example { subject.params("/foo") .should be_nil }
|
89
|
+
end
|
90
|
+
|
91
|
+
describe :match do
|
92
|
+
example { subject.should match("/foo/bar") }
|
93
|
+
example { subject.should_not match("/fox/bar") }
|
94
|
+
example { subject.should_not match("/foo") }
|
95
|
+
end
|
96
|
+
|
97
|
+
describe :expand do
|
98
|
+
example { subject.should_not respond_to(:expand) }
|
99
|
+
example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context :^ do
|
104
|
+
subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :^) }
|
105
|
+
|
106
|
+
describe :== do
|
107
|
+
example { subject.should be == subject }
|
108
|
+
example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') }
|
109
|
+
example { subject.should_not be == Mustermann.new('/foo/:name') }
|
110
|
+
example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) }
|
111
|
+
end
|
112
|
+
|
113
|
+
describe :=== do
|
114
|
+
example { subject.should_not be === "/foo/bar" }
|
115
|
+
example { subject.should be === "/fox/bar" }
|
116
|
+
example { subject.should_not be === "/foo" }
|
117
|
+
end
|
118
|
+
|
119
|
+
describe :params do
|
120
|
+
example { subject.params("/foo/bar") .should be_nil }
|
121
|
+
example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } }
|
122
|
+
example { subject.params("/foo") .should be_nil }
|
123
|
+
end
|
124
|
+
|
125
|
+
describe :match do
|
126
|
+
example { subject.should_not match("/foo/bar") }
|
127
|
+
example { subject.should match("/fox/bar") }
|
128
|
+
example { subject.should_not match("/foo") }
|
129
|
+
end
|
130
|
+
|
131
|
+
describe :expand do
|
132
|
+
example { subject.should_not respond_to(:expand) }
|
133
|
+
example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe :inspect do
|
138
|
+
let(:sinatra) { Mustermann.new('x') }
|
139
|
+
let(:rails) { Mustermann.new('x', type: :rails) }
|
140
|
+
let(:identity) { Mustermann.new('x', type: :identity) }
|
141
|
+
|
142
|
+
example { (sinatra | rails) .inspect.should include('(sinatra:"x" | rails:"x")') }
|
143
|
+
example { (sinatra ^ rails) .inspect.should include('(sinatra:"x" ^ rails:"x")') }
|
144
|
+
example { (sinatra | rails | identity) .inspect.should include('(sinatra:"x" | rails:"x" | identity:"x")') }
|
145
|
+
example { (sinatra | rails & identity) .inspect.should include('(sinatra:"x" | (rails:"x" & identity:"x"))') }
|
146
|
+
end
|
147
|
+
end
|
data/spec/expander_spec.rb
CHANGED
@@ -30,6 +30,21 @@ describe Mustermann::Expander do
|
|
30
30
|
expander.expand(foo: 'pony', ext: nil).should be == '/pony'
|
31
31
|
end
|
32
32
|
|
33
|
+
it 'supports splat' do
|
34
|
+
expander = Mustermann::Expander.new << Mustermann.new("/foo/*/baz")
|
35
|
+
expander.expand(splat: 'bar').should be == '/foo/bar/baz'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'supports multiple splats' do
|
39
|
+
expander = Mustermann::Expander.new << Mustermann.new("/foo/*/bar/*")
|
40
|
+
expander.expand(splat: [123, 456]).should be == '/foo/123/bar/456'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'supports identity patterns' do
|
44
|
+
expander = Mustermann::Expander.new('/:foo', type: :identity)
|
45
|
+
expander.expand.should be == '/:foo'
|
46
|
+
end
|
47
|
+
|
33
48
|
describe :additional_values do
|
34
49
|
context "illegal value" do
|
35
50
|
example { expect { Mustermann::Expander.new(additional_values: :foo) }.to raise_error(ArgumentError) }
|