pendragon 0.3.0 → 0.4.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.
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.