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 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