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