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