usher 0.7.5 → 0.8.0

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