usher 0.7.5 → 0.8.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.
@@ -7,4 +7,6 @@ class Usher
7
7
  class MissingParameterException < RuntimeError; end
8
8
  # Raised when a route is added with identical variable names and allow_identical_variable_names? is false
9
9
  class MultipleParameterException < RuntimeError; end
10
+ # Raised when a route is added with two regex validators
11
+ class DoubleRegexpException < RuntimeError; end
10
12
  end
@@ -33,7 +33,7 @@ class Usher
33
33
  if redirect_on_trailing_delimiters
34
34
  options[:ignore_trailing_delimiters] = true
35
35
  end
36
- usher_options = {:request_methods => request_methods, :generator => generator, :allow_identical_variable_names => allow_identical_variable_names}
36
+ usher_options = {:request_methods => request_methods, :generator => generator, :allow_identical_variable_names => allow_identical_variable_names, :detailed_failure => true}
37
37
  usher_options.merge!(options)
38
38
  @router = Usher.new(usher_options)
39
39
  @router.route_class = Rack::Route
@@ -120,12 +120,12 @@ class Usher
120
120
  env[router_key] = self
121
121
  request = ::Rack::Request.new(env)
122
122
  response = @router.recognize(request, request.path_info)
123
- if redirect_on_trailing_delimiters and response.only_trailing_delimiters and (request.get? || request.head?)
123
+ if response.succeeded? && redirect_on_trailing_delimiters and response.only_trailing_delimiters and (request.get? || request.head?)
124
124
  response = ::Rack::Response.new
125
125
  response.redirect(request.path_info[0, request.path_info.size - 1], 302)
126
126
  response.finish
127
127
  else
128
- after_match(request, response) if response
128
+ after_match(request, response) if response.succeeded?
129
129
  determine_respondant(response).call(env)
130
130
  end
131
131
  end
@@ -161,11 +161,15 @@ class Usher
161
161
  #
162
162
  # @api private
163
163
  def determine_respondant(response)
164
- usable_response = use_destinations? && response && response.destination
164
+ usable_response = response.succeeded? && use_destinations? && response && response.destination
165
165
  if usable_response && response.destination.respond_to?(:call)
166
166
  response.destination
167
167
  elsif usable_response && response.destination.respond_to?(:args) && response.destination.args.first.respond_to?(:call)
168
168
  response.args.first
169
+ elsif !response.succeeded? && response.request_method?
170
+ rack_response = ::Rack::Response.new("Method not allowed", 405)
171
+ rack_response['Allow'] = response.acceptable_responses_only_strings.join(", ")
172
+ proc { |env| rack_response.finish }
169
173
  else
170
174
  _app
171
175
  end
@@ -23,10 +23,18 @@ class Usher
23
23
  private
24
24
  def route!(base=self.class, pass_block=nil)
25
25
  if base.router and match = base.router.recognize(@request, @request.path_info)
26
- @block_params = match.params.map { |p| p.last }
27
- (@params ||= {}).merge!(match.params_as_hash)
28
- pass_block = catch(:pass) do
29
- route_eval(&match.destination)
26
+ if match.succeeded?
27
+ @block_params = match.params.map { |p| p.last }
28
+ (@params ||= {}).merge!(match.params_as_hash)
29
+ pass_block = catch(:pass) do
30
+ route_eval(&match.destination)
31
+ end
32
+ elsif match.request_method?
33
+ route_eval {
34
+ response['Allow'] = match.acceptable_responses_only_strings.join(", ")
35
+ status 405
36
+ }
37
+ return
30
38
  end
31
39
  end
32
40
 
@@ -76,7 +84,8 @@ class Usher
76
84
  :ignore_trailing_delimiters => true,
77
85
  :generator => Usher::Util::Generators::URL.new,
78
86
  :delimiters => ['/', '.', '-'],
79
- :valid_regex => '[0-9A-Za-z\$_\+!\*\',]+')
87
+ :valid_regex => '[0-9A-Za-z\$_\+!\*\',]+',
88
+ :detailed_failure => true)
80
89
  block_given? ? yield(@router) : @router
81
90
  end
82
91
 
@@ -114,6 +123,25 @@ class Usher
114
123
  </html>
115
124
  HTML
116
125
  end
126
+ error 405 do
127
+ content_type 'text/html'
128
+
129
+ (<<-HTML).gsub(/^ {17}/, '')
130
+ <!DOCTYPE html>
131
+ <html>
132
+ <head>
133
+ <style type="text/css">
134
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
135
+ color:#888;margin:20px}
136
+ #c {margin:0 auto;width:500px;text-align:left}
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <h2>Sinatra sorta knows this ditty, but the request method is not allowed.</h2>
141
+ </body>
142
+ </html>
143
+ HTML
144
+ end
117
145
  end
118
146
 
119
147
  @_configured = true
@@ -0,0 +1,34 @@
1
+ class Usher
2
+ class Node
3
+ # The response from {Usher::Node::Root#lookup}. Adds some convenience methods for common parameter manipulation.
4
+ class FailedResponse < Struct.new(:last_matching_node, :fail_type, :fail_sub_type)
5
+ # The success of the response
6
+ # @return [Boolean] Always returns false
7
+ def succeeded?
8
+ false
9
+ end
10
+
11
+ def request_method?
12
+ fail_type == :request_method
13
+ end
14
+
15
+ def normal_or_greedy?
16
+ fail_type == :normal_or_greedy
17
+ end
18
+
19
+ def acceptable_responses
20
+ case fail_type
21
+ when :request_method
22
+ last_matching_node.request.keys
23
+ when :normal_or_greedy
24
+ (last_matching_node.greedy || []) + (last_matching_node.normal || [])
25
+ end
26
+ end
27
+
28
+ def acceptable_responses_only_strings
29
+ acceptable_responses.select{|r| r.is_a?(String)}
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -3,6 +3,12 @@ class Usher
3
3
  # The response from {Usher::Node::Root#lookup}. Adds some convenience methods for common parameter manipulation.
4
4
  class Response < Struct.new(:path, :params_as_array, :remaining_path, :matched_path, :only_trailing_delimiters)
5
5
 
6
+ # The success of the response
7
+ # @return [Boolean] Always returns true
8
+ def succeeded?
9
+ true
10
+ end
11
+
6
12
  # The params from recognition
7
13
  # @return [Array<Symbol, String>] The parameters detected from recognition returned as an array of arrays.
8
14
  def params
@@ -13,7 +13,7 @@ class Usher
13
13
  if path.size > 1
14
14
  new_path = path.gsub(@stripper, '')
15
15
  response = lookup_without_stripping(request_object, new_path)
16
- response.only_trailing_delimiters = (new_path.size != path.size) if response
16
+ response.only_trailing_delimiters = (new_path.size != path.size) if response && response.succeeded?
17
17
  response
18
18
  else
19
19
  lookup_without_stripping(request_object, path)
data/lib/usher/node.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require File.join('usher', 'node', 'root')
2
2
  require File.join('usher', 'node', 'root_ignoring_trailing_delimiters')
3
3
  require File.join('usher', 'node', 'response')
4
+ require File.join('usher', 'node', 'failed_response')
4
5
 
5
6
  class Usher
6
7
 
@@ -120,19 +121,25 @@ class Usher
120
121
  route_candidates << ret
121
122
  end
122
123
  route_candidates.sort!{|r1, r2| r1.path.route.priority <=> r2.path.route.priority}
123
- route_candidates.last
124
+ request_method_respond(route_candidates.last, request_method_type)
124
125
  else
125
126
  if specific_node = request[request_object.send(request_method_type)] and ret = specific_node.find(request_object, original_path, path.dup, params && params.dup)
126
127
  ret
127
128
  elsif general_node = request[nil] and ret = general_node.find(request_object, original_path, path.dup, params && params.dup)
128
- ret
129
+ request_method_respond(ret, request_method_type)
130
+ else
131
+ request_method_respond(nil, request_method_type)
129
132
  end
130
133
  end
131
134
  else
132
- nil
135
+ route_set.detailed_failure? ? FailedResponse.new(self, :normal_or_greedy, nil) : nil
133
136
  end
134
137
  end
135
138
 
139
+ def request_method_respond(ret, request_method_respond)
140
+ ret || (route_set.detailed_failure? ? FailedResponse.new(self, :request_method, request_method_respond) : nil)
141
+ end
142
+
136
143
  def activate_normal!
137
144
  @normal ||= {}
138
145
  end
@@ -12,7 +12,7 @@ class Usher
12
12
  include Validator
13
13
  def valid!(val)
14
14
  begin
15
- @validator.call(val)
15
+ @validator.call(val) or raise(ValidationException.new("#{val} does not conform to #{@validator}"))
16
16
  rescue Exception => e
17
17
  raise ValidationException.new("#{val} does not conform to #{@validator}, root cause #{e.inspect}")
18
18
  end
@@ -1,7 +1,7 @@
1
1
  class Usher
2
2
  class Splitter
3
3
 
4
- def self.for_delimiters(delimiters_array)
4
+ def self.new(delimiters_array)
5
5
  delimiters = Delimiters.new(delimiters_array)
6
6
  delimiters.any?{|d| d.size > 1} ?
7
7
  MultiCharacterSplitterInstance.new(delimiters) :
@@ -13,7 +13,7 @@ class Usher
13
13
  def initialize(delimiters)
14
14
  @url_split_regex = Regexp.new("[^#{delimiters.regexp_char_class}]+|[#{delimiters.regexp_char_class}]")
15
15
  end
16
-
16
+
17
17
  def split(path)
18
18
  path.scan(@url_split_regex)
19
19
  end
@@ -178,7 +178,7 @@ class Usher
178
178
  when Array
179
179
  v.each do |v_part|
180
180
  extra_params_result << '&' unless extra_params_result.empty?
181
- extra_params_result << Rack::Utils.escape("#{k.to_s}[]") << '=' << Rack::Utils.escape(v_part.to_s)
181
+ extra_params_result << Rack::Utils.escape(k.to_s) << '%5B%5D=' << Rack::Utils.escape(v_part.to_s)
182
182
  end
183
183
  else
184
184
  extra_params_result << '&' unless extra_params_result.empty?
@@ -6,16 +6,9 @@ class Usher
6
6
 
7
7
  attr_reader :router
8
8
 
9
- def self.for_delimiters(router, valid_regex)
10
- new(
11
- router,
12
- Regexp.new('((:|\*)?' + valid_regex + '|' + router.delimiters_regex + '|\(|\)|\||\{)')
13
- )
14
- end
15
-
16
- def initialize(router, split_regex)
9
+ def initialize(router, valid_regex)
17
10
  @router = router
18
- @split_regex = split_regex
11
+ @split_regex = Regexp.new('((:|\*)?' + valid_regex + '|' + router.delimiters_regex + '|\(|\)|\||\{)')
19
12
  @delimiters_regex = Regexp.new(router.delimiters_regex)
20
13
  end
21
14
 
@@ -102,12 +95,10 @@ class Usher
102
95
  end
103
96
 
104
97
  case part[0]
105
- when ?*
106
- var_name = part[1, part.size - 1].to_sym
107
- current_group << Usher::Route::Variable::Glob.new(part[1, part.size - 1], nil, requirements && requirements[var_name])
108
- when ?:
98
+ when ?*, ?:
99
+ variable_class = part[0] == ?* ? Usher::Route::Variable::Glob : Usher::Route::Variable::Single
109
100
  var_name = part[1, part.size - 1].to_sym
110
- current_group << Usher::Route::Variable::Single.new(part[1, part.size - 1], nil, requirements && requirements[var_name])
101
+ current_group << variable_class.new(part[1, part.size - 1], requirements && requirements[var_name].is_a?(Regexp) ? requirements[var_name] : nil, requirements && requirements[var_name])
111
102
  when ?{
112
103
  pattern = ''
113
104
  count = 1
@@ -131,6 +122,7 @@ class Usher
131
122
  when ?: then Usher::Route::Variable::Single
132
123
  end
133
124
  variable_name = variable[0, variable.size - 1].to_sym
125
+ raise DoubleRegexpException.new("#{variable_name} has two regex validators, #{pattern} and #{requirements[variable_name]}") if requirements && requirements[variable_name] && requirements[variable_name].is_a?(Regexp)
134
126
  current_group << variable_class.new(variable_name, Regexp.new(pattern), requirements && requirements[variable_name])
135
127
  elsif simple
136
128
  static = Usher::Route::Static::Greedy.new(pattern)
data/lib/usher.rb CHANGED
@@ -56,7 +56,7 @@ class Usher
56
56
  @routes = []
57
57
  @grapher = Grapher.new(self)
58
58
  @priority_lookups = false
59
- @parser = Util::Parser.for_delimiters(self, valid_regex)
59
+ @parser = Util::Parser.new(self, valid_regex)
60
60
  end
61
61
 
62
62
  # Creates a route set, with options
@@ -67,6 +67,7 @@ class Usher
67
67
  # @option options [nil or Generator] :generator (nil) Take a look at `Usher::Util::Generators for examples.`.
68
68
  # @option options [Boolean] :ignore_trailing_delimiters (false) Ignore trailing delimiters in recognizing paths.
69
69
  # @option options [Boolean] :consider_destination_keys (false) When generating, and using hash destinations, you can have Usher use the destination hash to match incoming params.
70
+ # @option options [Boolean] :detailed_failure (false) When a route fails to match, return a {Node::FailedResponse} instead of a `nil`
70
71
  # Example, you create a route with a destination of :controller => 'test', :action => 'action'. If you made a call to generator with :controller => 'test',
71
72
  # :action => 'action', it would pick that route to use for generation.
72
73
  # @option options [Boolean] :allow_identical_variable_names (true) When adding routes, allow identical variable names to be used.
@@ -79,6 +80,8 @@ class Usher
79
80
  self.ignore_trailing_delimiters = options && options.key?(:ignore_trailing_delimiters) ? options.delete(:ignore_trailing_delimiters) : false
80
81
  self.consider_destination_keys = options && options.key?(:consider_destination_keys) ? options.delete(:consider_destination_keys) : false
81
82
  self.allow_identical_variable_names = options && options.key?(:allow_identical_variable_names) ? options.delete(:allow_identical_variable_names) : true
83
+ self.detailed_failure = options && options.key?(:detailed_failure) ? options.delete(:detailed_failure) : false
84
+
82
85
  unless options.nil? || options.empty?
83
86
  raise "unrecognized options -- #{options.keys.join(', ')}"
84
87
  end
@@ -90,6 +93,11 @@ class Usher
90
93
  @allow_identical_variable_names
91
94
  end
92
95
 
96
+ # @return [Boolean] State of detailed_failure feature.
97
+ def detailed_failure?
98
+ @detailed_failure
99
+ end
100
+
93
101
  # @return [Boolean] State of ignore_trailing_delimiters feature.
94
102
  def ignore_trailing_delimiters?
95
103
  @ignore_trailing_delimiters
@@ -320,7 +328,7 @@ class Usher
320
328
 
321
329
  private
322
330
 
323
- attr_accessor :request_methods, :ignore_trailing_delimiters, :consider_destination_keys, :allow_identical_variable_names
331
+ attr_accessor :request_methods, :ignore_trailing_delimiters, :consider_destination_keys, :allow_identical_variable_names, :detailed_failure
324
332
  attr_reader :valid_regex
325
333
  attr_writer :parser
326
334
 
@@ -340,7 +348,7 @@ class Usher
340
348
 
341
349
  def valid_regex=(valid_regex)
342
350
  @valid_regex = valid_regex
343
- @splitter = Splitter.for_delimiters(self.delimiters)
351
+ @splitter = Splitter.new(self.delimiters)
344
352
  @valid_regex
345
353
  end
346
354
 
@@ -66,6 +66,15 @@ describe "Usher (for rack) route dispatching" do
66
66
  end
67
67
  end
68
68
 
69
+ it "should returns HTTP 405 if the method mis-matches" do
70
+ route_set.reset!
71
+ route_set.add('/sample', :conditions => {:request_method => 'POST'}).to(@app)
72
+ route_set.add('/sample', :conditions => {:request_method => 'PUT'}).to(@app)
73
+ response = route_set.call_with_mock_request('/sample', 'GET')
74
+ response.status.should eql(405)
75
+ response['Allow'].should == 'POST, PUT'
76
+ end
77
+
69
78
  it "should returns HTTP 404 if route doesn't exist" do
70
79
  response = route_set.call_with_mock_request("/not-existing-url")
71
80
  response.status.should eql(404)
@@ -288,9 +288,18 @@ describe "Usher route recognition" do
288
288
  result.params.should == [[:name, "homer"],[:surname, "simpson"]]
289
289
  end
290
290
 
291
- it "should should raise if malformed variables are used" do
291
+ it "should use a regexp requirement as part of recognition" do
292
292
  @route_set.add_route('/products/show/:id', :id => /\d+/, :conditions => {:method => 'get'})
293
- proc {@route_set.recognize(build_request({:method => 'get', :path => '/products/show/qweasd', :domain => 'admin.host.com'}))}.should raise_error
293
+ @route_set.recognize(build_request({:method => 'get', :path => '/products/show/qweasd', :domain => 'admin.host.com'})).should be_nil
294
+ end
295
+
296
+ it "should use a inline regexp and proc requirement as part of recognition" do
297
+ @route_set.add_route('/products/show/{:id,^\d+$}', :id => proc{|v| v == '123'}, :conditions => {:method => 'get'})
298
+ proc { @route_set.recognize(build_request({:method => 'get', :path => '/products/show/234', :domain => 'admin.host.com'}))}.should raise_error(Usher::ValidationException)
299
+ end
300
+
301
+ it "should not allow the use of an inline regexp and regexp requirement as part of recognition" do
302
+ proc { @route_set.add_route('/products/show/{:id,^\d+$}', :id => /\d+/, :conditions => {:method => 'get'}) }.should raise_error(Usher::DoubleRegexpException)
294
303
  end
295
304
 
296
305
  it "should recognize multiple optional parts" do
@@ -117,4 +117,16 @@ describe "Usher (for Sinatra) route recognition" do
117
117
  end
118
118
  end
119
119
 
120
+ describe "method not allowed" do
121
+
122
+ it "should correctly generate a not found page without images and return a 405" do
123
+ @app.post('/bar') { 'found' }
124
+ @app.put('/bar') { 'found' }
125
+ response = @app.call_with_mock_request('/bar')
126
+ response.status.should == 405
127
+ response.headers['Allow'].should == 'POST, PUT'
128
+ response.body.should_not match(/__sinatra__/)
129
+ end
130
+ end
131
+
120
132
  end
@@ -4,37 +4,37 @@ require "usher"
4
4
  describe Usher::Splitter, "#split" do
5
5
  describe "when there are single-character delimiters" do
6
6
  it "should split correctly" do
7
- Usher::Splitter.for_delimiters(['.', '/']).split('/one/two.three/').should == ['/', 'one', '/', 'two', '.', 'three', '/']
7
+ Usher::Splitter.new(['.', '/']).split('/one/two.three/').should == ['/', 'one', '/', 'two', '.', 'three', '/']
8
8
  end
9
9
  end
10
10
 
11
11
  describe "when there are multi-character delimiters" do
12
12
  it "should split correctly" do
13
- Usher::Splitter.for_delimiters(['/', '%28', '%29']).split('/one%28two%29three/').should == ['/', 'one', '%28', 'two', '%29', 'three', '/']
13
+ Usher::Splitter.new(['/', '%28', '%29']).split('/one%28two%29three/').should == ['/', 'one', '%28', 'two', '%29', 'three', '/']
14
14
  end
15
15
  end
16
16
 
17
17
  describe "when there is no delimiter in the end" do
18
18
  it "should split correctly" do
19
- Usher::Splitter.for_delimiters(['.', '/']).split('/one/two.three').should == ['/', 'one', '/', 'two', '.', 'three']
19
+ Usher::Splitter.new(['.', '/']).split('/one/two.three').should == ['/', 'one', '/', 'two', '.', 'three']
20
20
  end
21
21
  end
22
22
 
23
23
  describe "when there is no delimiter in the beginning" do
24
24
  it "should split correctly" do
25
- Usher::Splitter.for_delimiters(['.', '/']).split('one/two.three/').should == ['one', '/', 'two', '.', 'three', '/']
25
+ Usher::Splitter.new(['.', '/']).split('one/two.three/').should == ['one', '/', 'two', '.', 'three', '/']
26
26
  end
27
27
  end
28
28
 
29
29
  describe "when delimiters are consecutive" do
30
30
  it "should split correctly" do
31
- Usher::Splitter.for_delimiters(['/', '!']).split('/cheese/!parmesan').should == ['/', 'cheese', '/', '!', 'parmesan']
31
+ Usher::Splitter.new(['/', '!']).split('/cheese/!parmesan').should == ['/', 'cheese', '/', '!', 'parmesan']
32
32
  end
33
33
  end
34
34
 
35
35
  describe "when delimiters contain escaped characters" do
36
36
  it "should split correctly" do
37
- Usher::Splitter.for_delimiters(['/', '\(', '\)']).split('/cheese(parmesan)').should == ['/', 'cheese', '(', 'parmesan', ')']
37
+ Usher::Splitter.new(['/', '\(', '\)']).split('/cheese(parmesan)').should == ['/', 'cheese', '(', 'parmesan', ')']
38
38
  end
39
39
  end
40
40
  end
data/usher.gemspec CHANGED
@@ -5,7 +5,7 @@ require "base64"
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "usher"
8
- s.version = "0.7.5"
8
+ s.version = "0.8.0"
9
9
  s.authors = ["Daniel Neighman", "Daniel Vartanov", "Jakub Šťastný", "Joshua Hull", "Davide D'Agostino"].sort
10
10
  s.homepage = "http://github.com/joshbuddy/usher"
11
11
  s.summary = "Pure ruby general purpose router with interfaces for rails, rack, email or choose your own adventure"
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 7
8
- - 5
9
- version: 0.7.5
7
+ - 8
8
+ - 0
9
+ version: 0.8.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Daniel Neighman
@@ -19,7 +19,7 @@ authors:
19
19
  autorequire:
20
20
  bindir: bin
21
21
  cert_chain:
22
- date: 2010-05-02 00:00:00 -04:00
22
+ date: 2010-05-05 00:00:00 -04:00
23
23
  default_executable:
24
24
  dependencies:
25
25
  - !ruby/object:Gem::Dependency
@@ -121,6 +121,7 @@ files:
121
121
  - lib/usher/interface/sinatra.rb
122
122
  - lib/usher/interface/text.rb
123
123
  - lib/usher/node.rb
124
+ - lib/usher/node/failed_response.rb
124
125
  - lib/usher/node/response.rb
125
126
  - lib/usher/node/root.rb
126
127
  - lib/usher/node/root_ignoring_trailing_delimiters.rb