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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +4 -3
  3. data/README.md +680 -376
  4. data/lib/mustermann/ast/compiler.rb +13 -7
  5. data/lib/mustermann/ast/expander.rb +11 -5
  6. data/lib/mustermann/ast/node.rb +27 -1
  7. data/lib/mustermann/ast/param_scanner.rb +20 -0
  8. data/lib/mustermann/ast/parser.rb +131 -12
  9. data/lib/mustermann/ast/pattern.rb +45 -6
  10. data/lib/mustermann/ast/template_generator.rb +28 -0
  11. data/lib/mustermann/ast/validation.rb +5 -3
  12. data/lib/mustermann/composite.rb +103 -0
  13. data/lib/mustermann/expander.rb +1 -1
  14. data/lib/mustermann/express.rb +34 -0
  15. data/lib/mustermann/flask.rb +204 -0
  16. data/lib/mustermann/identity.rb +54 -0
  17. data/lib/mustermann/pattern.rb +186 -12
  18. data/lib/mustermann/pattern_cache.rb +49 -0
  19. data/lib/mustermann/pyramid.rb +25 -0
  20. data/lib/mustermann/regexp_based.rb +18 -1
  21. data/lib/mustermann/regular.rb +1 -1
  22. data/lib/mustermann/shell.rb +8 -0
  23. data/lib/mustermann/simple.rb +1 -1
  24. data/lib/mustermann/simple_match.rb +5 -0
  25. data/lib/mustermann/sinatra.rb +19 -5
  26. data/lib/mustermann/string_scanner.rb +314 -0
  27. data/lib/mustermann/template.rb +10 -0
  28. data/lib/mustermann/to_pattern.rb +11 -6
  29. data/lib/mustermann/version.rb +1 -1
  30. data/lib/mustermann.rb +52 -3
  31. data/mustermann.gemspec +1 -1
  32. data/spec/composite_spec.rb +147 -0
  33. data/spec/expander_spec.rb +15 -0
  34. data/spec/express_spec.rb +209 -0
  35. data/spec/flask_spec.rb +361 -0
  36. data/spec/flask_subclass_spec.rb +368 -0
  37. data/spec/identity_spec.rb +44 -0
  38. data/spec/mustermann_spec.rb +14 -0
  39. data/spec/pattern_spec.rb +7 -3
  40. data/spec/pyramid_spec.rb +101 -0
  41. data/spec/rails_spec.rb +76 -2
  42. data/spec/regular_spec.rb +25 -0
  43. data/spec/shell_spec.rb +33 -0
  44. data/spec/simple_spec.rb +25 -0
  45. data/spec/sinatra_spec.rb +184 -9
  46. data/spec/string_scanner_spec.rb +271 -0
  47. data/spec/support/expand_matcher.rb +7 -5
  48. data/spec/support/generate_template_matcher.rb +27 -0
  49. data/spec/support/pattern.rb +3 -0
  50. data/spec/support/scan_matcher.rb +63 -0
  51. data/spec/support.rb +2 -1
  52. data/spec/template_spec.rb +22 -0
  53. data/spec/to_pattern_spec.rb +49 -0
  54. metadata +47 -61
  55. data/internals.md +0 -64
@@ -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
- on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
14
- on(?() { |c| node(:group) { read unless scan(?)) } }
15
- on(?:) { |c| node(:capture) { scan(/\w+/) } }
16
- on(?\\) { |c| node(:char, expect(/./)) }
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
@@ -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 {String}, {Symbol}, {Regexp} and {Mustermann::Pattern}.
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
- # Mustermann.new("foo").to_pattern # => #<Mustermann::Sinatra:"foo">
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
- Mustermann.new(self, options)
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
- append_features String
42
- append_features Regexp
43
- append_features Mustermann::Pattern
46
+ PRIMITIVES.each do |klass|
47
+ append_features(klass)
48
+ end
44
49
  end
45
50
  end
@@ -1,3 +1,3 @@
1
1
  module Mustermann
2
- VERSION ||= '0.3.1'
2
+ VERSION ||= '0.3.1.1'
3
3
  end
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
- # @param [String, Pattern, Regexp, #to_pattern] input The representation of the new pattern
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, options = {})
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
- else input.to_pattern(options.merge(type: type))
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 internals.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
@@ -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) }