rack-jet_router 1.0.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +230 -0
- data/Rakefile +57 -0
- data/lib/rack/jet_router.rb +298 -0
- data/rack-jet_router.gemspec +27 -0
- data/test/rack/jet_router_test.rb +618 -0
- data/test/test_helper.rb +10 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7c6751942e65d4b71cede69c49f3af1358a01df8
|
4
|
+
data.tar.gz: 399a8826aa1cd09d46ff602e769095ef1835c1f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 676af23010d1a41ea37db5320356ecfc398792f9b606a1e029f94d1c8cc12b3cab2ec464df9954ac7d5397a779a1e77d5edcf1c2ad3685a906fcb12eb152abff
|
7
|
+
data.tar.gz: ed4ee93454abecf030a50a318d55c2fa6e12eb18631fd962ab42f3944e88bef70bfd6f76ffff21f113b11428cf2881739f878b827d2845f8cd07f2ae95064586
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
$Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# Rack::JetRouter
|
2
|
+
|
3
|
+
Rack::JetRouter is crazy-fast router library for Rack application,
|
4
|
+
derived from [Keight.rb](https://github.com/kwatch/keight/tree/ruby).
|
5
|
+
|
6
|
+
|
7
|
+
## Benchmark
|
8
|
+
|
9
|
+
Benchmark script is [here](https://github.com/kwatch/rack-jet_router/blob/dev/bench/bench.rb).
|
10
|
+
|
11
|
+
```
|
12
|
+
## Ranking real[ns]
|
13
|
+
(Rack plain) /api/hello 1.555 (100.0%) ********************
|
14
|
+
(JetRouter) /api/hello 1.597 ( 97.4%) *******************
|
15
|
+
(JetRouter) /api/hello/123 6.424 ( 24.2%) *****
|
16
|
+
(R::Req+Res) /api/hello 9.837 ( 15.8%) ***
|
17
|
+
(Sinatra) /api/hello 106.965 ( 1.5%)
|
18
|
+
(Sinatra) /api/hello/123 116.672 ( 1.3%)
|
19
|
+
```
|
20
|
+
|
21
|
+
* If URL path has no path parameter (such as `/api/hello`),
|
22
|
+
Rack::JetRouter is just a litte slower than plain Rack application.
|
23
|
+
* If URL path contains path parameter (such as `/api/hello/:id`),
|
24
|
+
Rack::JetRouter becomes slower, but it is enough small (about 6.4ns/req).
|
25
|
+
* Overhead of Rack::JetRouter is smaller than that of Rack::Reqeust +
|
26
|
+
Rack::Response.
|
27
|
+
* Sinatra is too slow.
|
28
|
+
|
29
|
+
|
30
|
+
## Examples
|
31
|
+
|
32
|
+
### #1: Depends only on Request Path
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# -*- coding: utf-8 -*-
|
36
|
+
|
37
|
+
require 'rack'
|
38
|
+
require 'rack/jet_router'
|
39
|
+
|
40
|
+
## Assume that welcome_app, books_api, ... are Rack application.
|
41
|
+
urlpath_mapping = [
|
42
|
+
['/' , welcome_app],
|
43
|
+
['/api', [
|
44
|
+
['/books', [
|
45
|
+
['' , books_api],
|
46
|
+
['/:id(.:format)' , book_api],
|
47
|
+
['/:book_id/comments/:comment_id', comment_api],
|
48
|
+
]],
|
49
|
+
]],
|
50
|
+
['/admin', [
|
51
|
+
['/books' , admin_books_app],
|
52
|
+
]],
|
53
|
+
]
|
54
|
+
|
55
|
+
router = Rack::JetRouter.new(urlpath_mapping)
|
56
|
+
p router.find('/api/books/123.html')
|
57
|
+
#=> [book_api, {"id"=>"123", "format"=>"html"}]
|
58
|
+
|
59
|
+
status, headers, body = router.call(env)
|
60
|
+
```
|
61
|
+
|
62
|
+
|
63
|
+
### #2: Depends on both Request Path and Method
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# -*- coding: utf-8 -*-
|
67
|
+
|
68
|
+
require 'rack'
|
69
|
+
require 'rack/jet_router'
|
70
|
+
|
71
|
+
## Assume that welcome_app, book_list_api, ... are Rack application.
|
72
|
+
urlpath_mapping = [
|
73
|
+
['/' , {GET: welcome_app}],
|
74
|
+
['/api', [
|
75
|
+
['/books', [
|
76
|
+
['' , {GET: book_list_api, POST: book_create_api}],
|
77
|
+
['/:id(.:format)' , {GET: book_show_api, PUT: book_update_api}],
|
78
|
+
['/:book_id/comments/:comment_id', {POST: comment_create_api}],
|
79
|
+
]],
|
80
|
+
]],
|
81
|
+
['/admin', [
|
82
|
+
['/books' , {ANY: admin_books_app}],
|
83
|
+
]],
|
84
|
+
]
|
85
|
+
|
86
|
+
router = Rack::JetRouter.new(urlpath_mapping)
|
87
|
+
p router.find('/api/books/123')
|
88
|
+
#=> [{"GET"=>book_show_api, "PUT"=>book_update_api}, {"id"=>"123", "format"=>nil}]
|
89
|
+
|
90
|
+
status, headers, body = router.call(env)
|
91
|
+
```
|
92
|
+
|
93
|
+
Notice that `{GET: ..., PUT: ...}` is converted into `{"GET"=>..., "PUT"=>...}`
|
94
|
+
automatically when passing to `Rack::JetRouter.new()`.
|
95
|
+
|
96
|
+
|
97
|
+
### #3: RESTful Framework
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# -*- coding: utf-8 -*-
|
101
|
+
|
102
|
+
require 'rack'
|
103
|
+
require 'rack/jet_router'
|
104
|
+
|
105
|
+
class API
|
106
|
+
def initialize(request, response)
|
107
|
+
@request = request
|
108
|
+
@response = response
|
109
|
+
end
|
110
|
+
attr_reader :request, :response
|
111
|
+
end
|
112
|
+
|
113
|
+
class BooksAPI < API
|
114
|
+
def index(); ....; end
|
115
|
+
def create(); ....; end
|
116
|
+
def show(id); ....; end
|
117
|
+
def update(id: nil); ....; end
|
118
|
+
def delete(id: nil); ....; end
|
119
|
+
end
|
120
|
+
|
121
|
+
urlpath_mapping = [
|
122
|
+
['/api', [
|
123
|
+
['/books', [
|
124
|
+
['' , {GET: [BooksAPI, :index],
|
125
|
+
POST: [BooksAPI, :create]}],
|
126
|
+
['/:id' , {GET: [BooksAPI, :show],
|
127
|
+
PUT: [BooksAPI, :update],
|
128
|
+
DELETE: [BooksAPI, :delete]}],
|
129
|
+
]],
|
130
|
+
]],
|
131
|
+
]
|
132
|
+
router = Rack::JetRouter.new(urlpath_mapping)
|
133
|
+
p router.find('/api/books/123')
|
134
|
+
#=> [{"GET"=>[BooksAPI, :show], "PUT"=>..., "DELETE"=>...}, {"id"=>"123"}]
|
135
|
+
|
136
|
+
dict, args = router.find('/api/books/123')
|
137
|
+
klass, action = dict["GET"]
|
138
|
+
handler = klass.new(Rack::Request.new(env), Rack::Response.new)
|
139
|
+
handler.__send__(action, args)
|
140
|
+
```
|
141
|
+
|
142
|
+
|
143
|
+
## Topics
|
144
|
+
|
145
|
+
|
146
|
+
### URL Path Parameters
|
147
|
+
|
148
|
+
URL path parameters (such as `{"id"=>"123"}`) is available via
|
149
|
+
`env['rack.urlpath_params']`.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
BookApp = proc {|env|
|
153
|
+
p env['rack.urlpath_params'] #=> {"id"=>"123"}
|
154
|
+
[200, {}, []]
|
155
|
+
}
|
156
|
+
```
|
157
|
+
|
158
|
+
If you want to tweak URL path parameters, define subclass of Rack::JetRouter
|
159
|
+
and override `#build_urlpath_parameter_vars(env, vars)`.
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class MyRouter < JetRouter
|
163
|
+
|
164
|
+
def build_urlpath_parameter_vars(names, values)
|
165
|
+
return names.zip(values).each_with_object({}) {|(k, v), d|
|
166
|
+
## converts urlpath pavam value into integer
|
167
|
+
v = v.to_i if k == 'id' || k.end_with?('_id')
|
168
|
+
d[k] = v
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
|
176
|
+
### Variable URL Path Cache
|
177
|
+
|
178
|
+
It is possible to classify URL path patterns into two types: fixed and variable.
|
179
|
+
|
180
|
+
* **Fixed URL path pattern** doesn't contain any urlpath paramters.<br>
|
181
|
+
Example: `/`, `/login`, `/api/books`
|
182
|
+
* **Variable URL path pattern** contains urlpath parameters.<br>
|
183
|
+
Example: `/api/books/:id`, `/index(.:format)`
|
184
|
+
|
185
|
+
`Rack::JetRouter` caches only fixed URL path patterns in default.
|
186
|
+
It is possible for `Rack::JetRouter` to cache variable URL path patterns
|
187
|
+
as well as fixed ones. It will make routing much faster.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
## Enable variable urlpath cache.
|
191
|
+
router = Rack::JetRouter.new(urlpath_mapping, urlpath_cache_size: 200)
|
192
|
+
p router.find('/api/books/123') # caches even varaible urlpath
|
193
|
+
```
|
194
|
+
|
195
|
+
|
196
|
+
### Custom Error Response
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
class MyRouter < Rack::JetRouter
|
200
|
+
|
201
|
+
def error_not_found(env)
|
202
|
+
html = ("<h2>404 Not Found</h2>\n" \
|
203
|
+
"<p>Path: #{env['PATH_INFO']}</p>\n")
|
204
|
+
[404, {"Content-Type"=>"text/html"}, [html]]
|
205
|
+
end
|
206
|
+
|
207
|
+
def error_not_allowed(env)
|
208
|
+
html = ("<h2>405 Method Not Allowed</h2>\n" \
|
209
|
+
"<p>Method: #{env['REQUEST_METHOD']}</p>\n")
|
210
|
+
[405, {"Content-Type"=>"text/html"}, [html]]
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
Above methods are invoked from `Rack::JetRouter#call()`.
|
217
|
+
|
218
|
+
|
219
|
+
## Copyright and License
|
220
|
+
|
221
|
+
$Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
|
222
|
+
|
223
|
+
$License: MIT License $
|
224
|
+
|
225
|
+
|
226
|
+
## History
|
227
|
+
|
228
|
+
### 2015-12-06: Release 0.1.0
|
229
|
+
|
230
|
+
* First release
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :test
|
11
|
+
|
12
|
+
|
13
|
+
desc "show how to release"
|
14
|
+
task :help do
|
15
|
+
puts <<END
|
16
|
+
How to release:
|
17
|
+
|
18
|
+
$ git checkout dev
|
19
|
+
$ git diff
|
20
|
+
$ which ruby
|
21
|
+
$ rake test # for confirmation
|
22
|
+
$ git checkout -b rel-1.0 # or git checkout rel-1.0
|
23
|
+
$ rake edit rel=1.0.0
|
24
|
+
$ git diff
|
25
|
+
$ git commit -a -m "release preparation for 1.0.0"
|
26
|
+
$ rake build # for confirmation
|
27
|
+
$ rake install # for confirmation
|
28
|
+
$ #rake release
|
29
|
+
$ gem push pkg/rake-jet_router-1.0.0.gem
|
30
|
+
$ git tag v1.0.0
|
31
|
+
$ git push -u origin rel-1.0
|
32
|
+
$ git push --tags
|
33
|
+
END
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
desc "edit files (for release preparation)"
|
39
|
+
task :edit do
|
40
|
+
rel = ENV['rel'] or
|
41
|
+
raise "ERROR: 'rel' environment variable expected."
|
42
|
+
filenames = Dir[*%w[lib/**/*.rb test/**/*_test.rb test/test_helper.rb *.gemspec]]
|
43
|
+
filenames.each do |fname|
|
44
|
+
File.open(fname, 'r+', encoding: 'utf-8') do |f|
|
45
|
+
content = f.read()
|
46
|
+
x = content.gsub!(/\$Release:.*?\$/, "$Release: #{rel} $")
|
47
|
+
if x.nil?
|
48
|
+
puts "[_] #{fname}"
|
49
|
+
else
|
50
|
+
puts "[C] #{fname}"
|
51
|
+
f.rewind()
|
52
|
+
f.truncate(0)
|
53
|
+
f.write(content)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
###
|
4
|
+
### $Release: 1.0.0 $
|
5
|
+
### $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $
|
6
|
+
### $License: MIT License $
|
7
|
+
###
|
8
|
+
|
9
|
+
require 'rack'
|
10
|
+
|
11
|
+
|
12
|
+
module Rack
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
## Jet-speed router class, derived from Keight.rb.
|
17
|
+
##
|
18
|
+
## ex:
|
19
|
+
## urlpath_mapping = [
|
20
|
+
## ['/' , welcome_app],
|
21
|
+
## ['/api', [
|
22
|
+
## ['/books', [
|
23
|
+
## ['' , books_api],
|
24
|
+
## ['/:id(.:format)' , book_api],
|
25
|
+
## ['/:book_id/comments/:comment_id', comment_api],
|
26
|
+
## ]],
|
27
|
+
## ]],
|
28
|
+
## ['/admin', [
|
29
|
+
## ['/books' , admin_books_app],
|
30
|
+
## ]],
|
31
|
+
## ]
|
32
|
+
## router = Rack::JetRouter.new(urlpath_mapping)
|
33
|
+
## router.find('/api/books/123.html')
|
34
|
+
## #=> [book_api, {"id"=>"123", "format"=>"html"}]
|
35
|
+
## status, headers, body = router.call(env)
|
36
|
+
##
|
37
|
+
## ### or:
|
38
|
+
## urlpath_mapping = [
|
39
|
+
## ['/' , {GET: welcome_app}],
|
40
|
+
## ['/api', [
|
41
|
+
## ['/books', [
|
42
|
+
## ['' , {GET: book_list_api, POST: book_create_api}],
|
43
|
+
## ['/:id(.:format)' , {GET: book_show_api, PUT: book_update_api}],
|
44
|
+
## ['/:book_id/comments/:comment_id', {POST: comment_create_api}],
|
45
|
+
## ]],
|
46
|
+
## ]],
|
47
|
+
## ['/admin', [
|
48
|
+
## ['/books' , {ANY: admin_books_app}],
|
49
|
+
## ]],
|
50
|
+
## ]
|
51
|
+
## router = Rack::JetRouter.new(urlpath_mapping)
|
52
|
+
## router.find('/api/books/123')
|
53
|
+
## #=> [{"GET"=>book_show_api, "PUT"=>book_update_api}, {"id"=>"123", "format"=>nil}]
|
54
|
+
## status, headers, body = router.call(env)
|
55
|
+
##
|
56
|
+
class JetRouter
|
57
|
+
|
58
|
+
def initialize(mapping, urlpath_cache_size: 0)
|
59
|
+
#; [!u2ff4] compiles urlpath mapping.
|
60
|
+
(@urlpath_rexp, # ex: {'/api/books'=>BooksApp}
|
61
|
+
@fixed_urlpath_dict, # ex: [[%r'\A/api/books/([^./]+)\z', ['id'], BookApp]]
|
62
|
+
@variable_urlpath_list, # ex: %r'\A(?:/api(?:/books(?:/[^./]+(\z))))\z'
|
63
|
+
) = compile_mapping(mapping)
|
64
|
+
## cache for variable urlpath (= containg urlpath parameters)
|
65
|
+
@urlpath_cache_size = urlpath_cache_size
|
66
|
+
@variable_urlpath_cache = urlpath_cache_size > 0 ? {} : nil
|
67
|
+
end
|
68
|
+
|
69
|
+
## Finds rack app according to PATH_INFO and REQUEST_METHOD and invokes it.
|
70
|
+
def call(env)
|
71
|
+
#; [!fpw8x] finds mapped app according to env['PATH_INFO'].
|
72
|
+
req_path = env['PATH_INFO']
|
73
|
+
app, urlpath_params = find(req_path)
|
74
|
+
#; [!wxt2g] guesses correct urlpath and redirects to it automaticaly when request path not found.
|
75
|
+
#; [!3vsua] doesn't redict automatically when request path is '/'.
|
76
|
+
unless app || req_path == '/'
|
77
|
+
location = req_path =~ /\/\z/ ? req_path[0..-2] : req_path + '/'
|
78
|
+
app, urlpath_params = find(location)
|
79
|
+
return redirect_to(location) if app
|
80
|
+
end
|
81
|
+
#; [!30x0k] returns 404 when request urlpath not found.
|
82
|
+
return error_not_found(env) unless app
|
83
|
+
#; [!gclbs] if mapped object is a Hash...
|
84
|
+
if app.is_a?(Hash)
|
85
|
+
#; [!p1fzn] invokes app mapped to request method.
|
86
|
+
#; [!5m64a] returns 405 when request method is not allowed.
|
87
|
+
#; [!ys1e2] uses GET method when HEAD is not mapped.
|
88
|
+
#; [!2hx6j] try ANY method when request method is not mapped.
|
89
|
+
dict = app
|
90
|
+
req_meth = env['REQUEST_METHOD']
|
91
|
+
app = dict[req_meth] || (req_meth == 'HEAD' ? dict['GET'] : nil) || dict['ANY']
|
92
|
+
return error_not_allowed(env) unless app
|
93
|
+
end
|
94
|
+
#; [!2c32f] stores urlpath parameters as env['rack.urlpath_params'].
|
95
|
+
store_urlpath_params(env, urlpath_params)
|
96
|
+
#; [!hse47] invokes app mapped to request urlpath.
|
97
|
+
return app.call(env) # make body empty when HEAD?
|
98
|
+
end
|
99
|
+
|
100
|
+
## Finds app or Hash mapped to request path.
|
101
|
+
##
|
102
|
+
## ex:
|
103
|
+
## find('/api/books/123') #=> [BookApp, {"id"=>"123"}]
|
104
|
+
def find(req_path)
|
105
|
+
#; [!24khb] finds in fixed urlpaths at first.
|
106
|
+
#; [!iwyzd] urlpath param value is nil when found in fixed urlpaths.
|
107
|
+
obj = @fixed_urlpath_dict[req_path]
|
108
|
+
return obj, nil if obj
|
109
|
+
#; [!upacd] finds in variable urlpath cache if it is enabled.
|
110
|
+
#; [!1zx7t] variable urlpath cache is based on LRU.
|
111
|
+
cache = @variable_urlpath_cache
|
112
|
+
if cache && (pair = cache.delete(req_path))
|
113
|
+
cache[req_path] = pair
|
114
|
+
return pair
|
115
|
+
end
|
116
|
+
#; [!vpdzn] returns nil when urlpath not found.
|
117
|
+
m = @urlpath_rexp.match(req_path)
|
118
|
+
return nil unless m
|
119
|
+
index = m.captures.find_index('')
|
120
|
+
return nil unless index
|
121
|
+
#; [!ijqws] returns mapped object and urlpath parameter values when urlpath found.
|
122
|
+
full_urlpath_rexp, param_names, obj = @variable_urlpath_list[index]
|
123
|
+
m = full_urlpath_rexp.match(req_path)
|
124
|
+
param_values = m.captures
|
125
|
+
vars = build_urlpath_parameter_vars(param_names, param_values)
|
126
|
+
#; [!84inr] caches result when variable urlpath cache enabled.
|
127
|
+
if cache
|
128
|
+
cache.shift() if cache.length >= @urlpath_cache_size
|
129
|
+
cache[req_path] = [obj, vars]
|
130
|
+
end
|
131
|
+
return obj, vars
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
## Returns [404, {...}, [...]]. Override in subclass if necessary.
|
137
|
+
def error_not_found(env)
|
138
|
+
#; [!mlruv] returns 404 response.
|
139
|
+
return [404, {"Content-Type"=>"text/plain"}, ["404 Not Found"]]
|
140
|
+
end
|
141
|
+
|
142
|
+
## Returns [405, {...}, [...]]. Override in subclass if necessary.
|
143
|
+
def error_not_allowed(env)
|
144
|
+
#; [!mjigf] returns 405 response.
|
145
|
+
return [405, {"Content-Type"=>"text/plain"}, ["405 Method Not Allowed"]]
|
146
|
+
end
|
147
|
+
|
148
|
+
## Returns [301, {"Location"=>location, ...}, [...]]. Override in subclass if necessary.
|
149
|
+
def redirect_to(location)
|
150
|
+
content = "Redirect to #{location}"
|
151
|
+
return [301, {"Content-Type"=>"text/plain", "Location"=>location}, [content]]
|
152
|
+
end
|
153
|
+
|
154
|
+
## Sets env['rack.urlpath_params'] = vars. Override in subclass if necessary.
|
155
|
+
def store_urlpath_params(env, vars)
|
156
|
+
env['rack.urlpath_params'] = vars if vars
|
157
|
+
end
|
158
|
+
|
159
|
+
## Returns Hash object representing urlpath parameters. Override if necessary.
|
160
|
+
##
|
161
|
+
## ex:
|
162
|
+
## class MyRouter < JetRouter
|
163
|
+
## def build_urlpath_parameter_vars(names, values)
|
164
|
+
## return names.zip(values).each_with_object({}) {|(k, v), d|
|
165
|
+
## ## converts urlpath pavam value into integer
|
166
|
+
## v = v.to_i if k == 'id' || k.end_with?('_id')
|
167
|
+
## d[k] = v
|
168
|
+
## }
|
169
|
+
## end
|
170
|
+
## end
|
171
|
+
def build_urlpath_parameter_vars(names, values)
|
172
|
+
return Hash[names.zip(values)]
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
## Compiles urlpath mapping. Called from '#initialize()'.
|
178
|
+
def compile_mapping(mapping)
|
179
|
+
rexp_buf = ['\A']
|
180
|
+
prefix_pat = ''
|
181
|
+
fixed_urlpaths = {} # ex: {'/api/books'=>BooksApp}
|
182
|
+
variable_urlpaths = [] # ex: [[%r'\A/api/books/([^./]+)\z', ['id'], BookApp]]
|
183
|
+
_compile_mapping(mapping, rexp_buf, prefix_pat, fixed_urlpaths, variable_urlpaths)
|
184
|
+
## ex: %r'\A(?:/api(?:/books(?:/[^./]+(\z)|/[^./]+/edit(\z))))\z'
|
185
|
+
rexp_buf << '\z'
|
186
|
+
urlpath_rexp = Regexp.new(rexp_buf.join())
|
187
|
+
#; [!xzo7k] returns regexp, hash, and array.
|
188
|
+
return urlpath_rexp, fixed_urlpaths, variable_urlpaths
|
189
|
+
end
|
190
|
+
|
191
|
+
def _compile_mapping(mapping, rexp_buf, prefix_pat, fixed_dict, variable_list)
|
192
|
+
param_pat1 = '[^./]+'
|
193
|
+
param_pat2 = '([^./]+)'
|
194
|
+
rexp_buf << '(?:'
|
195
|
+
len = rexp_buf.length
|
196
|
+
mapping.each do |urlpath_pat, obj|
|
197
|
+
rexp_buf << '|' if rexp_buf.length != len
|
198
|
+
full_urlpath_pat = "#{prefix_pat}#{urlpath_pat}"
|
199
|
+
#; [!ospaf] accepts nested mapping.
|
200
|
+
if obj.is_a?(Array)
|
201
|
+
rexp_str, _ = compile_urlpath_pattern(urlpath_pat, param_pat1)
|
202
|
+
rexp_buf << rexp_str
|
203
|
+
len2 = rexp_buf.length
|
204
|
+
_compile_mapping(obj, rexp_buf, full_urlpath_pat, fixed_dict, variable_list)
|
205
|
+
#; [!pv2au] deletes unnecessary urlpath regexp.
|
206
|
+
if rexp_buf.length == len2
|
207
|
+
x = rexp_buf.pop()
|
208
|
+
x == rexp_str or raise "assertion failed"
|
209
|
+
end
|
210
|
+
#; [!2ktpf] handles end-point.
|
211
|
+
else
|
212
|
+
#; [!guhdc] if mapping dict is specified...
|
213
|
+
if obj.is_a?(Hash)
|
214
|
+
obj = normalize_mapping_keys(obj)
|
215
|
+
end
|
216
|
+
#; [!l63vu] handles urlpath pattern as fixed when no urlpath params.
|
217
|
+
full_urlpath_rexp_str, param_names = compile_urlpath_pattern(full_urlpath_pat, param_pat2)
|
218
|
+
fixed_pattern = param_names.nil?
|
219
|
+
if fixed_pattern
|
220
|
+
fixed_dict[full_urlpath_pat] = obj
|
221
|
+
#; [!vfytw] handles urlpath pattern as variable when urlpath param exists.
|
222
|
+
else
|
223
|
+
rexp_str, _ = compile_urlpath_pattern(urlpath_pat, param_pat1)
|
224
|
+
rexp_buf << rexp_str << '(\z)'
|
225
|
+
full_urlpath_rexp = Regexp.new("\\A#{full_urlpath_rexp_str}\\z")
|
226
|
+
variable_list << [full_urlpath_rexp, param_names, obj]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
#; [!gfxgr] deletes unnecessary grouping.
|
231
|
+
if rexp_buf.length == len
|
232
|
+
x = rexp_buf.pop() # delete '(?:'
|
233
|
+
x == '(?:' or raise "assertion failed"
|
234
|
+
else
|
235
|
+
rexp_buf << ')'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
## Compiles '/books/:id' into ['/books/([^./]+)', ["id"]].
|
240
|
+
def compile_urlpath_pattern(urlpath_pat, param_pat='([^./]+)')
|
241
|
+
s = "".dup()
|
242
|
+
param_names = []
|
243
|
+
pos = 0
|
244
|
+
urlpath_pat.scan(/:(\w+)|\((.*?)\)/) do |name, optional|
|
245
|
+
#; [!joozm] escapes metachars with backslash in text part.
|
246
|
+
m = Regexp.last_match
|
247
|
+
text = urlpath_pat[pos...m.begin(0)]
|
248
|
+
pos = m.end(0)
|
249
|
+
s << Regexp.escape(text)
|
250
|
+
#; [!rpezs] converts '/books/:id' into '/books/([^./]+)'.
|
251
|
+
if name
|
252
|
+
param_names << name
|
253
|
+
s << param_pat
|
254
|
+
#; [!4dcsa] converts '/index(.:format)' into '/index(?:\.([^./]+))?'.
|
255
|
+
elsif optional
|
256
|
+
s << '(?:'
|
257
|
+
optional.scan(/(.*?)(?::(\w+))/) do |text2, name2|
|
258
|
+
s << Regexp.escape(text2) << param_pat
|
259
|
+
param_names << name2
|
260
|
+
end
|
261
|
+
s << Regexp.escape($' || optional)
|
262
|
+
s << ')?'
|
263
|
+
#
|
264
|
+
else
|
265
|
+
raise "unreachable: urlpath=#{urlpath.inspect}"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
#; [!1d5ya] rethrns compiled string and nil when no urlpath parameters nor parens.
|
269
|
+
#; [!of1zq] returns compiled string and urlpath param names when urlpath param or parens exist.
|
270
|
+
if pos == 0
|
271
|
+
return Regexp.escape(urlpath_pat), nil
|
272
|
+
else
|
273
|
+
s << Regexp.escape(urlpath_pat[pos..-1])
|
274
|
+
return s, param_names
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def normalize_mapping_keys(dict)
|
279
|
+
#; [!r7cmk] converts keys into string.
|
280
|
+
#; [!z9kww] allows 'ANY' as request method.
|
281
|
+
#; [!k7sme] raises error when unknown request method specified.
|
282
|
+
request_methods = REQUEST_METHODS
|
283
|
+
return dict.each_with_object({}) do |(meth, app), newdict|
|
284
|
+
meth_str = meth.to_s
|
285
|
+
request_methods[meth_str] || meth_str == 'ANY' or
|
286
|
+
raise ArgumentError.new("#{meth.inspect}: unknown request method.")
|
287
|
+
newdict[meth_str] = app
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
#; [!haggu] contains available request methods.
|
292
|
+
REQUEST_METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE LINK UNLINK] \
|
293
|
+
.each_with_object({}) {|s, d| d[s] = s.intern }
|
294
|
+
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
end
|