mustermann19 0.3.1 → 0.3.1.1

Sign up to get free protection for your applications and to get access to all the features.
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) }