pendragon 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 47c14505bf8ecd94fbdfff0f4bd88ef93ae869d6
4
- data.tar.gz: db2b7552746a423ce69adf38a727d1fe3a963c0c
3
+ metadata.gz: 34a16924c1d2032821eab7f8e670a9823cfe9e6e
4
+ data.tar.gz: f28901a7ae7bb3dd844b31fd09e65140ab40f700
5
5
  SHA512:
6
- metadata.gz: c65a69814973dec2ff98523b6a090ad7e552403e349789ce5af6e28014b4d98d3d45c36498f9836887e6e143884e6539f6f45dc71b6be5021776f06aba3b9337
7
- data.tar.gz: 62145cf3f8f5e16c26cbc56412be3bb29da00562abc3e10413da142365e7fcfb3c3c463c4677acd0fb0c4d1e65f9caa15c4a3820c60990f57dde24219f61695f
6
+ metadata.gz: 69305896d92e425a2901eb8670b644a55d1a22ddab91c33058889fcf7dae1bdcf6c288e0831cc98967cbe65838f182047e825f67cee641b631a39f92c74cf941
7
+ data.tar.gz: 6be88a0862cdcabe0206bdf3612869b4eb13f890478c4d16336bd2da4cf21c1132f8c561e0bc865d5ec8518b0cc2107d89f498cbc94ecf384bf47d28c8981d9a
data/.travis.yml CHANGED
@@ -5,9 +5,16 @@ install:
5
5
  - bundle update
6
6
  rvm:
7
7
  - 2.0.0
8
+ - 2.1.0
9
+ - jruby-head
10
+ - rbx
8
11
  notifications:
9
12
  recipients:
10
13
  - namusyaka@gmail.com
11
14
  branches:
12
15
  only:
13
16
  - master
17
+ matrix:
18
+ allow_failures:
19
+ - rvm: rbx
20
+ - rvm: jruby-head
data/Gemfile.lock CHANGED
@@ -8,7 +8,7 @@ PATH
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (4.0.2)
11
+ activesupport (4.0.3)
12
12
  i18n (~> 0.6, >= 0.6.4)
13
13
  minitest (~> 4.2)
14
14
  multi_json (~> 1.3)
@@ -17,11 +17,11 @@ GEM
17
17
  atomic (1.1.14)
18
18
  haml (4.0.5)
19
19
  tilt
20
- http_router (0.11.0)
20
+ http_router (0.11.1)
21
21
  rack (>= 1.0.0)
22
22
  url_mount (~> 0.2.1)
23
23
  i18n (0.6.9)
24
- metaclass (0.0.2)
24
+ metaclass (0.0.4)
25
25
  minitest (4.7.5)
26
26
  mocha (1.0.0)
27
27
  metaclass (~> 0.0.1)
data/README.md CHANGED
@@ -1,136 +1,236 @@
1
1
  # Pendragon
2
2
 
3
- [![Build Status](https://travis-ci.org/namusyaka/pendragon.png)](https://travis-ci.org/namusyaka/pendragon)
3
+ [![Build Status](https://travis-ci.org/namusyaka/pendragon.png)](https://travis-ci.org/namusyaka/pendragon) [![Gem Version](https://badge.fury.io/rb/pendragon.png)](http://badge.fury.io/rb/pendragon)
4
4
 
5
- Provides an HTTP router for use in Rack and Padrino.
5
+ Pendragon provides an HTTP router for use in Rack and Padrino.
6
+ As a Rack application, it makes it easy to define complicated routing.
7
+ As a Padrino plugin, your application uses Pendragon instead of http_router.
8
+ Therefore, some bugs of http_router will be fixed.
6
9
 
7
- Pendragon works only in Ruby2.0.
10
+ *If you want to use in Ruby1.9, you can do it by using [mustermann/1.9-support branch](https://github.com/rkh/mustermann/tree/1.9-support).*
8
11
 
9
- If you want to use in Ruby1.9, you can do it by using [mustermann/1.9-support branch](https://github.com/rkh/mustermann/tree/1.9-support).
12
+ ```ruby
13
+ Pendragon.new do
14
+ get("/"){ "hello world" }
15
+ get("/foo/:bar", name: :foo) do |params|
16
+ "foo is #{params[:bar]}"
17
+ end
18
+ end
19
+ ```
10
20
 
11
21
  ## Installation
12
22
 
13
- add this line to your Gemfile.
23
+ Depends on [rack](https://github.com/rack/rack) and [mustermann](https://github.com/rkh/mustermann).
14
24
 
15
- `gem 'pendragon'`
25
+ `gem install pendragon`
16
26
 
17
- or
27
+ ## Usage
18
28
 
19
- `$ gem install pendragon`
29
+ ### Configuration
20
30
 
21
- ## Configuration
31
+ |name |types |default|description |
32
+ |:---------------|:------|:------|:-------------------------|
33
+ |enable_compiler |boolean|false|The performance will be improved. However, it will increase the first load time.|
34
+ |auto_rack_format|boolean|true|If disable this param, the block of route should return the response of valid rack format.|
22
35
 
23
- If you enable compiler, performance will be improved at the expense of some features as below.
24
-
25
- * Route priority will not work (Might support in the future).
26
- * Duplicated routes will not work correctly.
27
- * MethodNotAllowed will not work.
28
-
29
- This implementation was inspired by [rack-multiplexer](https://github.com/r7kamura/rack-multiplexer).
36
+ #### `enable_compiler`
30
37
 
31
38
  ```ruby
39
+ # Performance will be improved!
32
40
  Pendragon.configure do |config|
33
- config.enable_compiler = true # default value is false
41
+ config.enable_compiler = true
34
42
  end
35
43
  ```
36
44
 
37
- ## Example
45
+ *The compiler mode was inspired by [rack-multiplexer](https://github.com/r7kamura/rack-multiplexer). Thank you!*
38
46
 
39
- Write this code to your config.ru.
47
+ #### `auto_rack_format`
40
48
 
41
49
  ```ruby
42
- require 'pendragon'
50
+ # Enable the param (default)
51
+ Pendragon.new do
52
+ get("/"){ "hey" }
53
+ end
43
54
 
44
- pendragon = Pendragon.new
45
- pendragon.add(:get, "/") do
46
- "get"
55
+ Pendragon.configure do |config|
56
+ config.auto_rack_format = false
57
+ end
58
+ # Disable the param
59
+ Pendragon.new do
60
+ get("/"){ [200, {"Content-Type" => "text/html;charset=utf-8"}, ["hey"]] }
47
61
  end
62
+ ```
63
+
64
+ ### Register the route
65
+
66
+ It has some methods to register a route. For example, `#get`, `#post` and `#delete` are so.
67
+ This section introduces all those methods.
48
68
 
49
- pendragon.get("/hey") do
50
- "hey"
69
+ #### `add(verb, path, option, &block)`
70
+
71
+ The method is the basis of the registration method of all.
72
+ In comparison with other registration methods, one argument is increased.
73
+
74
+ ```ruby
75
+ Pendragon.new do
76
+ # The two approach have the same meaning.
77
+ add(:get, "/"){ "hello world" }
78
+ get("/"){ "hello world" }
51
79
  end
80
+ ```
81
+
82
+ #### `get(path, option, &block)`, `post`, `delete`, `put` and `head`
83
+
84
+ Basically the usage is the same with `#add`.
85
+ You may as well use those methods instead of `#add` because those methods are easy to understand.
52
86
 
53
- pendragon.post("/hey") do
54
- "hey, postman!"
87
+ ```ruby
88
+ Pendragon.new do
89
+ get("/"){ "hello world" }
90
+ post("/"){ "hello world" }
91
+ delete("/"){ "hello world" }
92
+ put("/"){ "hello world" }
93
+ head("/"){ "hello world" }
55
94
  end
95
+ ```
96
+
97
+ ##### Path
98
+
99
+ The path must be an instance of String (this must be complied with the Mustermann::Sinatra's rule) or Regexp.
100
+
101
+ ##### Route options
102
+
103
+ |name|types |description |
104
+ |:----|:------|:-------------------------|
105
+ |name |symbol |specify the name of route for `Pendragon::Router#path` method.|
106
+ |order|integer|specify the order for the prioritized routes.|
107
+ |capture|hash|specify the capture for matching condition. [more information here](https://github.com/rkh/mustermann)|
108
+ |status|integer|specify the status code of response|
109
+ |header|hash|specify the header of response|
56
110
 
111
+ ##### Block Parameters
57
112
 
58
- pendragon.get("/users/:user_id") do |params|
59
- params.inspect
113
+ The block is allowed to pass a parameter.
114
+ It will be an instance of Hash.
115
+
116
+ ```ruby
117
+ pendragon = Pendragon.new do
118
+ get("/:id/:foo/:bar"){|params| params.inspect }
60
119
  end
61
120
 
62
- run pendragon
121
+ request = Rack::MockRequest.env_for("/123/hey/ho")
122
+ pendragon.recognize(request).first.call #=> '{id: "123", foo: "hey", bar: "ho"}'
63
123
  ```
64
124
 
65
- ## Normal path
125
+ ### Recognize the route
126
+
127
+ The route registered can be recognized by several methods.
66
128
 
67
- ### Base
129
+ #### `recognize(request)`
130
+
131
+ This method returns all the routes that match the conditions.
132
+ The format of returns will be such as `[[Pendragon::Route, params], ...]`.
133
+ The request must be an instance of `Rack::Request` or Hash created by `Rack::MockRequest.env_for`.
68
134
 
69
135
  ```ruby
70
136
  pendragon = Pendragon.new
137
+ index = pendragon.get("/"){ "hello world" }
138
+ foo = pendragon.get("/foo/:bar"){ "foo is bar" }
71
139
 
72
- pendragon.add(:get, "/") do
73
- "hello"
74
- end
140
+ mock_request = Rack::MockRequest.env_for("/")
141
+ route, params = pendragon.recognize(mock_request).first
142
+
143
+ route.path == index.path #=> true
144
+ params #=> {}
145
+
146
+ mock_request = Rack::MockRequest.env_for("/foo/baz")
147
+ route, params = pendragon.recognize(mock_request).first
148
+
149
+ route.path == foo.path #=> true
150
+ params #=> {bar: "baz"}
75
151
  ```
76
152
 
77
- ### Regexp
153
+ #### `recognize_path(path_info)`
78
154
 
79
- ```ruby
80
- pendragon = Pendragon.new
155
+ Recognizes a route from `path_info`.
156
+ The method uses `#recognize`, but return value is not same with it.
157
+ Maybe this is useful if you set the name to the route.
81
158
 
82
- pendragon.add(:get, /(\d+)/) do
83
- "hello"
159
+ ```ruby
160
+ pendragon = Pendragon.new do
161
+ get("/", name: :index){ "hello world" }
162
+ get("/:id", name: :foo){ "fooooo" }
84
163
  end
164
+
165
+ pendragon.recognize_path("/") #=> [:index, {}]
166
+ pendragon.recognize_path("/hey") #=> [:foo, {id: "hey"}]
85
167
  ```
86
168
 
87
- ### Params
169
+ #### `path(name, *args)`
88
170
 
89
- ```ruby
90
- pendragon = Pendragon.new
171
+ Recognizes a route from route's name, and expands the path from parameters.
172
+ If you pass a name that does not exist, Pendragon raises `InvalidRouteException`.
173
+ The parameters that is not required to expand will be treated as query.
91
174
 
92
- pendragon.add(:get, "/users/:name") do |params|
93
- "hello #{params[:name]}"
175
+ ```ruby
176
+ pendragon = Pendragon.new do
177
+ get("/", name: :index){ "hello world" }
178
+ get("/:id", name: :foo){ "fooooo" }
94
179
  end
95
180
 
96
- pendragon.add(:get, /\/page\/(.+?)/) do |params|
97
- "show #{params[:captures]}"
98
- end
181
+ pendragon.path(:index) #=> "/"
182
+ pendragon.path(:foo, id: "123") #=> "/123"
183
+ pendragon.path(:foo, id: "123", bar: "hey") #=> "/123?bar=hey"
99
184
  ```
100
185
 
101
- ### Captures
186
+ ### Prioritized Routes
102
187
 
103
- ```ruby
104
- pendragon = Pendragon.new
188
+ Pendragon supports for respecting route order.
189
+ If you want to use this, you should pass the `:order` option to the registration method.
105
190
 
106
- users = pendragon.add(:get, "/users/:name") do |params|
107
- "hello #{params[:name]}"
191
+ ```ruby
192
+ pendragon = Pendragon.new do
193
+ get("/", order: 1){ "two" }
194
+ get("/", order: 0){ "one" }
195
+ get("/", order: 2){ "three" }
108
196
  end
109
- users.captures[:name] = /\d+/
197
+
198
+ request = Rack::MockRequest.env_for("/")
199
+ pendragon.recognize(request).map{|route, _| route.call } #=> ["one", "two", "three"]
110
200
  ```
111
201
 
112
- ### Name and Path
202
+ ### Passing
113
203
 
114
- ```ruby
115
- pendragon = Pendragon.new
204
+ A route can punt processing to the next matching route using `throw :pass`
116
205
 
117
- users = pendragon.add(:get, "/users/:name") do |params|
118
- "hello #{params[:name]}"
206
+ ```ruby
207
+ pendragon = Pendragon.new do
208
+ foo = nil
209
+ get("/"){ foo = "yay"; throw :pass }
210
+ get("/"){ foo }
119
211
  end
120
- users.name = :users
121
212
 
122
- pendragon.path(:users, :name => "howl") #=> "/users/howl"
213
+ request = Rack::MockRequest.env_for("/")
214
+ pendragon.call(request) #=> [200, {"Content-Type"=>"text/html;charset=utf-8"}, ["yay"]]
123
215
  ```
124
216
 
125
- ## with Padrino
217
+ ### With Padrino
218
+
219
+ Add `register Pendragon::Padrino` to your padrino application.
220
+ Of course, Pendragon has compatibility with Padrino Routing.
126
221
 
127
- If you use Pendragon, your application does not use http_router.
128
222
 
129
223
  ```ruby
130
224
  require 'pendragon/padrino'
131
225
 
132
226
  class App < Padrino::Application
133
227
  register Pendragon::Padrino
228
+
229
+ ##
230
+ # Also, your app's performance will be improved by using compiler mode.
231
+ # Pendragon.configure do |config|
232
+ # config.enable_compiler = true
233
+ # end
134
234
 
135
235
  get :index do
136
236
  "hello pendragon!"
data/Rakefile CHANGED
@@ -15,5 +15,11 @@ Rake::TestTask.new(:test_with_compiler) do |test|
15
15
  test.verbose = true
16
16
  end
17
17
 
18
- task :test => [:test_without_compiler, :test_with_compiler]
18
+ Rake::TestTask.new(:configuration) do |test|
19
+ test.libs << 'test'
20
+ test.test_files = Dir['test/**/*_configuration.rb']
21
+ test.verbose = true
22
+ end
23
+
24
+ task :test => [:test_without_compiler, :test_with_compiler, :configuration]
19
25
  task :default => :test
@@ -2,40 +2,47 @@
2
2
  module Pendragon
3
3
  module CompileHelpers
4
4
  def compile!
5
- @compiled_regexps = Pendragon::HTTP_VERBS.inject({}){|all, verb| all[verb] = []; all }
6
- @routes.each_with_index do |route, index|
5
+ return if compiled?
6
+ @regexps = @routes.map.with_index do |route, index|
7
7
  regexp = route.matcher.handler
8
8
  regexp = regexp.to_regexp if route.matcher.mustermann?
9
- @compiled_regexps[route.verb] << /(?<_#{index}>#{regexp})/
10
- route.index = "_#{index}"
9
+ route.index = index
10
+ /(?<_#{index}>#{regexp})/
11
11
  end
12
- @compiled_regexps.each_pair{|verb, regexps| @compiled_regexps[verb] = /\A#{Regexp.union(regexps)}\Z/ }
12
+ @regexps = compile(@regexps)
13
+ end
14
+
15
+ def compile(regexps, paths = [])
16
+ return paths if regexps.length.zero?
17
+ paths << Regexp.union(regexps)
18
+ regexps.shift
19
+ compile(regexps, paths)
13
20
  end
14
21
 
15
22
  def compiled?
16
- !!@compiled_regexps
23
+ !!@regexps
17
24
  end
18
25
 
19
26
  def recognize_by_compiling_regexp(request)
20
- path_info, verb, request_params = parse_request(request)
21
-
22
- unless @compiled_regexps[verb] === path_info
23
- old_path_info = path_info
24
- path_info = path_info[0..-2] if path_info != "/" and path_info[-1] == "/"
25
- raise NotFound if old_path_info == path_info || !(@compiled_regexps[verb] === path_info)
26
- end
27
+ prepare! unless prepared?
28
+ pattern, verb, params = parse_request(request)
29
+ candidacies = match_with(pattern)
30
+ raise_exception(404) if candidacies.empty?
31
+ candidacies, allows = candidacies.partition{|route| route.verb == verb }
32
+ raise_exception(405, verbs: allows.map(&:verb)) if candidacies.empty?
33
+ candidacies.map{|route| [route, params_for(route, pattern, params)]}
34
+ end
27
35
 
28
- route = @routes.select{|route| route.verb == verb }.detect{|route| Regexp.last_match(route.index) }
29
- params, match_data = {}, route.match(path_info)
30
- if match_data.names.empty?
31
- params[:captures] = match_data.captures
32
- else
33
- params.merge!(match_data.names.inject({}){|result, name|
34
- result[name.to_sym] = match_data[name] ? Rack::Utils.unescape(match_data[name]) : nil
35
- result
36
- }).merge!(request_params){|key, self_val, new_val| self_val || new_val }
36
+ def match_with(pattern)
37
+ offset = 0
38
+ conditions = [pattern]
39
+ conditions << pattern[0..-2] if pattern != "/" && pattern.end_with?("/")
40
+ loop.with_object([]) do |_, candidacies|
41
+ return candidacies unless conditions.any?{|x| @regexps[offset] === x }
42
+ route = @routes[offset..-1].detect{|route| Regexp.last_match("_#{route.index}") }
43
+ candidacies << route
44
+ offset = route.index + 1
37
45
  end
38
- [[route, params]]
39
46
  end
40
47
  end
41
48
  end
@@ -1,25 +1,33 @@
1
1
  module Pendragon
2
2
  class Configuration
3
3
 
4
+ # Define the accessor as boolean method
5
+ def self.attr_boolean_accessor(*keys)
6
+ keys.each do |key|
7
+ attr_accessor key
8
+ define_method("#{key}?"){ !!__send__(key) }
9
+ end
10
+ end
11
+
4
12
  # Enables to compile the routes
5
13
  # Improve the performance by using this option,
6
14
  # but some features will not work correctly.
7
15
  # @see Pendragon::Router#compile
8
- attr_accessor :enable_compiler
16
+ attr_boolean_accessor :enable_compiler
17
+
18
+ # Automatically convert response into Rack format.
19
+ # Default value is `true`.
20
+ attr_boolean_accessor :auto_rack_format
9
21
 
10
22
  # Constructs an instance of Pendragon::Configuration
11
23
  def initialize
12
- @enable_compiler = false
24
+ @enable_compiler = false
25
+ @auto_rack_format = true
13
26
  end
14
27
 
15
28
  # Returns an instance variable
16
29
  def [](variable_name)
17
30
  instance_variable_get("@#{variable_name}")
18
31
  end
19
-
20
- # Returns a boolean of @enable_compiler
21
- def enable_compiler?
22
- !!@enable_compiler
23
- end
24
32
  end
25
33
  end
@@ -34,6 +34,10 @@ module Pendragon
34
34
  class MethodNotAllowed < ErrorHandler
35
35
  set :status, 405
36
36
  set :body, "Method Not Allowed"
37
+
38
+ def initialize(verbs)
39
+ default_response[1].merge!("Allow" => verbs.map{|verb| verb.upcase } * ", ")
40
+ end
37
41
  end
38
42
 
39
43
  class BadRequest < ErrorHandler
@@ -1,4 +1,4 @@
1
- require 'mustermann'
1
+ require 'mustermann/sinatra'
2
2
 
3
3
  module Pendragon
4
4
  class Matcher
@@ -22,7 +22,7 @@ module Pendragon
22
22
  # @return [Nil] If the pattern doesn't matched this route, return a nil.
23
23
  #
24
24
  def match(pattern)
25
- pattern = pattern[0..-2] if mustermann? and pattern != "/" and pattern[-1] == "/"
25
+ pattern = pattern[0..-2] if mustermann? and pattern != "/" and pattern.end_with?("/")
26
26
  handler.match(pattern)
27
27
  end
28
28
 
@@ -59,7 +59,7 @@ module Pendragon
59
59
  @handler ||=
60
60
  case @path
61
61
  when String
62
- Mustermann.new(@path, :capture => @capture)
62
+ Mustermann::Sinatra.new(@path, :capture => @capture)
63
63
  when Regexp
64
64
  /^(?:#{@path})$/
65
65
  end
@@ -13,32 +13,16 @@ module Pendragon
13
13
 
14
14
  def call(env)
15
15
  request = Rack::Request.new(env)
16
- raise BadRequest unless valid_verb?(request.request_method)
17
- prepare! unless prepared?
16
+ raise_exception(400) unless valid_verb?(request.request_method)
18
17
  [200, {}, recognize(request)]
19
18
  rescue BadRequest, NotFound, MethodNotAllowed
20
19
  $!.call
21
20
  end
22
21
 
23
22
  def path(name, *args)
24
- params = args.delete_at(args.last.is_a?(Hash) ? -1 : 0) || {}
25
- saved_args = args.dup
26
- @routes.each do |route|
27
- next unless route.options[:name] == name
28
- matcher = route.matcher
29
- if !args.empty? and matcher.mustermann?
30
- matcher_names = matcher.names
31
- params_for_expand = Hash[matcher_names.map{|matcher_name|
32
- [matcher_name.to_sym, (params[matcher_name.to_sym] || args.shift)]
33
- }]
34
- params_for_expand.merge!(Hash[params.select{|k, v| !matcher_names.include?(name.to_sym) }])
35
- args = saved_args.dup
36
- else
37
- params_for_expand = params.dup
38
- end
39
- return matcher.mustermann? ? matcher.expand(params_for_expand) : route.path_for_generation
23
+ extract_with_name(name, *args) do |route, params, matcher|
24
+ matcher.mustermann? ? matcher.expand(params) : route.path_for_generation
40
25
  end
41
- raise InvalidRouteException
42
26
  end
43
27
  end
44
28
  end
@@ -1,21 +1,25 @@
1
1
  module Pendragon
2
2
  class Route
3
3
 
4
- ##
5
4
  # The accessors are useful to access from Pendragon::Router
6
- attr_accessor :block, :capture, :router, :options, :verb, :order
5
+ attr_accessor :name, :capture, :order, :options
7
6
 
8
- ##
9
7
  # For compile option
10
8
  attr_accessor :index
11
9
 
12
- ##
10
+ # The verb should be read from Pendragon::Router
11
+ attr_reader :verb, :block
12
+
13
+ # The router will be treated in this class.
14
+ attr_writer :router
15
+
13
16
  # Constructs a new instance of Pendragon::Route
14
17
  def initialize(path, verb, options = {}, &block)
15
18
  @block = block if block_given?
16
- @path, @verb, @options = path, verb, options
19
+ @path, @verb = path, verb
17
20
  @capture = {}
18
21
  @order = 0
22
+ merge_with_options!(options)
19
23
  end
20
24
 
21
25
  def matcher
@@ -24,7 +28,7 @@ module Pendragon
24
28
  end
25
29
 
26
30
  def arity
27
- block.arity
31
+ @block.arity
28
32
  end
29
33
 
30
34
  def call(*args)
@@ -35,19 +39,10 @@ module Pendragon
35
39
  matcher.match(pattern)
36
40
  end
37
41
 
38
- def name
39
- @options[:name]
40
- end
41
-
42
- def name=(value)
43
- warn "[DEPRECATION] 'name=' is depreacted. Please use 'options[:name]=' instead"
44
- @options[:name] = value
45
- end
46
-
47
42
  def to(&block)
48
43
  @block = block if block_given?
49
- @order = router.current
50
- router.increment_order!
44
+ @order = @router.current
45
+ @router.increment_order!
51
46
  end
52
47
 
53
48
  def path(*args)
@@ -56,5 +51,33 @@ module Pendragon
56
51
  params.delete(:captures)
57
52
  matcher.expand(params) if matcher.mustermann?
58
53
  end
54
+
55
+ def params(pattern, parameters = {})
56
+ match_data, params = match(pattern), {}
57
+ if match_data.names.empty?
58
+ params.merge!(:captures => match_data.captures) unless match_data.captures.empty?
59
+ params
60
+ else
61
+ params = matcher.handler.params(pattern, :captures => match_data) || params
62
+ symbolize(params).merge(parameters){|key, old, new| old || new }
63
+ end
64
+ end
65
+
66
+ def symbolize(parameters)
67
+ parameters.inject({}){|result, (key, val)| result[key.to_sym] = val; result }
68
+ end
69
+
70
+ def merge_with_options!(options)
71
+ @options = {} unless @options
72
+ options.each_pair do |key, value|
73
+ accessor?(key) ? __send__("#{key}=", value) : (@options[key] = value)
74
+ end
75
+ end
76
+
77
+ def accessor?(key)
78
+ respond_to?("#{key}=") && respond_to?(key)
79
+ end
80
+
81
+ private :symbolize, :merge_with_options!, :accessor?
59
82
  end
60
83
  end
@@ -33,15 +33,22 @@ module Pendragon
33
33
  # @return the Rack style response
34
34
  def call(env)
35
35
  request = Rack::Request.new(env)
36
- raise BadRequest unless valid_verb?(request.request_method)
37
- prepare! unless prepared?
38
- route, params = recognize(request).first
39
- body = route.arity != 0 ? route.call(params) : route.call
40
- [200, {'Content-Type' => 'text/html;charset=utf-8'}, Array(body)]
36
+ raise_exception(400) unless valid_verb?(request.request_method)
37
+ recognize(request).each do |route, params|
38
+ catch(:pass){ return invoke(route, params) }
39
+ end
41
40
  rescue BadRequest, NotFound, MethodNotAllowed
42
41
  $!.call
43
42
  end
44
43
 
44
+ def invoke(route, params)
45
+ response = route.arity != 0 ? route.call(params) : route.call
46
+ return response unless Pendragon.configuration.auto_rack_format?
47
+ status = route.options[:status] || 200
48
+ header = {'Content-Type' => 'text/html;charset=utf-8'}.merge(route.options[:header] || {})
49
+ [status, header, Array(response)]
50
+ end
51
+
45
52
  # Provides some methods intuitive than #add
46
53
  # Basic usage is the same as #add
47
54
  # @see Pendragon::Router#add
@@ -70,15 +77,17 @@ module Pendragon
70
77
  # This method is executed only once in the initial load
71
78
  def prepare!
72
79
  @prepared = true
73
- @routes.sort_by!(&:order) unless current.zero?
74
- if Pendragon.configuration.enable_compiler?
75
- class << self
76
- include CompileHelpers
77
- alias_method :old_recognize, :recognize
78
- alias_method :recognize, :recognize_by_compiling_regexp
79
- end
80
- compile!
81
- end
80
+ @routes.sort_by!(&:order)
81
+ self.class.setup_compiler! && compile! if Pendragon.configuration.enable_compiler?
82
+ end
83
+
84
+ # Setups the compiler by using CompileHelpers
85
+ # @see Pendragon::CompileHelpers
86
+ def self.setup_compiler!
87
+ include CompileHelpers
88
+ alias_method :old_recognize, :recognize
89
+ alias_method :recognize, :recognize_by_compiling_regexp
90
+ true
82
91
  end
83
92
 
84
93
  # @return [Boolean] the router is already prepared?
@@ -95,19 +104,9 @@ module Pendragon
95
104
  # @param request [Rack::Request]
96
105
  # @return [Array]
97
106
  def recognize(request)
98
- path_info, verb, request_params = parse_request(request)
99
- scan(path_info, verb) do |route|
100
- params, match_data = {}, route.match(path_info)
101
- if match_data.names.empty?
102
- params[:captures] = match_data.captures
103
- else
104
- params.merge!(match_data.names.inject({}){|result, name|
105
- result[name.to_sym] = match_data[name] ? Rack::Utils.unescape(match_data[name]) : nil
106
- result
107
- }).merge!(request_params){|key, self_val, new_val| self_val || new_val }
108
- end
109
- [route, params]
110
- end
107
+ prepare! unless prepared?
108
+ pattern, verb, params = parse_request(request)
109
+ fetch(pattern, verb){|route| [route, params_for(route, pattern, params)] }
111
110
  end
112
111
 
113
112
  # Recognizes a given path
@@ -115,7 +114,7 @@ module Pendragon
115
114
  # @return [Array]
116
115
  def recognize_path(path_info)
117
116
  route, params = recognize(Rack::MockRequest.env_for(path_info)).first
118
- [route.options[:name], params]
117
+ [route.name, params]
119
118
  end
120
119
 
121
120
  # Returns a expanded path matched with the conditions as arguments
@@ -126,23 +125,9 @@ module Pendragon
126
125
  # router.path(:index, :id => 1) #=> "/1"
127
126
  # router.path(:index, :id => 2, :foo => "bar") #=> "/1?foo=bar"
128
127
  def path(name, *args)
129
- params = args.delete_at(args.last.is_a?(Hash) ? -1 : 0) || {}
130
- saved_args = args.dup
131
- @routes.each do |route|
132
- next unless route.options[:name] == name
133
- matcher = route.matcher
134
- if !args.empty? and matcher.mustermann?
135
- matcher_names = matcher.names
136
- params_for_expand = Hash[matcher_names.map{|matcher_name|
137
- [matcher_name.to_sym, (params[matcher_name.to_sym] || args.shift)]}]
138
- params_for_expand.merge!(Hash[params.select{|k, v| !matcher_names.include?(name.to_sym) }])
139
- args = saved_args.dup
140
- else
141
- params_for_expand = params.dup
142
- end
143
- return matcher.mustermann? ? matcher.expand(params_for_expand) : route.path
128
+ extract_with_name(name, *args) do |route, params, matcher|
129
+ matcher.mustermann? ? matcher.expand(params) : route.path
144
130
  end
145
- raise InvalidRouteException
146
131
  end
147
132
 
148
133
  private
@@ -153,19 +138,12 @@ module Pendragon
153
138
  end
154
139
 
155
140
  # @!visibility private
156
- def scan(pattern, verb)
157
- raise NotFound if (selected_routes = routes.select{|route| route.match(pattern) }).empty?
158
-
159
- result = selected_routes.map do |route|
160
- next unless verb == route.verb
161
- yield route
162
- end.compact
163
-
164
- if result.empty?
165
- raise MethodNotAllowed.new(selected_routes.map(&:verb))
166
- else
167
- result
168
- end
141
+ def fetch(pattern, verb)
142
+ _routes = routes.select{|route| route.match(pattern) }
143
+ raise_exception(404) if _routes.empty?
144
+ result = _routes.map{|route| yield(route) if verb == route.verb }.compact
145
+ raise_exception(405, :verbs => _routes.map(&:verb)) if result.empty?
146
+ result
169
147
  end
170
148
 
171
149
  # @!visibility private
@@ -184,5 +162,49 @@ module Pendragon
184
162
  result
185
163
  end
186
164
  end
165
+
166
+ # @!visibility private
167
+ def params_for(route, pattern, params)
168
+ route.params(pattern, params)
169
+ end
170
+
171
+ # @!visibility private
172
+ # @example
173
+ # extract_with_name(:index) do |route, params|
174
+ # route.matcher.mustermann? ? route.matcher.expand(params) : route.path
175
+ # end
176
+ def extract_with_name(name, *args)
177
+ params = args.delete_at(args.last.is_a?(Hash) ? -1 : 0) || {}
178
+ saved_args = args.dup
179
+ @routes.each do |route|
180
+ next unless route.name == name
181
+ matcher = route.matcher
182
+ if !args.empty? and matcher.mustermann?
183
+ matcher_names = matcher.names
184
+ params_for_expand = Hash[matcher_names.map{|matcher_name|
185
+ [matcher_name.to_sym, (params[matcher_name.to_sym] || args.shift)]}]
186
+ params_for_expand.merge!(Hash[params.select{|k, v| !matcher_names.include?(name.to_sym) }])
187
+ args = saved_args.dup
188
+ else
189
+ params_for_expand = params.dup
190
+ end
191
+ return yield(route, params_for_expand, matcher)
192
+ end
193
+ raise InvalidRouteException
194
+ end
195
+
196
+ # @!visibility private
197
+ def raise_exception(error_code, options = {})
198
+ raise ->(error_code) {
199
+ case error_code
200
+ when 400
201
+ BadRequest
202
+ when 404
203
+ NotFound
204
+ when 405
205
+ MethodNotAllowed.new(options[:verbs])
206
+ end
207
+ }.(error_code)
208
+ end
187
209
  end
188
210
  end
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Pendragon
3
- VERSION = '0.3.0'
3
+ VERSION = '0.4.0'
4
4
  end
data/test/padrino_test.rb CHANGED
@@ -148,7 +148,6 @@ describe "Pendragon::Padrino" do
148
148
  end
149
149
 
150
150
  should "match user agents" do
151
- skip if enable_compiler?
152
151
  app = mock_app do
153
152
  get("/main", :agent => /IE/){ "hello IE" }
154
153
  get("/main"){ "hello" }
@@ -595,7 +594,6 @@ describe "Pendragon::Padrino" do
595
594
  end
596
595
 
597
596
  should '405 on wrong request_method' do
598
- skip if enable_compiler?
599
597
  mock_app do
600
598
  post('/bar'){ "bar" }
601
599
  end
@@ -603,6 +601,15 @@ describe "Pendragon::Padrino" do
603
601
  assert_equal 405, status
604
602
  end
605
603
 
604
+ should "set Allow header when occur 405" do
605
+ mock_app do
606
+ post("/bar"){}
607
+ put("/bar"){}
608
+ end
609
+ get "/bar"
610
+ assert_equal "POST, PUT", response.headers['Allow']
611
+ end
612
+
606
613
  should 'respond to' do
607
614
  mock_app do
608
615
  get(:a, :provides => :js){ "js" }
@@ -847,7 +854,6 @@ describe "Pendragon::Padrino" do
847
854
 
848
855
 
849
856
  should 'respect priorities' do
850
- skip if enable_compiler?
851
857
  route_order = []
852
858
  mock_app do
853
859
  get(:index, :priority => :normal) { route_order << :normal; pass }
@@ -1083,7 +1089,6 @@ describe "Pendragon::Padrino" do
1083
1089
  end
1084
1090
 
1085
1091
  should "transitions to the next matching route on pass" do
1086
- skip if enable_compiler?
1087
1092
  mock_app do
1088
1093
  get '/:foo' do
1089
1094
  pass
@@ -1263,7 +1268,6 @@ describe "Pendragon::Padrino" do
1263
1268
  end
1264
1269
 
1265
1270
  should 'allows custom route-conditions to be set via route options using two routes' do
1266
- skip if enable_compiler?
1267
1271
  protector = Module.new do
1268
1272
  def protect(*args)
1269
1273
  condition { authorize(params["user"], params["password"]) }
@@ -1329,7 +1333,6 @@ describe "Pendragon::Padrino" do
1329
1333
  end
1330
1334
 
1331
1335
  should "allow passing & halting in before filters" do
1332
- skip if enable_compiler?
1333
1336
  mock_app do
1334
1337
  controller do
1335
1338
  before { env['QUERY_STRING'] == 'secret' or pass }
@@ -1813,10 +1816,8 @@ describe "Pendragon::Padrino" do
1813
1816
  post '/hi.json', {'_method'=>'PUT'}
1814
1817
  assert_equal 200, status
1815
1818
  assert_equal 'hi', body
1816
- unless enable_compiler?
1817
- post '/hi.json'
1818
- assert_equal 405, status
1819
- end
1819
+ post '/hi.json'
1820
+ assert_equal 405, status
1820
1821
  end
1821
1822
 
1822
1823
  should 'parse nested params' do
@@ -0,0 +1,31 @@
1
+ require File.expand_path('../../lib/pendragon', __FILE__)
2
+ $:.unshift(File.dirname(__FILE__))
3
+ require 'helper'
4
+
5
+ describe Pendragon::Configuration do
6
+ setup{ @pendragon = pendragon }
7
+ teardown{ Pendragon.reset_configuration! }
8
+
9
+ describe "auto_rack_format" do
10
+ should "set `true` as default value" do
11
+ @pendragon.get("/"){ "hey" }
12
+ get "/"
13
+ assert_equal "hey", body
14
+ assert_equal true, Pendragon.configuration.auto_rack_format?
15
+ end
16
+
17
+ should "not serialize for rack format if `auto_rack_format` is false" do
18
+ Pendragon.configure do |config|
19
+ config.auto_rack_format = false
20
+ end
21
+
22
+ @pendragon.get("/"){ "hey" }
23
+ assert_raises(Rack::Lint::LintError){ get "/" }
24
+
25
+ @pendragon.post("/"){ [200, {'Content-Type' => 'text/html;charset=utf-8'}, ["hey"]] }
26
+ post "/"
27
+ assert_equal "hey", body
28
+ assert_equal false, Pendragon.configuration.auto_rack_format?
29
+ end
30
+ end
31
+ end
@@ -82,7 +82,68 @@ describe Pendragon do
82
82
  head("/")
83
83
  assert_equal "", body
84
84
  end
85
+
86
+ should "support for correct options" do
87
+ named_route = @pendragon.get("/name/:name", name: :named_route){}
88
+ incorrect_route = @pendragon.get("/router", router: :incorrect!){}
89
+ status_route = @pendragon.get("/router", status: 200){}
90
+ assert_equal :named_route, named_route.name
91
+ assert_equal "/name/foo", @pendragon.path(:named_route, name: :foo)
92
+ assert_equal true, incorrect_route.instance_variable_get(:@router).instance_of?(Pendragon::Router)
93
+ end
94
+
95
+ should "allow to throw :pass for routing like journey" do
96
+ foo = nil
97
+ @pendragon.get("/"){ foo = "yay"; throw :pass }
98
+ @pendragon.get("/"){ foo }
99
+ get "/"
100
+ assert_equal "yay", body
101
+ end
85
102
  end
103
+
104
+ describe "route options" do
105
+ should "support for :capture option" do
106
+ capture = {foo: /\d+/, bar: "bar"}
107
+ route = @pendragon.get("/:foo/:bar", capture: capture){}
108
+ assert_equal route.capture, capture
109
+ assert_equal nil, route.match("/foo/bar")
110
+ assert_equal nil, route.match("/123/baz")
111
+ assert_equal true, route.match("/123/bar").instance_of?(MatchData)
112
+ end
113
+
114
+ should "support for :name option" do
115
+ route = @pendragon.get("/name/:name", name: :named_route){}
116
+ assert_equal :named_route, route.name
117
+ assert_equal "/name/foo", @pendragon.path(:named_route, name: :foo)
118
+ end
119
+
120
+ should "not support for :router option" do
121
+ route = @pendragon.get("/router", router: :incorrect!){}
122
+ assert_equal true, route.instance_variable_get(:@router).instance_of?(Pendragon::Router)
123
+ end
124
+
125
+ should "support for :order option" do
126
+ @pendragon.get("/", order: 2){ "three" }
127
+ @pendragon.get("/", order: 0){ "one" }
128
+ @pendragon.get("/", order: 1){ "two" }
129
+ request = Rack::MockRequest.env_for("/")
130
+ assert_equal ["one", "two", "three"], @pendragon.recognize(request).map{|route, _| route.call }
131
+ end
132
+
133
+ should "support for :status option" do
134
+ @pendragon.get("/", status: 201){ "hey" }
135
+ get "/"
136
+ assert_equal 201, status
137
+ end
138
+
139
+ should "support for :header option" do
140
+ header = {"Content-Type" => "text/plain;"}
141
+ @pendragon.get("/", header: header){ "hey" }
142
+ get "/"
143
+ assert_equal header.merge("Content-Length" => "3"), headers
144
+ end
145
+ end
146
+
86
147
  describe "regexp routing" do
87
148
  before(:each){ @pendragon.reset! }
88
149
 
@@ -107,18 +168,18 @@ describe Pendragon do
107
168
  foo_bar = @pendragon.add(:post, "/foo/bar", :name => :foo_bar){}
108
169
  users = @pendragon.add(:get, "/users/:user_id", :name => :users){}
109
170
 
110
- assert_equal @pendragon.path(:index), "/"
111
- assert_equal @pendragon.path(:foo_bar), "/foo/bar"
112
- assert_equal @pendragon.path(:users, :user_id => 1), "/users/1"
113
- assert_equal @pendragon.path(:users, :user_id => 1, :query => "string"), "/users/1?query=string"
171
+ assert_equal "/", @pendragon.path(:index)
172
+ assert_equal "/foo/bar", @pendragon.path(:foo_bar)
173
+ assert_equal "/users/1", @pendragon.path(:users, :user_id => 1)
174
+ assert_equal "/users/1?query=string", @pendragon.path(:users, :user_id => 1, :query => "string")
114
175
  end
115
176
 
116
177
  should "regexp" do
117
178
  index = @pendragon.add(:get, /.+?/, :name => :index){}
118
179
  foo_bar = @pendragon.add(:post, /\d+/, :name => :foo_bar){}
119
180
 
120
- assert_equal @pendragon.path(:index), /.+?/
121
- assert_equal @pendragon.path(:foo_bar), /\d+/
181
+ assert_equal /.+?/, @pendragon.path(:index)
182
+ assert_equal /\d+/, @pendragon.path(:foo_bar)
122
183
  end
123
184
  end
124
185
 
@@ -132,8 +193,17 @@ describe Pendragon do
132
193
  assert_equal "foo", body
133
194
  post("/")
134
195
  assert_equal "bar", body
135
- assert_equal @app.path(:foo), "/"
136
- assert_equal @app.path(:bar), "/"
196
+ assert_equal "/", @app.path(:foo)
197
+ assert_equal "/", @app.path(:bar)
198
+ end
199
+ end
200
+
201
+ describe "header" do
202
+ should "set Allow header when occur 405" do
203
+ @pendragon.get("/"){}
204
+ @pendragon.put("/"){}
205
+ post "/"
206
+ assert_equal "GET, PUT", response.header['Allow']
137
207
  end
138
208
  end
139
209
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pendragon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - namusyaka
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-15 00:00:00.000000000 Z
11
+ date: 2014-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.3.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.3.0
27
27
  - !ruby/object:Gem::Dependency
@@ -42,56 +42,56 @@ dependencies:
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: 0.8.7
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.8.7
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rack-test
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: 0.5.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.5.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: mocha
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: 0.10.0
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.10.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: haml
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '>='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '>='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
@@ -114,7 +114,7 @@ executables: []
114
114
  extensions: []
115
115
  extra_rdoc_files: []
116
116
  files:
117
- - .travis.yml
117
+ - ".travis.yml"
118
118
  - Gemfile
119
119
  - Gemfile.lock
120
120
  - README.md
@@ -137,6 +137,7 @@ files:
137
137
  - test/compile_helper.rb
138
138
  - test/helper.rb
139
139
  - test/padrino_test.rb
140
+ - test/pendragon_configuration.rb
140
141
  - test/pendragon_test.rb
141
142
  homepage: https://github.com/namusyaka/pendragon
142
143
  licenses:
@@ -148,17 +149,17 @@ require_paths:
148
149
  - lib
149
150
  required_ruby_version: !ruby/object:Gem::Requirement
150
151
  requirements:
151
- - - '>='
152
+ - - ">="
152
153
  - !ruby/object:Gem::Version
153
154
  version: '0'
154
155
  required_rubygems_version: !ruby/object:Gem::Requirement
155
156
  requirements:
156
- - - '>='
157
+ - - ">="
157
158
  - !ruby/object:Gem::Version
158
159
  version: '0'
159
160
  requirements: []
160
161
  rubyforge_project:
161
- rubygems_version: 2.0.2
162
+ rubygems_version: 2.2.2
162
163
  signing_key:
163
164
  specification_version: 4
164
165
  summary: Provides an HTTP router for use in Rack and Padrino.