keight 0.2.0 → 0.3.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 +4 -4
- data/CHANGES.md +237 -0
- data/README.md +285 -81
- data/Rakefile +27 -1
- data/bench/benchmarker.rb +2 -2
- data/bin/k8rb +112 -305
- data/keight.gemspec +4 -6
- data/lib/keight.rb +860 -822
- data/test/keight_test.rb +1007 -933
- data/test/oktest.rb +2 -2
- metadata +4 -68
- data/CHANGES.txt +0 -64
- data/lib/keight/skeleton/.gitignore +0 -10
- data/lib/keight/skeleton/README.txt +0 -13
- data/lib/keight/skeleton/app/action.rb +0 -106
- data/lib/keight/skeleton/app/api/hello.rb +0 -39
- data/lib/keight/skeleton/app/form/.keep +0 -0
- data/lib/keight/skeleton/app/helper/.keep +0 -0
- data/lib/keight/skeleton/app/model.rb +0 -144
- data/lib/keight/skeleton/app/model/.keep +0 -0
- data/lib/keight/skeleton/app/page/welcome.rb +0 -17
- data/lib/keight/skeleton/app/template/_layout.html.eruby +0 -56
- data/lib/keight/skeleton/app/template/welcome.html.eruby +0 -6
- data/lib/keight/skeleton/app/usecase/.keep +0 -0
- data/lib/keight/skeleton/config.rb +0 -46
- data/lib/keight/skeleton/config.ru +0 -6
- data/lib/keight/skeleton/config/app.rb +0 -29
- data/lib/keight/skeleton/config/app_dev.rb +0 -8
- data/lib/keight/skeleton/config/app_prod.rb +0 -7
- data/lib/keight/skeleton/config/app_stg.rb +0 -5
- data/lib/keight/skeleton/config/app_test.rb +0 -8
- data/lib/keight/skeleton/config/server_puma.rb +0 -22
- data/lib/keight/skeleton/config/server_unicorn.rb +0 -21
- data/lib/keight/skeleton/config/urlpath_mapping.rb +0 -16
- data/lib/keight/skeleton/index.txt +0 -39
- data/lib/keight/skeleton/main.rb +0 -22
- data/lib/keight/skeleton/static/lib/.keep +0 -0
- data/lib/keight/skeleton/test/api/hello_test.rb +0 -27
- data/lib/keight/skeleton/test/test_helper.rb +0 -9
data/keight.gemspec
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |o|
|
5
5
|
o.name = "keight"
|
6
|
-
o.version = "$Release: 0.
|
6
|
+
o.version = "$Release: 0.3.0 $".split[1]
|
7
7
|
o.author = "makoto kuwata"
|
8
8
|
o.email = "kwa(at)kuwata-lab.com"
|
9
9
|
o.platform = Gem::Platform::RUBY
|
10
10
|
o.homepage = "https://github.com/kwatch/keight/tree/ruby"
|
11
|
-
o.license = "MIT
|
11
|
+
o.license = "MIT" # ref: http://spdx.org/licenses/
|
12
12
|
o.summary = "Jet-speed webapp framework for Ruby"
|
13
13
|
o.description = <<'END'
|
14
14
|
Keight.rb is the crazy-fast webapp framework for Ruby.
|
@@ -18,9 +18,9 @@ See https://github.com/kwatch/keight/tree/ruby for details.
|
|
18
18
|
END
|
19
19
|
|
20
20
|
o.files = Dir[*%w[
|
21
|
-
README.md CHANGES.
|
21
|
+
README.md CHANGES.md MIT-LICENSE keight.gemspec Rakefile
|
22
22
|
bin/k8rb
|
23
|
-
lib/keight.rb
|
23
|
+
lib/keight.rb
|
24
24
|
test/*_test.rb test/data/* test/oktest.rb
|
25
25
|
bench/bench.rb bench/benchmarker.rb
|
26
26
|
]]
|
@@ -30,8 +30,6 @@ END
|
|
30
30
|
o.test_file = "test/keight_test.rb"
|
31
31
|
|
32
32
|
o.required_ruby_version = '>= 2.0'
|
33
|
-
o.add_runtime_dependency 'rack', '~> 1.6'
|
34
|
-
o.add_runtime_dependency 'baby_erubis', '~> 2.1', '>= 2.1.1'
|
35
33
|
#o.add_development_dependency "oktest", "~> 0"
|
36
34
|
o.add_development_dependency 'rack-test_app', '~> 1.0', '>= 1.0.0'
|
37
35
|
end
|
data/lib/keight.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
###
|
4
|
-
### $Release: 0.
|
4
|
+
### $Release: 0.3.0 $
|
5
5
|
### $Copyright: copyright(c) 2014-2016 kuwata-lab.com all rights reserved $
|
6
6
|
### $License: MIT License $
|
7
7
|
###
|
@@ -15,7 +15,7 @@ require 'digest/sha1'
|
|
15
15
|
|
16
16
|
module K8
|
17
17
|
|
18
|
-
RELEASE = '$Release: 0.
|
18
|
+
RELEASE = '$Release: 0.3.0 $'.split()[1]
|
19
19
|
|
20
20
|
FILEPATH = __FILE__
|
21
21
|
|
@@ -30,60 +30,93 @@ module K8
|
|
30
30
|
"TRACE" => :TRACE,
|
31
31
|
}
|
32
32
|
|
33
|
+
## ref: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
33
34
|
HTTP_RESPONSE_STATUS = {
|
34
35
|
100 => "Continue",
|
35
36
|
101 => "Switching Protocols",
|
36
|
-
102 => "Processing",
|
37
|
+
102 => "Processing", # WebDAV; RFC 2518
|
38
|
+
103 => "Checkpoint", # Unofficial
|
37
39
|
200 => "OK",
|
38
40
|
201 => "Created",
|
39
41
|
202 => "Accepted",
|
40
|
-
203 => "Non-Authoritative Information",
|
42
|
+
203 => "Non-Authoritative Information", # since HTTP/1.1
|
41
43
|
204 => "No Content",
|
42
44
|
205 => "Reset Content",
|
43
|
-
206 => "Partial Content",
|
44
|
-
207 => "Multi-Status",
|
45
|
-
208 => "Already Reported",
|
46
|
-
226 => "IM Used",
|
45
|
+
206 => "Partial Content", # RFC 7233
|
46
|
+
207 => "Multi-Status", # WebDAV; RFC 4918
|
47
|
+
208 => "Already Reported", # WebDAV; RFC 5842
|
48
|
+
226 => "IM Used", # RFC 3229
|
47
49
|
300 => "Multiple Choices",
|
48
50
|
301 => "Moved Permanently",
|
49
51
|
302 => "Found",
|
50
|
-
303 => "See Other",
|
51
|
-
304 => "Not Modified",
|
52
|
-
305 => "Use Proxy",
|
53
|
-
|
52
|
+
303 => "See Other", # since HTTP/1.1
|
53
|
+
304 => "Not Modified", # RFC 7232
|
54
|
+
305 => "Use Proxy", # since HTTP/1.1
|
55
|
+
306 => "Switch Proxy",
|
56
|
+
307 => "Temporary Redirect", # since HTTP/1.1
|
57
|
+
308 => "Permanent Redirect", # RFC 7538
|
54
58
|
400 => "Bad Request",
|
55
|
-
401 => "Unauthorized",
|
59
|
+
401 => "Unauthorized", # RFC 7235
|
56
60
|
402 => "Payment Required",
|
57
61
|
403 => "Forbidden",
|
58
62
|
404 => "Not Found",
|
59
63
|
405 => "Method Not Allowed",
|
60
64
|
406 => "Not Acceptable",
|
61
|
-
407 => "Proxy Authentication Required",
|
65
|
+
407 => "Proxy Authentication Required", # RFC 7235
|
62
66
|
408 => "Request Timeout",
|
63
67
|
409 => "Conflict",
|
64
68
|
410 => "Gone",
|
65
69
|
411 => "Length Required",
|
66
|
-
412 => "Precondition Failed",
|
67
|
-
413 => "
|
68
|
-
414 => "
|
70
|
+
412 => "Precondition Failed", # RFC 7232
|
71
|
+
413 => "Payload Too Large", # RFC 7231
|
72
|
+
414 => "URI Too Long", # RFC 7231
|
69
73
|
415 => "Unsupported Media Type",
|
70
|
-
416 => "
|
74
|
+
416 => "Range Not Satisfiable", # RFC 7233
|
71
75
|
417 => "Expectation Failed",
|
72
|
-
418 => "I'm a teapot",
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
+
418 => "I'm a teapot", # RFC 2324
|
77
|
+
#420 => "Enhance Your Calm", # Twitter
|
78
|
+
#420 => "Method Failure", # Spring Framework
|
79
|
+
421 => "Misdirected Request", # RFC 7540
|
80
|
+
422 => "Unprocessable Entity", # WebDAV; RFC 4918
|
81
|
+
423 => "Locked", # WebDAV; RFC 4918
|
82
|
+
424 => "Failed Dependency", # WebDAV; RFC 4918
|
76
83
|
426 => "Upgrade Required",
|
84
|
+
428 => "Precondition Required", # RFC 6585
|
85
|
+
429 => "Too Many Requests", # RFC 6585
|
86
|
+
431 => "Request Header Fields Too Large", # RFC 6585
|
87
|
+
#440 => "Login Timeout", # IIS
|
88
|
+
444 => "No Response", # nginx
|
89
|
+
#449 => "Retry With", # IIS
|
90
|
+
#450 => "Blocked by Windows Parental Controls", # Microsoft
|
91
|
+
#451 => "Redirect", # IIS
|
92
|
+
451 => "Unavailable For Legal Reasons",
|
93
|
+
495 => "SSL Certificate Error", # nginx
|
94
|
+
496 => "SSL Certificate Required", # nginx
|
95
|
+
497 => "HTTP Request Sent to HTTPS Port", # nginx
|
96
|
+
#498 => "Invalid Token", # Esri
|
97
|
+
499 => "Client Closed Request", # nginx
|
98
|
+
#499 => "Request has been forbidden by antivirus",
|
99
|
+
#499 => "Token Required", # Esri
|
77
100
|
500 => "Internal Server Error",
|
78
101
|
501 => "Not Implemented",
|
79
102
|
502 => "Bad Gateway",
|
80
103
|
503 => "Service Unavailable",
|
81
104
|
504 => "Gateway Timeout",
|
82
105
|
505 => "HTTP Version Not Supported",
|
83
|
-
506 => "Variant Also Negotiates",
|
84
|
-
507 => "Insufficient Storage",
|
85
|
-
508 => "Loop Detected",
|
86
|
-
|
106
|
+
506 => "Variant Also Negotiates", # RFC 2295
|
107
|
+
507 => "Insufficient Storage", # WebDAV; RFC 4918
|
108
|
+
508 => "Loop Detected", # WebDAV; RFC 5842
|
109
|
+
509 => "Bandwidth Limit Exceeded", # Apache Web Server/cPanel
|
110
|
+
510 => "Not Extended", # RFC 2774
|
111
|
+
511 => "Network Authentication Required", # RFC 6585
|
112
|
+
520 => "Unknown Error", # CloudFlare
|
113
|
+
521 => "Web Server Is Down", # CloudFlare
|
114
|
+
522 => "Connection Timed Out", # CloudFlare
|
115
|
+
523 => "Origin Is Unreachable", # CloudFlare
|
116
|
+
524 => "A Timeout Occurred", # CloudFlare
|
117
|
+
525 => "SSL Handshake Failed", # CloudFlare
|
118
|
+
526 => "Invalid SSL Certificate", # CloudFlare
|
119
|
+
#530 => "Site is frozen", # Unofficial
|
87
120
|
}.each {|_, v| v.freeze }
|
88
121
|
|
89
122
|
MIME_TYPES = {
|
@@ -210,10 +243,10 @@ module K8
|
|
210
243
|
end
|
211
244
|
|
212
245
|
def _parse(query_str, separator)
|
213
|
-
#; [!engr6] returns empty Hash object when query string is empty.
|
246
|
+
#; [!engr6] returns empty Hash/dict object when query string is empty.
|
214
247
|
d = {}
|
215
248
|
return d if query_str.empty?
|
216
|
-
#; [!fzt3w] parses query string and returns
|
249
|
+
#; [!fzt3w] parses query string and returns Hash/dict object.
|
217
250
|
equal = '='
|
218
251
|
brackets = '[]'
|
219
252
|
query_str.split(separator).each do |s|
|
@@ -363,15 +396,204 @@ module K8
|
|
363
396
|
MONTHS = [nil, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
364
397
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].freeze
|
365
398
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
399
|
+
def _http_utc_time_without_locale(utc_time)
|
400
|
+
utc_time.utc? or
|
401
|
+
raise ArgumentError.new("http_utc_time(#{utc_time.inspect}): expected UTC time but got local time.")
|
402
|
+
return utc_time.strftime("#{WEEKDAYS[utc_time.wday]}, %d #{MONTHS[utc_time.month]} %Y %H:%M:%S GMT")
|
403
|
+
end
|
404
|
+
|
405
|
+
has_diff = (1..12).collect {|mo| Time.utc(2000, mo, 1) }.any? {|t|
|
406
|
+
http_utc_time(t) != _http_utc_time_without_locale(t)
|
407
|
+
}
|
408
|
+
alias http_utc_time _http_utc_time_without_locale if has_diff
|
409
|
+
|
410
|
+
|
411
|
+
##
|
412
|
+
## Temporary file which is removed automatically at end of response.
|
413
|
+
##
|
414
|
+
## Example:
|
415
|
+
##
|
416
|
+
## def do_download_csv()
|
417
|
+
## ## generate temporary filepath
|
418
|
+
## tmpfile = K8::Util::TemporaryFile.new
|
419
|
+
## p tmpfile.path #=> ex: '/var/tmp/tmp-0372042668525.tmp'
|
420
|
+
## p File.exist?(tmpfile.path) #=> false
|
421
|
+
## ## create temporary file and content
|
422
|
+
## sql = "select * from table1"
|
423
|
+
## system("psql dbname -A -F',' -c \"#{sql}\" | gzip > #{tmpfile.path}")
|
424
|
+
## p File.exist?(tmpfile.path) #=> true
|
425
|
+
## ## set resonse headers
|
426
|
+
## @resp.headers['Content-Type'] = 'text/csv;charset=UTF-8'
|
427
|
+
## @resp.headers['Content-Length'] = File.size(tmpfile.path)
|
428
|
+
## @resp.headers['Content-Disposition'] = 'attachment;filename="file1.csv"'
|
429
|
+
## @resp.headers['Content-Encoding'] = "gzip"
|
430
|
+
## ## return temporary file
|
431
|
+
## return tmpfile # will be removed automatically at end of response!!
|
432
|
+
## end
|
433
|
+
##
|
434
|
+
class TemporaryFile
|
435
|
+
|
436
|
+
TMPDIR = (ENV['TMPDIR'] || proc {require 'etc'; Etc.systmpdir}.call || '/tmp').chomp('/')
|
437
|
+
CHUNK_SIZE = 8 * 1024 # 8KB
|
438
|
+
|
439
|
+
def self.new_path(tmpdir=nil)
|
440
|
+
#; [!hvnzd] generates new temorary filepath under temporary directory.
|
441
|
+
#; [!ulb2e] uses default temporary directory path if tmpdir is not specified.
|
442
|
+
tmpdir ||= TMPDIR
|
443
|
+
randstr = rand().to_s[2..14]
|
444
|
+
return "#{tmpdir}/tmp-#{randstr}.tmp"
|
445
|
+
end
|
446
|
+
|
447
|
+
def initialize(path=nil, tmpdir: nil, chunk_size: nil)
|
448
|
+
#; [!ljilm] generates temporary filepath automatically when filepath is not specified.
|
449
|
+
@path = path || self.class.new_path(tmpdir)
|
450
|
+
@tmpdir = tmpdir if tmpdir
|
451
|
+
@chunk_size = chunk_size if chunk_size
|
452
|
+
end
|
453
|
+
|
454
|
+
attr_reader :path
|
455
|
+
|
456
|
+
def each
|
457
|
+
#; [!d9suq] opens temporary file with binary mode.
|
458
|
+
#; [!68xdj] reads chunk size data from temporary file per iteration.
|
459
|
+
size = @chunk_size || CHUNK_SIZE
|
460
|
+
File.open(@path, 'rb') do |f|
|
461
|
+
yield f.read(size)
|
462
|
+
end
|
463
|
+
ensure
|
464
|
+
#; [!i0dmd] removes temporary file automatically at end of loop.
|
465
|
+
#; [!347an] removes temporary file even if error raised in block.
|
466
|
+
File.unlink @path
|
467
|
+
self
|
468
|
+
end
|
469
|
+
|
470
|
+
end
|
471
|
+
|
472
|
+
|
473
|
+
##
|
474
|
+
## Invoke shell command and responses it's output to client.
|
475
|
+
##
|
476
|
+
## Example:
|
477
|
+
##
|
478
|
+
## def do_download_csv()
|
479
|
+
## ## for example, run SQL and generate CSV file (for postgresql)
|
480
|
+
## sql = "select * from table1"
|
481
|
+
## cmd = "psql -AF',' -U dbuser dbname | iconv -f UTF-8 -t CP932 -c | gzip"
|
482
|
+
## shell_command = K8::Util::ShellCommand.new(cmd, input: sql) do
|
483
|
+
## ## callback after sending response body
|
484
|
+
## File.unlink(tempfile) if File.exist?(templfile) # for example
|
485
|
+
## end
|
486
|
+
## begin
|
487
|
+
## return shell_command.start() do
|
488
|
+
## ## callback before sending response body
|
489
|
+
## @resp.headers['Content-Type'] = "text/csv;charset=Shift_JIS"
|
490
|
+
## @resp.headers['Content-Disposition'] = 'attachment;filename="file.csv"'
|
491
|
+
## @resp.headers['Content-Encoding'] = "gzip"
|
492
|
+
## end
|
493
|
+
## rescue K8::Util::ShellCommandError => ex
|
494
|
+
## logger = @req.env['rack.logger']
|
495
|
+
## logger.error(ex.message) if logger
|
496
|
+
## @resp.status = 500
|
497
|
+
## @resp.headers['Content-Type'] = "text/plain;charset=UTF-8"
|
498
|
+
## return ex.message
|
499
|
+
## end
|
500
|
+
## end
|
501
|
+
##
|
502
|
+
class ShellCommand
|
503
|
+
|
504
|
+
CHUNK_SIZE = 8 * 1024
|
505
|
+
|
506
|
+
def initialize(command, input: nil, chunk_size: nil, &teardown)
|
507
|
+
#; [!j95pi] takes shell command and input string.
|
508
|
+
@command = command # ex: "psql -AF',' dbname | gzip"
|
509
|
+
@input = input # ex: "select * from table1"
|
510
|
+
@chunk_size = chunk_size || CHUNK_SIZE
|
511
|
+
@teardown = teardown
|
512
|
+
@process_id = nil
|
513
|
+
@tuple = nil
|
514
|
+
end
|
515
|
+
|
516
|
+
attr_reader :command, :input, :process_id
|
517
|
+
|
518
|
+
def start
|
519
|
+
#; [!66uck] not allowed to start more than once.
|
520
|
+
@process_id.nil? or # TODO: close sout and serr
|
521
|
+
raise ShellCommandError.new("Already started (comand: #{@command.inspect})")
|
522
|
+
#; [!9seos] invokes shell command.
|
523
|
+
require 'open3' unless defined?(::Open3)
|
524
|
+
sin, sout, serr, waiter = ::Open3.popen3(@command)
|
525
|
+
@process_id = waiter.pid
|
526
|
+
size = @chunk_size
|
527
|
+
begin
|
528
|
+
#; [!d766y] writes input string if provided to initializer.
|
529
|
+
sin.write(input) if input
|
530
|
+
sin.close()
|
531
|
+
#; [!f651x] reads first chunk data.
|
532
|
+
#; [!cjstj] raises ShellCommandError when command prints something to stderr.
|
533
|
+
chunk = sout.read(size)
|
534
|
+
if chunk.nil?
|
535
|
+
error = serr.read()
|
536
|
+
log_error(error.to_s)
|
537
|
+
error = "Command failed: #{@command}" if ! error || error.empty?
|
538
|
+
raise ShellCommandError.new(error)
|
539
|
+
end
|
540
|
+
#; [!bt12n] saves stdout, stderr, command process, and first chunk data.
|
541
|
+
@tuple = [sout, serr, waiter, chunk]
|
542
|
+
#; [!kgnel] yields callback (if given) when command invoked successfully.
|
543
|
+
yield if block_given?
|
544
|
+
#; [!2989u] closes both stdout and stderr when error raised.
|
545
|
+
rescue => ex
|
546
|
+
sout.close()
|
547
|
+
serr.close()
|
548
|
+
raise
|
549
|
+
end
|
550
|
+
#; [!fp98i] returns self.
|
551
|
+
self
|
552
|
+
end
|
553
|
+
|
554
|
+
def each
|
555
|
+
#; [!ssgmm] '#start()' should be called before '#each()'.
|
556
|
+
@process_id or
|
557
|
+
raise ShellCommandError.new("Not started yet (command: #{@command.inspect}).")
|
558
|
+
#; [!vpmbw] yields each chunk data.
|
559
|
+
sout, serr, waiter, chunk = @tuple
|
560
|
+
@tuple = nil
|
561
|
+
yield chunk
|
562
|
+
size = @chunk_size
|
563
|
+
ex = nil
|
564
|
+
begin
|
565
|
+
while (chunk = sout.read(size))
|
566
|
+
yield chunk
|
567
|
+
end
|
568
|
+
#; [!70xdy] logs stderr output.
|
569
|
+
error = serr.read()
|
570
|
+
log_error(error) if error && ! error.empty?
|
571
|
+
rescue => ex
|
572
|
+
raise
|
573
|
+
ensure
|
574
|
+
#; [!2wll8] closes stdout and stderr, even if error raised.
|
575
|
+
sout.close()
|
576
|
+
serr.close()
|
577
|
+
#; [!0ebq5] calls callback specified at initializer with error object.
|
578
|
+
@teardown.yield(ex) if @teardown
|
579
|
+
end
|
580
|
+
#; [!ln8we] returns self.
|
581
|
+
self
|
582
|
+
end
|
583
|
+
|
584
|
+
def log_error(message)
|
585
|
+
$stderr.write("[ERROR] ShellCommand: #{@command.inspect} #-------\n")
|
586
|
+
$stderr.write(message); $stderr.write("\n") unless message.end_with?("\n")
|
587
|
+
$stderr.write("--------------------\n")
|
372
588
|
end
|
589
|
+
|
373
590
|
end
|
374
591
|
|
592
|
+
|
593
|
+
class ShellCommandError < StandardError
|
594
|
+
end
|
595
|
+
|
596
|
+
|
375
597
|
end
|
376
598
|
|
377
599
|
|
@@ -426,300 +648,27 @@ module K8
|
|
426
648
|
end
|
427
649
|
|
428
650
|
|
429
|
-
class BaseError <
|
430
|
-
end
|
431
|
-
|
432
|
-
|
433
|
-
class ContentTypeRequiredError < BaseError
|
651
|
+
class BaseError < StandardError
|
434
652
|
end
|
435
653
|
|
436
654
|
|
437
|
-
class
|
655
|
+
class ActionMappingError < BaseError
|
438
656
|
end
|
439
657
|
|
440
658
|
|
441
|
-
class
|
659
|
+
class ContentTypeRequiredError < BaseError
|
442
660
|
end
|
443
661
|
|
444
662
|
|
445
|
-
class
|
446
|
-
|
447
|
-
def initialize(env)
|
448
|
-
#; [!yb9k9] sets @env.
|
449
|
-
@env = env
|
450
|
-
#; [!yo22o] sets @method as Symbol value.
|
451
|
-
@method = HTTP_REQUEST_METHODS[env['REQUEST_METHOD']] or
|
452
|
-
raise HTTPException.new(400, "#{env['REQUEST_METHOD'].inspect}: unknown request method.")
|
453
|
-
#; [!twgmi] sets @path.
|
454
|
-
@path = (x = env['PATH_INFO'])
|
455
|
-
#; [!ae8ws] uses SCRIPT_NAME as urlpath when PATH_INFO is not provided.
|
456
|
-
@path = env['SCRIPT_NAME'] if x.nil? || x.empty?
|
457
|
-
end
|
458
|
-
|
459
|
-
attr_reader :env, :method, :path
|
460
|
-
|
461
|
-
def method(name=nil)
|
462
|
-
#; [!084jo] returns current request method when argument is not specified.
|
463
|
-
#; [!gwskf] calls Object#method() when argument specified.
|
464
|
-
return name ? super : @method
|
465
|
-
end
|
466
|
-
|
467
|
-
def header(name)
|
468
|
-
#; [!1z7wj] returns http header value from environment.
|
469
|
-
return @env["HTTP_#{name.upcase.sub('-', '_')}"]
|
470
|
-
end
|
471
|
-
|
472
|
-
def request_method
|
473
|
-
#; [!y8eos] returns env['REQUEST_METHOD'] as string.
|
474
|
-
return @env['REQUEST_METHOD']
|
475
|
-
end
|
476
|
-
|
477
|
-
##--
|
478
|
-
#def get? ; @method == :GET ; end
|
479
|
-
#def post? ; @method == :POST ; end
|
480
|
-
#def put? ; @method == :PUT ; end
|
481
|
-
#def delete? ; @method == :DELETE ; end
|
482
|
-
#def head? ; @method == :HEAD ; end
|
483
|
-
#def patch? ; @method == :PATCH ; end
|
484
|
-
#def options? ; @method == :OPTIONS ; end
|
485
|
-
#def trace? ; @method == :TRACE ; end
|
486
|
-
##++
|
487
|
-
|
488
|
-
def script_name ; @env['SCRIPT_NAME' ] || ''; end # may be empty
|
489
|
-
def path_info ; @env['PATH_INFO' ] || ''; end # may be empty
|
490
|
-
def query_string ; @env['QUERY_STRING'] || ''; end # may be empty
|
491
|
-
def server_name ; @env['SERVER_NAME' ] ; end # should NOT be empty
|
492
|
-
def server_port ; @env['SERVER_PORT' ].to_i ; end # should NOT be empty
|
493
|
-
|
494
|
-
def content_type
|
495
|
-
#; [!95g9o] returns env['CONTENT_TYPE'].
|
496
|
-
return @env['CONTENT_TYPE']
|
497
|
-
end
|
498
|
-
|
499
|
-
def content_length
|
500
|
-
#; [!0wbek] returns env['CONTENT_LENGHT'] as integer.
|
501
|
-
len = @env['CONTENT_LENGTH']
|
502
|
-
return len ? len.to_i : len
|
503
|
-
end
|
504
|
-
|
505
|
-
def referer ; @env['HTTP_REFERER'] ; end
|
506
|
-
def user_agent ; @env['HTTP_USER_AGENT'] ; end
|
507
|
-
def x_requested_with ; @env['HTTP_X_REQUESTED_WITH']; end
|
508
|
-
|
509
|
-
def xhr?
|
510
|
-
#; [!hsgkg] returns true when 'X-Requested-With' header is 'XMLHttpRequest'.
|
511
|
-
return self.x_requested_with == 'XMLHttpRequest'
|
512
|
-
end
|
513
|
-
|
514
|
-
def client_ip_addr
|
515
|
-
#; [!e1uvg] returns 'X-Real-IP' header value if provided.
|
516
|
-
addr = @env['HTTP_X_REAL_IP'] # nginx
|
517
|
-
return addr if addr
|
518
|
-
#; [!qdlyl] returns first item of 'X-Forwarded-For' header if provided.
|
519
|
-
addr = @env['HTTP_X_FORWARDED_FOR'] # apache, squid, etc
|
520
|
-
return addr.split(',').first if addr
|
521
|
-
#; [!8nzjh] returns 'REMOTE_ADDR' if neighter 'X-Real-IP' nor 'X-Forwarded-For' provided.
|
522
|
-
addr = @env['REMOTE_ADDR'] # http standard
|
523
|
-
return addr
|
524
|
-
end
|
525
|
-
|
526
|
-
def scheme
|
527
|
-
#; [!jytwy] returns 'https' when env['HTTPS'] is 'on'.
|
528
|
-
return 'https' if @env['HTTPS'] == 'on'
|
529
|
-
#; [!zg8r2] returns env['rack.url_scheme'] ('http' or 'https').
|
530
|
-
return @env['rack.url_scheme']
|
531
|
-
end
|
532
|
-
|
533
|
-
def rack_version ; @env['rack.version'] ; end # ex: [1, 3]
|
534
|
-
def rack_url_scheme ; @env['rack.url_scheme'] ; end # ex: 'http' or 'https'
|
535
|
-
def rack_input ; @env['rack.input'] ; end # ex: $stdout
|
536
|
-
def rack_errors ; @env['rack.errors'] ; end # ex: $stderr
|
537
|
-
def rack_multithread ; @env['rack.multithread'] ; end # ex: true
|
538
|
-
def rack_multiprocess ; @env['rack.multiprocess'] ; end # ex: true
|
539
|
-
def rack_run_once ; @env['rack.run_once'] ; end # ex: false
|
540
|
-
def rack_session ; @env['rack.session'] ; end # ex: {}
|
541
|
-
def rack_logger ; @env['rack.logger'] ; end # ex: Logger.new
|
542
|
-
def rack_hijack ; @env['rack.hijack'] ; end # ex: callable object
|
543
|
-
def rack_hijack? ; @env['rack.hijack?'] ; end # ex: true or false
|
544
|
-
def rack_hijack_io ; @env['rack.hijack_io'] ; end # ex: socket object
|
545
|
-
|
546
|
-
def params_query
|
547
|
-
#; [!6ezqw] parses QUERY_STRING and returns it as Hash object.
|
548
|
-
#; [!o0ws7] unquotes both keys and values.
|
549
|
-
return @params_query ||= Util.parse_query_string(@env['QUERY_STRING'] || "")
|
550
|
-
end
|
551
|
-
alias query params_query
|
552
|
-
|
553
|
-
MAX_POST_SIZE = 10*1024*1024
|
554
|
-
MAX_MULTIPART_SIZE = 100*1024*1024
|
555
|
-
|
556
|
-
def params_form
|
557
|
-
d = @params_form
|
558
|
-
return d if d
|
559
|
-
#
|
560
|
-
d = @params_form = _parse_post_data(:form)
|
561
|
-
return d
|
562
|
-
end
|
563
|
-
alias form params_form
|
564
|
-
|
565
|
-
def params_multipart
|
566
|
-
d1 = @params_form
|
567
|
-
d2 = @params_file
|
568
|
-
return d1, d2 if d1 && d2
|
569
|
-
d1, d2 = _parse_post_data(:multipart)
|
570
|
-
@params_form = d1; @params_file = d2
|
571
|
-
return d1, d2
|
572
|
-
end
|
573
|
-
alias multipart params_multipart
|
574
|
-
|
575
|
-
def params_json
|
576
|
-
d = @params_json
|
577
|
-
return d if d
|
578
|
-
d = @params_json = _parse_post_data(:json)
|
579
|
-
return d
|
580
|
-
end
|
581
|
-
alias json params_json
|
582
|
-
|
583
|
-
def _parse_post_data(kind)
|
584
|
-
#; [!q88w9] raises error when content length is missing.
|
585
|
-
cont_len = @env['CONTENT_LENGTH'] or
|
586
|
-
raise HttpException.new(400, 'Content-Length header expected.')
|
587
|
-
#; [!gi4qq] raises error when content length is invalid.
|
588
|
-
cont_len =~ /\A\d+\z/ or
|
589
|
-
raise HttpException.new(400, 'Content-Length should be an integer.')
|
590
|
-
#
|
591
|
-
len = cont_len.to_i
|
592
|
-
case @env['CONTENT_TYPE']
|
593
|
-
#; [!59ad2] parses form parameters and returns it as Hash object when form requested.
|
594
|
-
when 'application/x-www-form-urlencoded'
|
595
|
-
kind == :form or
|
596
|
-
raise HttpException.new(400, 'unexpected form data (expected multipart).')
|
597
|
-
#; [!puxlr] raises error when content length is too long (> 10MB).
|
598
|
-
len <= MAX_POST_SIZE or
|
599
|
-
raise HttpException.new(400, 'Content-Length is too long.')
|
600
|
-
qstr = @env['rack.input'].read(len)
|
601
|
-
d = Util.parse_query_string(qstr)
|
602
|
-
return d
|
603
|
-
#; [!y1jng] parses multipart when multipart form requested.
|
604
|
-
when /\Amultipart\/form-data(?:;\s*boundary=(.*))?/
|
605
|
-
kind == :multipart or
|
606
|
-
raise HttpException.new(400, 'unexpected multipart data.')
|
607
|
-
boundary = $1 or
|
608
|
-
raise HttpException.new(400, 'bounday attribute of multipart required.')
|
609
|
-
#; [!mtx6t] raises error when content length of multipart is too long (> 100MB).
|
610
|
-
len <= MAX_MULTIPART_SIZE or
|
611
|
-
raise HttpException.new(400, 'Content-Length of multipart is too long.')
|
612
|
-
d1, d2 = Util.parse_multipart(@env['rack.input'], boundary, len, nil, nil)
|
613
|
-
return d1, d2
|
614
|
-
#; [!ugik5] parses json data and returns it as hash object when json data is sent.
|
615
|
-
when /\Aapplication\/json\b/
|
616
|
-
kind == :json or
|
617
|
-
raise HttpException.new(400, 'unexpected JSON data.')
|
618
|
-
json_str = @env['rack.input'].read(10*1024*1024) # TODO
|
619
|
-
d = JSON.parse(json_str)
|
620
|
-
#; [!p9ybb] raises error when not a form data.
|
621
|
-
else
|
622
|
-
raise HttpException.new(400, 'POST data expected, but not.')
|
623
|
-
end
|
624
|
-
end
|
625
|
-
private :_parse_post_data
|
626
|
-
|
627
|
-
def params
|
628
|
-
#; [!erlc7] parses QUERY_STRING when request method is GET or HEAD.
|
629
|
-
#; [!cr0zj] parses JSON when content type is 'application/json'.
|
630
|
-
#; [!j2lno] parses form parameters when content type is 'application/x-www-form-urlencoded'.
|
631
|
-
#; [!4rmn9] parses multipart when content type is 'multipart/form-data'.
|
632
|
-
if @method == :GET || @method == :HEAD
|
633
|
-
return params_query()
|
634
|
-
end
|
635
|
-
case @env['CONTENT_TYPE']
|
636
|
-
when /\Aapplication\/json\b/
|
637
|
-
return params_json()
|
638
|
-
when /\Aapplication\/x-www-form-urlencoded\b/
|
639
|
-
return params_form()
|
640
|
-
when /\Amultipart\/form-data\b/
|
641
|
-
return params_multipart()
|
642
|
-
else
|
643
|
-
return {}
|
644
|
-
end
|
645
|
-
end
|
646
|
-
|
647
|
-
def cookies
|
648
|
-
#; [!c9pwr] parses cookie data and returns it as hash object.
|
649
|
-
return @cookies ||= Util.parse_cookie_string(@env['HTTP_COOKIE'] || "")
|
650
|
-
end
|
651
|
-
|
652
|
-
def clear
|
653
|
-
#; [!0jdal] removes uploaded files.
|
654
|
-
d = nil
|
655
|
-
d.each {|_, uploaded| uploaded.clean() } if (d = @params_file)
|
656
|
-
end
|
657
|
-
|
663
|
+
class UnknownActionMethodError < BaseError
|
658
664
|
end
|
659
665
|
|
660
666
|
|
661
|
-
class
|
662
|
-
|
663
|
-
def initialize
|
664
|
-
@status_code = 200
|
665
|
-
@headers = {}
|
666
|
-
end
|
667
|
-
|
668
|
-
attr_accessor :status_code
|
669
|
-
attr_reader :headers
|
670
|
-
## for compatibility with Rack::Response
|
671
|
-
alias status status_code
|
672
|
-
alias status= status_code=
|
673
|
-
|
674
|
-
def content_type
|
675
|
-
return @headers['Content-Type']
|
676
|
-
end
|
677
|
-
|
678
|
-
def content_type=(content_type)
|
679
|
-
@headers['Content-Type'] = content_type
|
680
|
-
end
|
681
|
-
|
682
|
-
def content_length
|
683
|
-
s = @headers['Content-Length']
|
684
|
-
return s ? s.to_i : nil
|
685
|
-
end
|
686
|
-
|
687
|
-
def content_length=(length)
|
688
|
-
@headers['Content-Length'] = length.to_s
|
689
|
-
end
|
690
|
-
|
691
|
-
def set_cookie(name, value, domain: nil, path: nil, expires: nil, max_age: nil, httponly: nil, secure: nil)
|
692
|
-
s = "#{name}=#{value}"
|
693
|
-
s << "; Domain=#{domain}" if domain
|
694
|
-
s << "; Path=#{path}" if path
|
695
|
-
s << "; Expires=#{expires}" if expires
|
696
|
-
s << "; Max-Age=#{max_age}" if max_age
|
697
|
-
s << "; HttpOnly" if httponly
|
698
|
-
s << "; Secure" if secure
|
699
|
-
value = @headers['Set-Cookie']
|
700
|
-
@headers['Set-Cookie'] = value ? (value << "\n" << s) : s
|
701
|
-
return value
|
702
|
-
end
|
703
|
-
|
704
|
-
def clear
|
705
|
-
end
|
706
|
-
|
667
|
+
class UnknownContentError < BaseError
|
707
668
|
end
|
708
669
|
|
709
670
|
|
710
|
-
|
711
|
-
RESPONSE_CLASS = Response
|
712
|
-
|
713
|
-
def self.REQUEST_CLASS=(klass)
|
714
|
-
#; [!7uqb4] changes default request class.
|
715
|
-
remove_const :REQUEST_CLASS
|
716
|
-
const_set :REQUEST_CLASS, klass
|
717
|
-
end
|
718
|
-
|
719
|
-
def self.RESPONSE_CLASS=(klass)
|
720
|
-
#; [!c1bd0] changes default response class.
|
721
|
-
remove_const :RESPONSE_CLASS
|
722
|
-
const_set :RESPONSE_CLASS, klass
|
671
|
+
class PayloadParseError < BaseError
|
723
672
|
end
|
724
673
|
|
725
674
|
|
@@ -730,20 +679,19 @@ module K8
|
|
730
679
|
#; [!uotpb] accepts request and response objects.
|
731
680
|
@req = req
|
732
681
|
@resp = resp
|
733
|
-
#; [!7sfyf] sets session object.
|
734
|
-
@sess = req.env['rack.session']
|
735
682
|
end
|
736
683
|
|
737
684
|
attr_reader :req, :resp, :sess
|
738
685
|
|
739
|
-
def handle_action(
|
740
|
-
@
|
686
|
+
def handle_action(action_name, urlpath_args)
|
687
|
+
@action_name = action_name
|
688
|
+
@action_args = urlpath_args
|
741
689
|
ex = nil
|
742
690
|
begin
|
743
691
|
#; [!5jnx6] calls '#before_action()' before handling request.
|
744
692
|
before_action()
|
745
693
|
#; [!ddgx3] invokes action method with urlpath params.
|
746
|
-
content = invoke_action(
|
694
|
+
content = invoke_action(action_name, urlpath_args)
|
747
695
|
#; [!aqa4e] returns content.
|
748
696
|
return handle_content(content)
|
749
697
|
rescue Exception => ex
|
@@ -763,8 +711,8 @@ module K8
|
|
763
711
|
def after_action(ex)
|
764
712
|
end
|
765
713
|
|
766
|
-
def invoke_action(
|
767
|
-
return self.__send__(
|
714
|
+
def invoke_action(action_name, urlpath_args)
|
715
|
+
return self.__send__(action_name, *urlpath_args)
|
768
716
|
end
|
769
717
|
|
770
718
|
def handle_content(content)
|
@@ -776,32 +724,39 @@ module K8
|
|
776
724
|
## mapping '/', :GET=>:do_index, :POST=>:do_create
|
777
725
|
## mapping '/{id}', :GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete
|
778
726
|
##
|
779
|
-
def self.mapping(urlpath_pattern,
|
727
|
+
def self.mapping(urlpath_pattern, action_methods={})
|
728
|
+
action_methods.each do |req_meth, action_name|
|
729
|
+
HTTP_REQUEST_METHODS[req_meth.to_s] or
|
730
|
+
raise ArgumentError.new("#{req_meth.inspect}: unknown request method.")
|
731
|
+
req_meth.is_a?(Symbol) or
|
732
|
+
raise ArgumentError.new("#{req_meth.inspect}: should be a Symbol but got #{req_meth.class.name}")
|
733
|
+
end
|
780
734
|
#; [!o148k] maps urlpath pattern and request methods.
|
781
|
-
self.
|
735
|
+
self._mappings << [urlpath_pattern, action_methods]
|
736
|
+
return self
|
782
737
|
end
|
783
738
|
|
784
|
-
def self.
|
785
|
-
return @action_method_mapping ||=
|
739
|
+
def self._mappings
|
740
|
+
return @action_method_mapping ||= []
|
786
741
|
end
|
787
742
|
|
788
743
|
def self._build_action_info(full_urlpath_pattern) # :nodoc:
|
789
744
|
#; [!ordhc] build ActionInfo objects for each action methods.
|
790
745
|
parent = full_urlpath_pattern
|
791
|
-
|
792
|
-
|
793
|
-
|
746
|
+
d = {}
|
747
|
+
self._mappings.each do |urlpath_pat, action_methods|
|
748
|
+
action_methods.each do |req_meth, action_name|
|
794
749
|
info = ActionInfo.create(req_meth, "#{parent}#{urlpath_pat}")
|
795
|
-
|
750
|
+
d[action_name] = info
|
796
751
|
end
|
797
752
|
end
|
798
|
-
@action_infos
|
753
|
+
@action_infos = d
|
799
754
|
end
|
800
755
|
|
801
|
-
def self.[](
|
756
|
+
def self.[](action_name)
|
802
757
|
#; [!1tq8z] returns ActionInfo object corresponding to action method.
|
803
758
|
#; [!6g2iw] returns nil when not mounted yet.
|
804
|
-
return (@action_infos || {})[
|
759
|
+
return (@action_infos || {})[action_name]
|
805
760
|
end
|
806
761
|
|
807
762
|
end
|
@@ -817,9 +772,15 @@ module K8
|
|
817
772
|
alias response resp # just for compatibility with other frameworks; use @resp!
|
818
773
|
alias session sess # just for compatibility with other frameworks; use @sess!
|
819
774
|
|
820
|
-
|
821
|
-
|
822
|
-
|
775
|
+
def initialize(req, res)
|
776
|
+
super
|
777
|
+
#; [!7sfyf] sets session object.
|
778
|
+
@sess = req.env['rack.session']
|
779
|
+
end
|
780
|
+
|
781
|
+
protected
|
782
|
+
|
783
|
+
def before_action
|
823
784
|
csrf_protection() if csrf_protection_required?()
|
824
785
|
end
|
825
786
|
|
@@ -833,7 +794,7 @@ module K8
|
|
833
794
|
end
|
834
795
|
end
|
835
796
|
|
836
|
-
def invoke_action(
|
797
|
+
def invoke_action(action_name, urlpath_args)
|
837
798
|
begin
|
838
799
|
return super
|
839
800
|
#; [!d5v0l] handles exception when handler method defined.
|
@@ -942,7 +903,7 @@ module K8
|
|
942
903
|
return false if @req.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
|
943
904
|
#; [!vwrqv] returns true when request method is one of POST, PUT, or DELETE.
|
944
905
|
#; [!jfhla] returns true when request method is GET or HEAD.
|
945
|
-
req_meth = @req.
|
906
|
+
req_meth = @req.meth
|
946
907
|
return req_meth == :POST || req_meth == :PUT || req_meth == :DELETE
|
947
908
|
end
|
948
909
|
|
@@ -1032,27 +993,28 @@ module K8
|
|
1032
993
|
class ActionInfo
|
1033
994
|
|
1034
995
|
def initialize(method, urlpath_format)
|
1035
|
-
@
|
996
|
+
@meth = method
|
1036
997
|
@urlpath_format = urlpath_format # ex: '/books/%s/comments/%s'
|
1037
998
|
end
|
1038
999
|
|
1039
|
-
attr_reader :
|
1000
|
+
attr_reader :meth
|
1040
1001
|
|
1041
|
-
|
1042
|
-
|
1002
|
+
## (experimental; use #meth instead)
|
1003
|
+
def method(name=nil) # :nodoc:
|
1004
|
+
return name ? super : @meth
|
1043
1005
|
end
|
1044
1006
|
|
1045
|
-
def
|
1007
|
+
def path(*args)
|
1046
1008
|
return @urlpath_format % args
|
1047
1009
|
end
|
1048
1010
|
|
1049
1011
|
def form_action_attr(*args)
|
1050
1012
|
#; [!qyhkm] returns '/api/books/123' when method is POST.
|
1051
1013
|
#; [!kogyx] returns '/api/books/123?_method=PUT' when method is not POST.
|
1052
|
-
if @
|
1053
|
-
return
|
1014
|
+
if @meth == 'POST'
|
1015
|
+
return path(*args)
|
1054
1016
|
else
|
1055
|
-
return "#{
|
1017
|
+
return "#{path(*args)}?_method=#{@meth}"
|
1056
1018
|
end
|
1057
1019
|
end
|
1058
1020
|
|
@@ -1079,153 +1041,252 @@ module K8
|
|
1079
1041
|
end
|
1080
1042
|
|
1081
1043
|
class ActionInfo0 < ActionInfo # :nodoc:
|
1082
|
-
def
|
1044
|
+
def path(); @urlpath_format; end
|
1083
1045
|
end
|
1084
1046
|
|
1085
1047
|
class ActionInfo1 < ActionInfo # :nodoc:
|
1086
|
-
def
|
1048
|
+
def path(a); @urlpath_format % [a]; end
|
1087
1049
|
end
|
1088
1050
|
|
1089
1051
|
class ActionInfo2 < ActionInfo # :nodoc:
|
1090
|
-
def
|
1052
|
+
def path(a, b); @urlpath_format % [a, b]; end
|
1091
1053
|
end
|
1092
1054
|
|
1093
1055
|
class ActionInfo3 < ActionInfo # :nodoc:
|
1094
|
-
def
|
1056
|
+
def path(a, b, c); @urlpath_format % [a, b, c]; end
|
1095
1057
|
end
|
1096
1058
|
|
1097
1059
|
class ActionInfo4 < ActionInfo # :nodoc:
|
1098
|
-
def
|
1060
|
+
def path(a, b, c, d); @urlpath_format % [a, b, c, d]; end
|
1099
1061
|
end
|
1100
1062
|
|
1101
1063
|
ActionInfo::SUBCLASSES << ActionInfo0 << ActionInfo1 << ActionInfo2 << ActionInfo3 << ActionInfo4
|
1102
1064
|
|
1103
1065
|
|
1104
|
-
class
|
1066
|
+
class ActionMapping
|
1105
1067
|
|
1106
|
-
def initialize
|
1107
|
-
|
1068
|
+
def initialize(urlpath_mapping, urlpath_cache_size: 0,
|
1069
|
+
enable_urlpath_param_range: true)
|
1070
|
+
#; [!34o67] keyword arg 'enable_urlpath_param_range' controls to generate range object or not.
|
1071
|
+
@enable_urlpath_param_range = enable_urlpath_param_range
|
1072
|
+
#; [!buj0d] prepares LRU cache if cache size specified.
|
1073
|
+
@urlpath_cache_size = urlpath_cache_size
|
1074
|
+
@urlpath_lru_cache = urlpath_cache_size > 0 ? {} : nil
|
1075
|
+
#; [!wsz8g] compiles urlpath mapping passed.
|
1076
|
+
@fixed_endpoints = {} # urlpath patterns which have no urlpath params
|
1077
|
+
@variable_endpoints = [] # urlpath patterns which have any ulrpath param
|
1078
|
+
@all_endpoints = [] # all urlpath patterns (fixed + variable)
|
1079
|
+
@urlpath_rexp = build(urlpath_mapping)
|
1108
1080
|
end
|
1109
1081
|
|
1110
|
-
|
1111
|
-
#; [!yfsom] registers urlpath param name, default pattern and converter block.
|
1112
|
-
@patterns << [urlpath_param_name, default_pattern, converter]
|
1113
|
-
self
|
1114
|
-
end
|
1082
|
+
attr_reader :urlpath_rexp
|
1115
1083
|
|
1116
|
-
|
1117
|
-
#; [!3gplv] deletes matched record.
|
1118
|
-
@patterns.delete_if {|tuple| tuple[0] == urlpath_param_name }
|
1119
|
-
self
|
1120
|
-
end
|
1084
|
+
private
|
1121
1085
|
|
1122
|
-
def
|
1123
|
-
#; [!
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1086
|
+
def build(urlpath_mapping)
|
1087
|
+
#; [!6f3vl] compiles urlpath mapping.
|
1088
|
+
empty_pargs = [].freeze
|
1089
|
+
rexp_str = traverse(urlpath_mapping, "") do |full_urlpath, action_class, action_methods|
|
1090
|
+
#; [!z2iax] classifies urlpath contains any parameter as variable one.
|
1091
|
+
if has_urlpath_param?(full_urlpath)
|
1092
|
+
pattern, pnames, procs = compile_urlpath(full_urlpath, true)
|
1093
|
+
rexp = Regexp.compile("\\A#{pattern}\\z")
|
1094
|
+
range = @enable_urlpath_param_range ? range_of_urlpath_param(full_urlpath) : nil
|
1095
|
+
tuple = [full_urlpath, action_class, action_methods, rexp, pnames, procs, range]
|
1096
|
+
@variable_endpoints << tuple
|
1097
|
+
#; [!rvdes] classifies urlpath contains no parameters as fixed one.
|
1098
|
+
else
|
1099
|
+
tuple = [full_urlpath, action_class, action_methods, empty_pargs]
|
1100
|
+
@fixed_endpoints[full_urlpath] = tuple
|
1101
|
+
end
|
1102
|
+
#
|
1103
|
+
@all_endpoints << tuple
|
1127
1104
|
end
|
1128
|
-
return
|
1105
|
+
return Regexp.compile("\\A#{rexp_str}\\z")
|
1129
1106
|
end
|
1130
1107
|
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1108
|
+
def traverse(urlpath_mapping, base_urlpath="", &block)
|
1109
|
+
buf = []
|
1110
|
+
urlpath_mapping.each do |urlpath, target|
|
1111
|
+
curr_urlpath = "#{base_urlpath}#{urlpath}"
|
1112
|
+
rexp_str = nil
|
1113
|
+
#; [!w45ad] can compile nested array.
|
1114
|
+
if target.is_a?(Array)
|
1115
|
+
rexp_str = traverse(target, curr_urlpath, &block)
|
1116
|
+
#; [!wd2eb] accepts subclass of Action class.
|
1117
|
+
else
|
1118
|
+
#; [!l2kz5] requires library when filepath and classname specified.
|
1119
|
+
klass = target.is_a?(String) ? require_action_class(target) : target
|
1120
|
+
#; [!irt5g] raises TypeError when unknown object specified.
|
1121
|
+
klass.is_a?(Class) && klass < BaseAction or
|
1122
|
+
raise TypeError.new("Action class or nested array expected, but got #{klass.inspect}")
|
1123
|
+
#; [!6xwhq] builds action infos for each action methods.
|
1124
|
+
action_class = klass
|
1125
|
+
action_class._build_action_info(curr_urlpath)
|
1126
|
+
#
|
1127
|
+
buf2 = []
|
1128
|
+
action_class._mappings.each do |upath, action_methods|
|
1129
|
+
validate(action_class, action_methods)
|
1130
|
+
#; [!m51yy] regards '.*' at end of urlpath pattern as extension.
|
1131
|
+
upath = upath.sub(/\.\*\z/, '{_:<(?:\.\w+)?>}')
|
1132
|
+
#
|
1133
|
+
full_urlpath = "#{curr_urlpath}#{upath}"
|
1134
|
+
if has_urlpath_param?(full_urlpath)
|
1135
|
+
buf2 << "#{compile_urlpath(upath)[0]}(\\z)" # ex: /{id} -> /\d+(\z)
|
1136
|
+
end
|
1137
|
+
yield full_urlpath, action_class, action_methods
|
1138
|
+
end
|
1139
|
+
n = buf2.length
|
1140
|
+
rexp_str = (n == 0 ? nil : n == 1 ? buf2[0] : union_urlpaths(buf2))
|
1141
|
+
end
|
1142
|
+
buf << "#{compile_urlpath(urlpath)[0]}#{rexp_str}" if rexp_str
|
1143
|
+
end
|
1144
|
+
#; [!169ad] removes unnecessary grouping.
|
1145
|
+
n = buf.length
|
1146
|
+
rexp_string = (n == 0 ? nil : n == 1 ? buf[0] : "(?:#{buf.join('|')})")
|
1147
|
+
return rexp_string # ex: '/books/\d+(?:(\z)|/edit(\z))'
|
1148
|
+
end
|
1152
1149
|
|
1150
|
+
def union_urlpaths(upaths)
|
1151
|
+
#; [!abj34] ex: (?:/\d+(\z)|/\d+/edit(\z)) -> /d+(?:(\z)|/edit(\z))
|
1152
|
+
prefixes = URLPATH_PARAM_PREFIXES # ex: ['/\d+', '/[^/]+']
|
1153
|
+
prefix = prefixes.find {|s| upaths.all? {|x| x.start_with?(s) } }
|
1154
|
+
upaths = upaths.map {|x| x[prefix.length..-1] } if prefix
|
1155
|
+
return "#{prefix}(?:#{upaths.join('|')})"
|
1156
|
+
end
|
1153
1157
|
|
1154
|
-
|
1158
|
+
#; [!92jcn] '{' and '}' are available in urlpath param pattern.
|
1159
|
+
#; [!do1zi] param type is optional (ex: '{id}' or '{id:<\d+>}').
|
1160
|
+
#; [!my6as] param pattern is optional (ex: '{id}' or '{id:int}').
|
1161
|
+
URLPATH_PARAM_REXP = /\{(\w*)(?::(\w*))?(?:<(.*?)>)?\}/
|
1155
1162
|
|
1156
|
-
def
|
1157
|
-
|
1163
|
+
def has_urlpath_param?(urlpath)
|
1164
|
+
return urlpath.include?('{')
|
1158
1165
|
end
|
1159
1166
|
|
1160
|
-
##
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1167
|
+
## ex: '/books/{id}', true -> ['/books/(\d+)', [['id', 'int', proc{|x| x.to_i}]]]
|
1168
|
+
def compile_urlpath(urlpath_pat, enable_capture=false)
|
1169
|
+
#; [!iln54] param names and conveter procs are nil when no urlpath params.
|
1170
|
+
pnames = nil # urlpath parameter names (ex: ['id'])
|
1171
|
+
procs = nil # proc objects to convert parameter value (ex: [proc{|x| x.to_i}])
|
1172
|
+
#
|
1173
|
+
rexp_str = urlpath_pat.gsub(URLPATH_PARAM_REXP) {
|
1174
|
+
#; [!3diea] '{id:<\d+>}' is ok but '{id<\d+>}' raises error.
|
1175
|
+
pname, ptype, pat = $1, $2, $3
|
1176
|
+
if ! ptype && pat # when ':ptype' is missing but '<pat>' exists
|
1177
|
+
raise ActionMappingError.new("'#{urlpath_pat}': missing ':' between param name and pattern (ex: '{id:<\\d+>}' is OK but '{id<\\d+>}' is not).")
|
1178
|
+
end
|
1179
|
+
ptype, pat, proc_ = resolve_param_type(pname, ptype, pat, urlpath_pat)
|
1180
|
+
#; [!lhtiz] skips empty param name.
|
1181
|
+
#; [!66zas] skips param name starting with '_'.
|
1182
|
+
skip = pname.empty? || pname.start_with?('_')
|
1183
|
+
pnames ||= []; pnames << pname unless skip
|
1184
|
+
procs ||= []; procs << proc_ unless skip
|
1185
|
+
#; [!bi7gr] captures urlpath params when 2nd argument is truthy.
|
1186
|
+
#; [!mprbx] ex: '/{id:x|y}' -> '/(x|y)', '/{:x|y}' -> '/(?:x|y)'
|
1187
|
+
if enable_capture && ! skip
|
1188
|
+
pat = "(#{pat})"
|
1189
|
+
elsif pat =~ /\|/
|
1190
|
+
pat = "(?:#{pat})"
|
1191
|
+
end
|
1192
|
+
pat
|
1193
|
+
}
|
1194
|
+
#; [!awfgs] returns regexp string, param names, and converter procs.
|
1195
|
+
return rexp_str, pnames, procs # ex: '/books/(\d+)', ['id'], [proc{|x| x.to_i}]}]
|
1171
1196
|
end
|
1172
1197
|
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1198
|
+
_to_date = proc {|s|
|
1199
|
+
begin
|
1200
|
+
yr, mo, dy = s.split('-')
|
1201
|
+
Date.new(yr.to_i, mo.to_i, dy.to_i)
|
1202
|
+
rescue
|
1203
|
+
raise HttpException.new(404)
|
1204
|
+
end
|
1205
|
+
}
|
1206
|
+
URLPATH_PARAM_TYPES = [
|
1207
|
+
# ptype , pname regexp , urlpath pattern , converter
|
1208
|
+
['int' , /(?:^|_)id\z/ , '\d+' , proc {|s| s.to_i }],
|
1209
|
+
['date' , /(?:^|_)date\z/ , '\d\d\d\d-\d\d-\d\d' , _to_date ],
|
1210
|
+
['str' , nil , '[^/]+' , nil ],
|
1211
|
+
]
|
1212
|
+
_to_date = nil
|
1213
|
+
URLPATH_PARAM_PREFIXES = URLPATH_PARAM_TYPES.collect {|t| "/#{t[2]}" }
|
1214
|
+
|
1215
|
+
def resolve_param_type(pname, ptype, pattern, urlpath)
|
1216
|
+
tuple = nil
|
1217
|
+
if ! ptype || ptype.empty?
|
1218
|
+
tuple = URLPATH_PARAM_TYPES.find {|t| pname =~ t[1] }
|
1219
|
+
ptype = tuple ? tuple[0] : 'str'
|
1180
1220
|
end
|
1181
|
-
|
1221
|
+
tuple ||= URLPATH_PARAM_TYPES.find {|t| t[0] == ptype } or
|
1222
|
+
raise ActionMappingError.new("'#{urlpath}': unknown param type '#{ptype}'.")
|
1223
|
+
pattern = tuple[2] if ! pattern || pattern.empty?
|
1224
|
+
converter = tuple[3] # ex: '123' -> 123, '2000-01-01' -> Date.new(2000, 1, 1)
|
1225
|
+
return ptype, pattern, converter
|
1182
1226
|
end
|
1183
|
-
private :_normalize
|
1184
1227
|
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1228
|
+
## range object to retrieve urlpath parameter value faster than Regexp matching
|
1229
|
+
## ex:
|
1230
|
+
## urlpath_pat == '/books/{id}/edit'
|
1231
|
+
## range = _range_of_urlpath_param(urlpath_pat)
|
1232
|
+
## p range #=> 7..-6 (Range object)
|
1233
|
+
## p "/books/123/edit"[range] #=> '123'
|
1234
|
+
def range_of_urlpath_param(urlpath)
|
1235
|
+
i = 0
|
1236
|
+
m = nil
|
1237
|
+
urlpath.scan(URLPATH_PARAM_REXP) do
|
1238
|
+
i += 1
|
1239
|
+
m = Regexp.last_match
|
1189
1240
|
end
|
1190
|
-
|
1241
|
+
return nil unless i == 1
|
1242
|
+
return m.begin(0) .. (m.end(0) - urlpath.length - 1) # ex: 7..-6 (Range object)
|
1191
1243
|
end
|
1192
1244
|
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
#; [!
|
1207
|
-
|
1245
|
+
## ex: './api/admin/books:Admin::BookAPI' -> Admin::BookAPI
|
1246
|
+
def require_action_class(filepath_and_classname)
|
1247
|
+
#; [!px9jy] requires file and finds class object.
|
1248
|
+
str = filepath_and_classname # ex: './admin/api/book:Admin::BookAPI'
|
1249
|
+
filepath, classname = filepath_and_classname.split(':', 2)
|
1250
|
+
begin
|
1251
|
+
require filepath
|
1252
|
+
rescue LoadError => ex
|
1253
|
+
#; [!dlcks] don't rescue LoadError when it is not related to argument.
|
1254
|
+
raise unless ex.path == filepath
|
1255
|
+
#; [!mngjz] raises error when failed to load file.
|
1256
|
+
raise LoadError.new("'#{str}': cannot load '#{filepath}'.")
|
1257
|
+
end
|
1258
|
+
#; [!8n6pf] class name may have module prefix name.
|
1259
|
+
#; [!6lv7l] raises error when action class not found.
|
1260
|
+
begin
|
1261
|
+
klass = classname.split('::').inject(Object) {|cls, x| cls.const_get(x) }
|
1262
|
+
rescue NameError
|
1263
|
+
raise NameError.new("'#{str}': class not found (#{classname}).")
|
1264
|
+
end
|
1265
|
+
#; [!thf7t] raises TypeError when not a class.
|
1266
|
+
klass.is_a?(Class) or
|
1267
|
+
raise TypeError.new("'#{str}': class name expected but got #{klass.inspect}.")
|
1268
|
+
#; [!yqcgx] raises TypeError when not a subclass of K8::Action.
|
1269
|
+
klass < Action or
|
1270
|
+
raise TypeError.new("'#{str}': expected subclass of K8::Action but not.")
|
1271
|
+
#
|
1272
|
+
return klass
|
1208
1273
|
end
|
1209
1274
|
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
return self
|
1275
|
+
## raises error when action method is not defined in action class
|
1276
|
+
def validate(action_class, action_methods)
|
1277
|
+
#; [!ue766] raises error when action method is not defined in action class.
|
1278
|
+
action_methods.each do |req_meth, action_name|
|
1279
|
+
action_class.method_defined?(action_name) or
|
1280
|
+
raise UnknownActionMethodError.new("#{req_meth.inspect}=>#{action_name.inspect}: unknown action method in #{action_class}.")
|
1281
|
+
end
|
1218
1282
|
end
|
1219
1283
|
|
1220
|
-
|
1284
|
+
public
|
1221
1285
|
|
1222
|
-
def
|
1286
|
+
def find(req_urlpath)
|
1223
1287
|
#; [!j34yh] finds from fixed urlpaths at first.
|
1224
|
-
|
1225
|
-
|
1226
|
-
pnames = pvalues = EMPTY_ARRAY
|
1227
|
-
return action_class, action_methods, pnames, pvalues
|
1228
|
-
end
|
1288
|
+
tuple = @fixed_endpoints[req_urlpath]
|
1289
|
+
return tuple[1..-1] if tuple # ex: [BooksAction, {:GET=>:do_index}, []]
|
1229
1290
|
#; [!uqwr7] uses LRU as cache algorithm.
|
1230
1291
|
cache = @urlpath_lru_cache
|
1231
1292
|
if cache && (result = cache.delete(req_urlpath))
|
@@ -1256,8 +1317,8 @@ module K8
|
|
1256
1317
|
else ; procs.zip(strs).map {|pr, v| pr ? pr.call(v) : v }
|
1257
1318
|
end # ex: ["123"] -> [123]
|
1258
1319
|
end
|
1259
|
-
#; [!jyxlm] returns action class
|
1260
|
-
result = [action_class, action_methods,
|
1320
|
+
#; [!jyxlm] returns action class, action methods and urlpath param args.
|
1321
|
+
result = [action_class, action_methods, pvalues]
|
1261
1322
|
#; [!uqwr7] stores result into cache if cache is enabled.
|
1262
1323
|
if cache
|
1263
1324
|
cache[req_urlpath] = result
|
@@ -1279,162 +1340,292 @@ module K8
|
|
1279
1340
|
self
|
1280
1341
|
end
|
1281
1342
|
|
1282
|
-
|
1343
|
+
end
|
1283
1344
|
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
klass = _require_action_class(target)
|
1298
|
-
buf << _compile_class(klass, curr_urlpath_pat, child)
|
1299
|
-
#; [!irt5g] raises TypeError when unknown object specified.
|
1300
|
-
else
|
1301
|
-
raise TypeError.new("Action class or nested array expected, but got #{target.inspect}")
|
1302
|
-
end
|
1303
|
-
end
|
1304
|
-
#; [!bcgc9] skips classes which have only fixed urlpaths.
|
1305
|
-
buf.compact!
|
1306
|
-
rexp_str = _build_rexp_str(urlpath_pat, buf)
|
1307
|
-
return rexp_str # ex: '/api(?:/books/\d+(\z)|/authors/\d+(\z))'
|
1345
|
+
|
1346
|
+
class RackRequest
|
1347
|
+
|
1348
|
+
def initialize(env)
|
1349
|
+
#; [!yb9k9] sets @env.
|
1350
|
+
@env = env
|
1351
|
+
#; [!yo22o] sets @meth as Symbol value.
|
1352
|
+
@meth = HTTP_REQUEST_METHODS[env['REQUEST_METHOD']] or
|
1353
|
+
raise HTTPException.new(400, "#{env['REQUEST_METHOD'].inspect}: unknown request method.")
|
1354
|
+
#; [!twgmi] sets @path.
|
1355
|
+
@path = (x = env['PATH_INFO'])
|
1356
|
+
#; [!ae8ws] uses SCRIPT_NAME as urlpath when PATH_INFO is not provided.
|
1357
|
+
@path = env['SCRIPT_NAME'] if x.nil? || x.empty?
|
1308
1358
|
end
|
1309
1359
|
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
fullpath_pat = "#{curr_urlpath_pat}#{child_urlpath_pat}"
|
1318
|
-
rexp_str, pnames, procs = _compile_urlpath_pat(fullpath_pat, true)
|
1319
|
-
#; [!z2iax] classifies urlpath contains any parameter as variable one.
|
1320
|
-
if pnames
|
1321
|
-
fullpath_rexp = Regexp.compile("\\A#{rexp_str}\\z")
|
1322
|
-
range = @enable_urlpath_param_range ? _range_of_urlpath_param(fullpath_pat) : nil
|
1323
|
-
tuple = [fullpath_pat, action_class, methods, fullpath_rexp, pnames.freeze, procs, range]
|
1324
|
-
@variable_endpoints << tuple
|
1325
|
-
buf << (_compile_urlpath_pat(child_urlpath_pat).first << '(\z)')
|
1326
|
-
#; [!rvdes] classifies urlpath contains no parameters as fixed one.
|
1327
|
-
else
|
1328
|
-
tuple = [fullpath_pat, action_class, methods]
|
1329
|
-
@fixed_endpoints[fullpath_pat] = tuple
|
1330
|
-
end
|
1331
|
-
#
|
1332
|
-
@all_endpoints << tuple
|
1333
|
-
end
|
1334
|
-
#; [!6xwhq] builds action infos for each action methods.
|
1335
|
-
action_class._build_action_info(curr_urlpath_pat) if action_class
|
1336
|
-
#
|
1337
|
-
rexp_str = _build_rexp_str(urlpath_pat, buf)
|
1338
|
-
return rexp_str # ex: '/books(?:/\d+(\z)|/\d+/edit(\z))'
|
1360
|
+
attr_reader :env, :meth, :path
|
1361
|
+
|
1362
|
+
## (experimental; use #meth instead)
|
1363
|
+
def method(name=nil) # :nodoc:
|
1364
|
+
#; [!084jo] returns current request method when argument is not specified.
|
1365
|
+
#; [!gwskf] calls Object#method() when argument specified.
|
1366
|
+
return name ? super : @meth
|
1339
1367
|
end
|
1340
1368
|
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
#; [!169ad] removes unnecessary grouping.
|
1346
|
-
return "#{prefix}#{buf.first}" if buf.length == 1
|
1347
|
-
return "#{prefix}(?:#{buf.join('|')})"
|
1348
|
-
ensure
|
1349
|
-
buf.clear() # for GC
|
1369
|
+
def path_ext
|
1370
|
+
#; [!tf6yz] returns extension of request path such as '.html' or '.json'.
|
1371
|
+
#; [!xnurj] returns empty string when no extension.
|
1372
|
+
return File.extname(@path)
|
1350
1373
|
end
|
1351
1374
|
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
URLPATH_PARAM_REXP = /\{(\w*)(?::([^{}]*?(?:\{[^{}]*?\}[^{}]*?)*))?\}/
|
1356
|
-
URLPATH_PARAM_REXP_NOGROUP = /\{(?:\w*)(?::(?:[^{}]*?(?:\{[^{}]*?\}[^{}]*?)*))?\}/
|
1357
|
-
proc {|rx1, rx2|
|
1358
|
-
rx1.source.gsub(/\(([^?])/, '(?:\1') == rx2.source or
|
1359
|
-
raise "*** assertion failed: #{rx1.source.inspect} != #{rx2.source.inspect}"
|
1360
|
-
}.call(URLPATH_PARAM_REXP, URLPATH_PARAM_REXP_NOGROUP)
|
1361
|
-
|
1362
|
-
## ex: '/books/{id}', true -> ['/books/(\d+)', ['id'], [proc{|x| x.to_i}]]
|
1363
|
-
def _compile_urlpath_pat(urlpath_pat, enable_capture=false)
|
1364
|
-
#; [!iln54] param names and conveter procs are nil when no urlpath params.
|
1365
|
-
pnames = nil # urlpath parameter names (ex: ['id'])
|
1366
|
-
procs = nil # proc objects to convert parameter value (ex: [proc{|x| x.to_i}])
|
1367
|
-
#
|
1368
|
-
rexp_str = urlpath_pat.gsub(URLPATH_PARAM_REXP) {
|
1369
|
-
pname, s, proc_ = $1, $2, nil
|
1370
|
-
s, proc_ = @default_patterns.lookup(pname) if s.nil?
|
1371
|
-
#; [!lhtiz] skips empty param name.
|
1372
|
-
#; [!66zas] skips param name starting with '_'.
|
1373
|
-
skip = pname.empty? || pname.start_with?('_')
|
1374
|
-
pnames ||= []; pnames << pname unless skip
|
1375
|
-
procs ||= []; procs << proc_ unless skip
|
1376
|
-
#; [!bi7gr] captures urlpath params when 2nd argument is truthy.
|
1377
|
-
#; [!mprbx] ex: '/{id:x|y}' -> '/(x|y)', '/{:x|y}' -> '/(?:x|y)'
|
1378
|
-
if enable_capture && ! skip
|
1379
|
-
s = "(#{s})"
|
1380
|
-
elsif s =~ /\|/
|
1381
|
-
s = "(?:#{s})"
|
1382
|
-
end
|
1383
|
-
s
|
1384
|
-
}
|
1385
|
-
#; [!awfgs] returns regexp string, param names, and converter procs.
|
1386
|
-
return rexp_str, pnames, procs # ex: '/books/(\d+)', ['id'], [proc{|x| x.to_i}]}]
|
1375
|
+
def header(name)
|
1376
|
+
#; [!1z7wj] returns http header value from environment.
|
1377
|
+
return @env["HTTP_#{name.upcase.sub('-', '_')}"]
|
1387
1378
|
end
|
1388
1379
|
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
action_class.method_defined?(action_method_name) or
|
1393
|
-
raise UnknownActionMethodError.new("#{req_meth.inspect}=>#{action_method_name.inspect}: unknown action method in #{action_class}.")
|
1394
|
-
end
|
1380
|
+
def request_method
|
1381
|
+
#; [!y8eos] returns env['REQUEST_METHOD'] as string.
|
1382
|
+
return @env['REQUEST_METHOD']
|
1395
1383
|
end
|
1396
1384
|
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
1402
|
-
|
1403
|
-
def
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1385
|
+
##--
|
1386
|
+
#def get? ; @meth == :GET ; end
|
1387
|
+
#def post? ; @meth == :POST ; end
|
1388
|
+
#def put? ; @meth == :PUT ; end
|
1389
|
+
#def delete? ; @meth == :DELETE ; end
|
1390
|
+
#def head? ; @meth == :HEAD ; end
|
1391
|
+
#def patch? ; @meth == :PATCH ; end
|
1392
|
+
#def options? ; @meth == :OPTIONS ; end
|
1393
|
+
#def trace? ; @meth == :TRACE ; end
|
1394
|
+
##++
|
1395
|
+
|
1396
|
+
def script_name ; @env['SCRIPT_NAME' ] || ''; end # may be empty
|
1397
|
+
def path_info ; @env['PATH_INFO' ] || ''; end # may be empty
|
1398
|
+
def query_string ; @env['QUERY_STRING'] || ''; end # may be empty
|
1399
|
+
def server_name ; @env['SERVER_NAME' ] ; end # should NOT be empty
|
1400
|
+
def server_port ; @env['SERVER_PORT' ].to_i ; end # should NOT be empty
|
1401
|
+
|
1402
|
+
def content_type
|
1403
|
+
#; [!95g9o] returns env['CONTENT_TYPE'].
|
1404
|
+
return @env['CONTENT_TYPE']
|
1408
1405
|
end
|
1409
1406
|
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1407
|
+
def content_length
|
1408
|
+
#; [!0wbek] returns env['CONTENT_LENGHT'] as integer.
|
1409
|
+
len = @env['CONTENT_LENGTH']
|
1410
|
+
return len ? len.to_i : len
|
1411
|
+
end
|
1412
|
+
|
1413
|
+
def referer ; @env['HTTP_REFERER'] ; end
|
1414
|
+
def user_agent ; @env['HTTP_USER_AGENT'] ; end
|
1415
|
+
def x_requested_with ; @env['HTTP_X_REQUESTED_WITH']; end
|
1416
|
+
|
1417
|
+
def xhr?
|
1418
|
+
#; [!hsgkg] returns true when 'X-Requested-With' header is 'XMLHttpRequest'.
|
1419
|
+
return self.x_requested_with == 'XMLHttpRequest'
|
1420
|
+
end
|
1421
|
+
|
1422
|
+
def client_ip_addr
|
1423
|
+
#; [!e1uvg] returns 'X-Real-IP' header value if provided.
|
1424
|
+
addr = @env['HTTP_X_REAL_IP'] # nginx
|
1425
|
+
return addr if addr
|
1426
|
+
#; [!qdlyl] returns first item of 'X-Forwarded-For' header if provided.
|
1427
|
+
addr = @env['HTTP_X_FORWARDED_FOR'] # apache, squid, etc
|
1428
|
+
return addr.split(',').first if addr
|
1429
|
+
#; [!8nzjh] returns 'REMOTE_ADDR' if neighter 'X-Real-IP' nor 'X-Forwarded-For' provided.
|
1430
|
+
addr = @env['REMOTE_ADDR'] # http standard
|
1431
|
+
return addr
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
def scheme
|
1435
|
+
#; [!jytwy] returns 'https' when env['HTTPS'] is 'on'.
|
1436
|
+
return 'https' if @env['HTTPS'] == 'on'
|
1437
|
+
#; [!zg8r2] returns env['rack.url_scheme'] ('http' or 'https').
|
1438
|
+
return @env['rack.url_scheme']
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
def rack_version ; @env['rack.version'] ; end # ex: [1, 3]
|
1442
|
+
def rack_url_scheme ; @env['rack.url_scheme'] ; end # ex: 'http' or 'https'
|
1443
|
+
def rack_input ; @env['rack.input'] ; end # ex: $stdout
|
1444
|
+
def rack_errors ; @env['rack.errors'] ; end # ex: $stderr
|
1445
|
+
def rack_multithread ; @env['rack.multithread'] ; end # ex: true
|
1446
|
+
def rack_multiprocess ; @env['rack.multiprocess'] ; end # ex: true
|
1447
|
+
def rack_run_once ; @env['rack.run_once'] ; end # ex: false
|
1448
|
+
def rack_session ; @env['rack.session'] ; end # ex: {}
|
1449
|
+
def rack_logger ; @env['rack.logger'] ; end # ex: Logger.new
|
1450
|
+
def rack_hijack ; @env['rack.hijack'] ; end # ex: callable object
|
1451
|
+
def rack_hijack? ; @env['rack.hijack?'] ; end # ex: true or false
|
1452
|
+
def rack_hijack_io ; @env['rack.hijack_io'] ; end # ex: socket object
|
1453
|
+
|
1454
|
+
MAX_FORM_SIZE = 10*1024*1024
|
1455
|
+
MAX_JSON_SIZE = 10*1024*1024
|
1456
|
+
MAX_MULTIPART_SIZE = 100*1024*1024
|
1457
|
+
|
1458
|
+
def params_query
|
1459
|
+
#; [!6ezqw] parses QUERY_STRING and returns it as Hash/dict object.
|
1460
|
+
#; [!o0ws7] unquotes both keys and values.
|
1461
|
+
#; [!2fhrk] returns same value when called more than once.
|
1462
|
+
return @params_query ||= Util.parse_query_string(@env['QUERY_STRING'] || "")
|
1463
|
+
end
|
1464
|
+
alias query params_query
|
1465
|
+
|
1466
|
+
def params_form(max_content_length=nil)
|
1467
|
+
#; [!iultp] returns same value when called more than once.
|
1468
|
+
d = @params_form
|
1469
|
+
return d if d
|
1470
|
+
#; [!uq46o] raises 400 error when payload is not form data.
|
1471
|
+
self.content_type == 'application/x-www-form-urlencoded' or
|
1472
|
+
raise HttpException.new(400, "expected form data, but Content-Type header is #{self.content_type.inspect}.")
|
1473
|
+
#; [!puxlr] raises 400 error when content length is too large (> 10MB).
|
1474
|
+
clen = get_content_length(max_content_length || MAX_FORM_SIZE)
|
1475
|
+
#; [!59ad2] parses form parameters and returns it as Hash object.
|
1476
|
+
payload = get_input_stream().read(clen)
|
1477
|
+
d = @params_form = Util.parse_query_string(payload)
|
1478
|
+
return d
|
1479
|
+
end
|
1480
|
+
alias form params_form
|
1481
|
+
|
1482
|
+
def params_multipart(max_content_length=nil)
|
1483
|
+
#; [!gbdxu] returns same values when called more than once.
|
1484
|
+
d1 = @params_form
|
1485
|
+
d2 = @params_file
|
1486
|
+
return d1, d2 if d1 && d2
|
1487
|
+
#; [!ho5ii] raises 400 error when not multipart data.
|
1488
|
+
self.content_type =~ /\Amultipart\/form-data(?:;\s*boundary=(.+))?/ or
|
1489
|
+
raise HttpException.new(400, "expected multipart data, but Content-Type header is #{self.content_type.inspect}.")
|
1490
|
+
#; [!davzs] raises 400 error when boundary is missing.
|
1491
|
+
boundary = $1 or
|
1492
|
+
raise HttpException.new(400, 'bounday attribute of multipart required.')
|
1493
|
+
#; [!mtx6t] raises error when content length of multipart is too large (> 100MB).
|
1494
|
+
clen = get_content_length(max_content_length || MAX_MULTIPART_SIZE)
|
1495
|
+
#; [!y1jng] parses multipart when multipart data posted.
|
1496
|
+
d1, d2 = Util.parse_multipart(get_input_stream(), boundary, clen, nil, nil)
|
1497
|
+
@params_form = d1; @params_file = d2
|
1498
|
+
return d1, d2
|
1499
|
+
end
|
1500
|
+
alias multipart params_multipart
|
1501
|
+
|
1502
|
+
def params_json(max_content_length=nil)
|
1503
|
+
#; [!5kwij] returns same value when called more than once.
|
1504
|
+
d = @params_json
|
1505
|
+
return d if d
|
1506
|
+
#; [!qjgfz] raises 400 error when not JSON data.
|
1507
|
+
self.content_type =~ /\Aapplication\/json\b/ or
|
1508
|
+
raise HttpException.new(400, "expected JSON data, but Content-Type header is #{self.content_type.inspect}.")
|
1509
|
+
#; [!on107] raises error when content length of JSON is too large (> 10MB).
|
1510
|
+
clen = get_content_length(max_content_length || MAX_JSON_SIZE)
|
1511
|
+
#; [!ugik5] parses json data and returns it as hash object when json data is sent.
|
1512
|
+
payload = get_input_stream().read(clen)
|
1513
|
+
d = @params_json = JSON.parse(payload)
|
1514
|
+
return d
|
1515
|
+
end
|
1516
|
+
alias json params_json
|
1517
|
+
|
1518
|
+
def get_content_length(max_size)
|
1519
|
+
#; [!q88w9] raises error when content length is missing.
|
1520
|
+
clen = self.content_length or
|
1521
|
+
raise HttpException.new(400, 'Content-Length header expected.')
|
1522
|
+
#; [!ls6ir] raises error when content length is too large.
|
1523
|
+
clen <= max_size or
|
1524
|
+
raise HttpException.new(400, "Content-Length is too large (max: #{max_size}, actual: #{clen}).")
|
1525
|
+
return clen
|
1526
|
+
end
|
1527
|
+
private :get_content_length
|
1528
|
+
|
1529
|
+
def get_input_stream
|
1530
|
+
#; [!2buc6] returns input stream.
|
1531
|
+
return @env['rack.input']
|
1532
|
+
end
|
1533
|
+
protected :get_input_stream
|
1534
|
+
|
1535
|
+
def params
|
1536
|
+
#; [!erlc7] parses QUERY_STRING when request method is GET or HEAD.
|
1537
|
+
#; [!j2lno] parses form parameters when content type is 'application/x-www-form-urlencoded'.
|
1538
|
+
#; [!z5w4k] raises error when content type is 'multipart/form-data' (because params_multipart() returns two values).
|
1539
|
+
#; [!td6fw] raises error when content type is 'application/json' (because JSON data can contain non-string values).
|
1540
|
+
if @meth == :GET || @meth == :HEAD
|
1541
|
+
return params_query() # hash object
|
1422
1542
|
end
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
raise
|
1543
|
+
case @env['CONTENT_TYPE']
|
1544
|
+
when /\Aapplication\/x-www-form-urlencoded\b/
|
1545
|
+
return params_form() # hash object
|
1546
|
+
when /\Amultipart\/form-data\b/
|
1547
|
+
#return params_multipart() # array of hash objects
|
1548
|
+
raise PayloadParseError.new("don't use `@req.params' for multipart data; use `@req.params_multipart' instead.")
|
1549
|
+
when /\Aapplication\/json\b/
|
1550
|
+
#return params_json() # hash object
|
1551
|
+
raise PayloadParseError.new("use `@req.json' for JSON data instead of `@req.params'.")
|
1552
|
+
else
|
1553
|
+
return {} # hash object
|
1429
1554
|
end
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
#; [!
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
def cookies
|
1558
|
+
#; [!c9pwr] parses cookie data and returns it as hash object.
|
1559
|
+
return @cookies ||= Util.parse_cookie_string(@env['HTTP_COOKIE'] || "")
|
1560
|
+
end
|
1561
|
+
|
1562
|
+
def clear
|
1563
|
+
#; [!0jdal] removes uploaded files.
|
1564
|
+
d = @params_file
|
1565
|
+
d.each {|_, uploaded| uploaded.clean() } if d
|
1566
|
+
self
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
|
1572
|
+
class RackResponse
|
1573
|
+
|
1574
|
+
def initialize
|
1575
|
+
#; [!ehdkl] default status code is 200.
|
1576
|
+
@status_code = 200
|
1577
|
+
@headers = {}
|
1578
|
+
end
|
1579
|
+
|
1580
|
+
attr_accessor :status_code
|
1581
|
+
attr_reader :headers
|
1582
|
+
## for compatibility with Rack::Response
|
1583
|
+
alias status status_code
|
1584
|
+
alias status= status_code=
|
1585
|
+
|
1586
|
+
def status_line
|
1587
|
+
#; [!apy81] returns status line such as '200 OK'.
|
1588
|
+
return "#{@status_code} #{HTTP_RESPONSE_STATUS[@status_code] || 'UNKNOWN'}"
|
1589
|
+
end
|
1590
|
+
|
1591
|
+
def content_type
|
1592
|
+
return @headers['Content-Type']
|
1593
|
+
end
|
1594
|
+
|
1595
|
+
def content_type=(content_type)
|
1596
|
+
@headers['Content-Type'] = content_type
|
1597
|
+
end
|
1598
|
+
|
1599
|
+
def content_length
|
1600
|
+
s = @headers['Content-Length']
|
1601
|
+
return s ? s.to_i : nil
|
1602
|
+
end
|
1603
|
+
|
1604
|
+
def content_length=(length)
|
1605
|
+
@headers['Content-Length'] = length.to_s
|
1606
|
+
end
|
1607
|
+
|
1608
|
+
def set_cookie(name, value, domain: nil, path: nil, expires: nil, max_age: nil, httponly: nil, secure: nil)
|
1609
|
+
#; [!oanme] converts Time object into HTTP timestamp string.
|
1610
|
+
if expires && expires.is_a?(Time)
|
1611
|
+
expires = Util.http_utc_time(expires)
|
1612
|
+
end
|
1613
|
+
#; [!58tby] adds 'Set-Cookie' response header.
|
1614
|
+
s = "#{name}=#{value}"
|
1615
|
+
s << "; Domain=#{domain}" if domain
|
1616
|
+
s << "; Path=#{path}" if path
|
1617
|
+
s << "; Expires=#{expires}" if expires
|
1618
|
+
s << "; Max-Age=#{max_age}" if max_age
|
1619
|
+
s << "; HttpOnly" if httponly
|
1620
|
+
s << "; Secure" if secure
|
1621
|
+
#; [!u9w9l] supports multiple cookies.
|
1622
|
+
value = @headers['Set-Cookie']
|
1623
|
+
@headers['Set-Cookie'] = value ? (value << "\n" << s) : s
|
1624
|
+
#; [!7otip] returns cookie string.
|
1625
|
+
return s
|
1626
|
+
end
|
1627
|
+
|
1628
|
+
def clear
|
1438
1629
|
end
|
1439
1630
|
|
1440
1631
|
end
|
@@ -1442,59 +1633,84 @@ module K8
|
|
1442
1633
|
|
1443
1634
|
class RackApplication
|
1444
1635
|
|
1445
|
-
|
1636
|
+
REQUEST_CLASS = RackRequest
|
1637
|
+
RESPONSE_CLASS = RackResponse
|
1638
|
+
|
1639
|
+
def self.REQUEST_CLASS=(klass)
|
1640
|
+
#; [!7uqb4] changes default request class.
|
1641
|
+
remove_const :REQUEST_CLASS
|
1642
|
+
const_set :REQUEST_CLASS, klass
|
1643
|
+
end
|
1644
|
+
|
1645
|
+
def self.RESPONSE_CLASS=(klass)
|
1646
|
+
#; [!c1bd0] changes default response class.
|
1647
|
+
remove_const :RESPONSE_CLASS
|
1648
|
+
const_set :RESPONSE_CLASS, klass
|
1649
|
+
end
|
1650
|
+
|
1651
|
+
def initialize(urlpath_mapping=[], urlpath_cache_size: 0,
|
1446
1652
|
enable_urlpath_param_range: true)
|
1447
1653
|
#; [!vkp65] mounts urlpath mappings.
|
1448
1654
|
@mapping = ActionMapping.new(urlpath_mapping,
|
1449
|
-
|
1450
|
-
urlpath_cache_size: urlpath_cache_size,
|
1655
|
+
urlpath_cache_size: urlpath_cache_size,
|
1451
1656
|
enable_urlpath_param_range: enable_urlpath_param_range)
|
1452
1657
|
end
|
1453
1658
|
|
1454
|
-
def
|
1659
|
+
def find(req_path)
|
1455
1660
|
#; [!o0rnr] returns action class, action methods, urlpath names and values.
|
1456
|
-
return @mapping.
|
1661
|
+
return @mapping.find(req_path)
|
1662
|
+
end
|
1663
|
+
|
1664
|
+
def lookup(req_meth, req_path, query_string="")
|
1665
|
+
#; [!7476i] uses '_method' value of query string as request method when 'POST' method.
|
1666
|
+
if req_meth == :POST && query_string =~ /\A_method=(\w+)/
|
1667
|
+
req_meth = HTTP_REQUEST_METHODS[$1.upcase] || $1.upcase
|
1668
|
+
end
|
1669
|
+
#
|
1670
|
+
tuple = find(req_path)
|
1671
|
+
unless tuple
|
1672
|
+
#; [!c0job] redirects only when request method is GET or HEAD.
|
1673
|
+
if req_meth == :GET || req_meth == :HEAD
|
1674
|
+
#; [!u1qfv] raises 301 when urlpath not found but found with tailing '/'.
|
1675
|
+
#; [!kbff3] raises 301 when urlpath not found but found without tailing '/'.
|
1676
|
+
rpath = req_path
|
1677
|
+
rpath2 = rpath.end_with?('/') ? rpath[0..-2] : rpath + '/'
|
1678
|
+
if find(rpath2)
|
1679
|
+
#; [!cgxx4] adds query string to 'Location' header when redirecting.
|
1680
|
+
qs = query_string
|
1681
|
+
location = ! qs || qs.empty? ? rpath2 : "#{rpath2}?#{qs}"
|
1682
|
+
status = 301 # 301 Moved Permanently
|
1683
|
+
raise HttpException.new(status, nil, {'Location'=>location})
|
1684
|
+
end
|
1685
|
+
end
|
1686
|
+
#; [!hdy1f] raises HTTP 404 when urlpath not found.
|
1687
|
+
raise HttpException.new(404) # 404 Not Found
|
1688
|
+
end
|
1689
|
+
action_class, action_methods, urlpath_args = tuple
|
1690
|
+
#; [!0znwr] uses 'GET' method to find action when request method is 'HEAD'.
|
1691
|
+
d = action_methods
|
1692
|
+
action_name = d[req_meth] || (req_meth == :HEAD ? d[:GET] : nil) || d[:ANY]
|
1693
|
+
#; [!bfpav] raises HTTP 405 when urlpath found but request method not allowed.
|
1694
|
+
action_name or
|
1695
|
+
raise HttpException.new(405) # 405 Method Not Allowed
|
1696
|
+
return action_class, action_name, urlpath_args
|
1457
1697
|
end
|
1458
1698
|
|
1459
1699
|
def call(env)
|
1460
1700
|
#; [!uvmxe] takes env object.
|
1461
1701
|
#; [!gpe4g] returns status, headers and content.
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
def handle_request(req, resp)
|
1468
|
-
req_meth = HTTP_REQUEST_METHODS[req.env['REQUEST_METHOD']]
|
1702
|
+
#; [!eb2ms] returns 301 when urlpath not found but found with tailing '/'.
|
1703
|
+
#; [!02dow] returns 301 when urlpath not found but found without tailing '/'.
|
1704
|
+
#; [!2a9c9] adds query string to 'Location' header.
|
1705
|
+
#; [!vz07j] redirects only when request method is GET or HEAD.
|
1469
1706
|
#; [!l6kmc] uses 'GET' method to find action when request method is 'HEAD'.
|
1470
|
-
if req_meth == :HEAD
|
1471
|
-
req_meth_ = :GET
|
1472
1707
|
#; [!4vmd3] uses '_method' value of query string as request method when 'POST' method.
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1477
|
-
end
|
1708
|
+
#; [!rz13i] returns HTTP 404 when urlpath not found.
|
1709
|
+
#; [!rv3cf] returns HTTP 405 when urlpath found but request method not allowed.
|
1710
|
+
req = REQUEST_CLASS.new(env)
|
1711
|
+
resp = RESPONSE_CLASS.new
|
1478
1712
|
begin
|
1479
|
-
|
1480
|
-
#; [!vz07j] redirects only when request method is GET or HEAD.
|
1481
|
-
if tuple4.nil? && req_meth_ == :GET
|
1482
|
-
#; [!eb2ms] returns 301 when urlpath not found but found with tailing '/'.
|
1483
|
-
#; [!02dow] returns 301 when urlpath not found but found without tailing '/'.
|
1484
|
-
location = lookup_autoredirect_location(req)
|
1485
|
-
return [301, {'Location'=>location}, []] if location
|
1486
|
-
end
|
1487
|
-
#; [!rz13i] returns HTTP 404 when urlpath not found.
|
1488
|
-
tuple4 or
|
1489
|
-
raise HttpException.new(404)
|
1490
|
-
action_class, action_methods, urlpath_param_names, urlpath_param_values = tuple4
|
1491
|
-
#; [!rv3cf] returns HTTP 405 when urlpath found but request method not allowed.
|
1492
|
-
action_method = action_methods[req_meth_] or
|
1493
|
-
raise HttpException.new(405)
|
1494
|
-
#; [!0fgbd] finds action class and invokes action method with urlpath params.
|
1495
|
-
action_obj = action_class.new(req, resp)
|
1496
|
-
content = action_obj.handle_action(action_method, urlpath_param_values)
|
1497
|
-
ret = [resp.status, resp.headers, content]
|
1713
|
+
ret = handle_request(req, resp)
|
1498
1714
|
rescue HttpException => ex
|
1499
1715
|
ret = handle_http(ex, req, resp)
|
1500
1716
|
rescue Exception => ex
|
@@ -1504,11 +1720,23 @@ module K8
|
|
1504
1720
|
req.clear() if req.respond_to?(:clear)
|
1505
1721
|
resp.clear() if resp.respond_to?(:clear)
|
1506
1722
|
end
|
1507
|
-
#; [!9wp9z] returns empty body when request method is HEAD.
|
1508
|
-
ret[2] = [""] if req_meth == :HEAD
|
1509
1723
|
return ret
|
1510
1724
|
end
|
1511
1725
|
|
1726
|
+
protected
|
1727
|
+
|
1728
|
+
def handle_request(req, resp)
|
1729
|
+
#; [!0fgbd] finds action class and invokes action method with urlpath params.
|
1730
|
+
req_meth = HTTP_REQUEST_METHODS[req.env['REQUEST_METHOD']] || req.env['REQUEST_METHOD']
|
1731
|
+
tuple = lookup(req_meth, req.path, req.env['QUERY_STRING'])
|
1732
|
+
action_class, action_name, pargs = tuple # ex: [BooksAction, :do_show, [123]]
|
1733
|
+
action_obj = action_class.new(req, resp)
|
1734
|
+
content = action_obj.handle_action(action_name, pargs)
|
1735
|
+
#; [!9wp9z] returns empty body when request method is HEAD.
|
1736
|
+
content = [""] if req_meth == :HEAD
|
1737
|
+
return [resp.status, resp.headers, content]
|
1738
|
+
end
|
1739
|
+
|
1512
1740
|
def handle_http(ex, req, resp)
|
1513
1741
|
if json_expected?(req)
|
1514
1742
|
content = render_http_exception_as_json(ex, req, resp)
|
@@ -1551,14 +1779,6 @@ END
|
|
1551
1779
|
return false
|
1552
1780
|
end
|
1553
1781
|
|
1554
|
-
def lookup_autoredirect_location(req)
|
1555
|
-
location = req.path.end_with?('/') ? req.path[0..-2] : "#{req.path}/"
|
1556
|
-
return nil unless lookup(location)
|
1557
|
-
#; [!2a9c9] adds query string to 'Location' header.
|
1558
|
-
qs = req.env['QUERY_STRING']
|
1559
|
-
return qs && ! qs.empty? ? "#{location}?#{qs}" : location
|
1560
|
-
end
|
1561
|
-
|
1562
1782
|
public
|
1563
1783
|
|
1564
1784
|
def each_mapping(&block)
|
@@ -1573,8 +1793,8 @@ END
|
|
1573
1793
|
s = ""
|
1574
1794
|
each_mapping do |full_urlpath_pat, action_class, action_methods|
|
1575
1795
|
arr = req_methods.collect {|req_meth|
|
1576
|
-
|
1577
|
-
|
1796
|
+
action_name = action_methods[req_meth]
|
1797
|
+
action_name ? "#{req_meth}: #{action_name}" : nil
|
1578
1798
|
}.compact()
|
1579
1799
|
s << "- urlpath: #{full_urlpath_pat}\n"
|
1580
1800
|
s << " class: #{action_class}\n"
|
@@ -1587,186 +1807,4 @@ END
|
|
1587
1807
|
end
|
1588
1808
|
|
1589
1809
|
|
1590
|
-
class SecretValue < Object
|
1591
|
-
|
1592
|
-
def initialize(name=nil)
|
1593
|
-
#; [!fbwnh] takes environment variable name.
|
1594
|
-
@name = name
|
1595
|
-
end
|
1596
|
-
|
1597
|
-
attr_reader :name
|
1598
|
-
|
1599
|
-
def value
|
1600
|
-
#; [!gg06v] returns environment variable value.
|
1601
|
-
return @name ? ENV[@name] : nil
|
1602
|
-
end
|
1603
|
-
|
1604
|
-
def to_s
|
1605
|
-
#; [!7ymqq] returns '<SECRET>' string when name not eixst.
|
1606
|
-
#; [!x6edf] returns 'ENV[<name>]' string when name exists.
|
1607
|
-
return @name ? "ENV['#{@name}']" : "<SECRET>"
|
1608
|
-
end
|
1609
|
-
|
1610
|
-
#; [!j27ji] 'inspect()' is alias of 'to_s()'.
|
1611
|
-
alias inspect to_s
|
1612
|
-
|
1613
|
-
def [](name)
|
1614
|
-
#; [!jjqmn] creates new instance object with name.
|
1615
|
-
self.class.new(name)
|
1616
|
-
end
|
1617
|
-
|
1618
|
-
end
|
1619
|
-
|
1620
|
-
|
1621
|
-
class BaseConfig < Object
|
1622
|
-
|
1623
|
-
SECRET = SecretValue.new
|
1624
|
-
|
1625
|
-
def initialize(freeze: true)
|
1626
|
-
#; [!vvd1n] copies key and values from class object.
|
1627
|
-
self.class.each do |key, val, _, _|
|
1628
|
-
#; [!xok12] when value is SECRET...
|
1629
|
-
if val.is_a?(SecretValue)
|
1630
|
-
#; [!a4a4p] raises error when key not specified.
|
1631
|
-
val.name or
|
1632
|
-
raise ConfigError.new("config '#{key}' should be set, but not.")
|
1633
|
-
#; [!w4yl7] raises error when ENV value not specified.
|
1634
|
-
ENV[val.name] or
|
1635
|
-
raise ConfigError.new("config '#{key}' depends on ENV['#{val.name}'], but not set.")
|
1636
|
-
#; [!he20d] get value from ENV.
|
1637
|
-
val = ENV[val.name]
|
1638
|
-
end
|
1639
|
-
instance_variable_set("@#{key}", val)
|
1640
|
-
end
|
1641
|
-
#; [!6dilv] freezes self and class object if 'freeze:' is true.
|
1642
|
-
self.class.freeze if freeze
|
1643
|
-
self.freeze if freeze
|
1644
|
-
end
|
1645
|
-
|
1646
|
-
def self.validate_values # :nodoc:
|
1647
|
-
not_set = []
|
1648
|
-
not_env = []
|
1649
|
-
each() do |key, val, _, _|
|
1650
|
-
if val.is_a?(SecretValue)
|
1651
|
-
if ! val.name
|
1652
|
-
not_set << [key, val]
|
1653
|
-
elsif ! ENV[val.name]
|
1654
|
-
not_env << [key, val]
|
1655
|
-
end
|
1656
|
-
end
|
1657
|
-
end
|
1658
|
-
return nil if not_set.empty? && not_env.empty?
|
1659
|
-
sb = []
|
1660
|
-
sb << "**"
|
1661
|
-
sb << "** ERROR: insufficient config"
|
1662
|
-
unless not_set.empty?
|
1663
|
-
sb << "**"
|
1664
|
-
sb << "** The following configs should be set, but not."
|
1665
|
-
sb << "**"
|
1666
|
-
not_set.each do |key, val|
|
1667
|
-
sb << "** %-25s %s" % [key, val]
|
1668
|
-
end
|
1669
|
-
end
|
1670
|
-
unless not_env.empty?
|
1671
|
-
sb << "**"
|
1672
|
-
sb << "** The following configs expect environment variable, but not set."
|
1673
|
-
sb << "**"
|
1674
|
-
not_env.each do |key, val|
|
1675
|
-
sb << "** %-25s %s" % [key, val]
|
1676
|
-
end
|
1677
|
-
end
|
1678
|
-
sb << "**"
|
1679
|
-
sb << ""
|
1680
|
-
return sb.join("\n")
|
1681
|
-
end
|
1682
|
-
|
1683
|
-
private
|
1684
|
-
|
1685
|
-
def self.__all
|
1686
|
-
return @__all ||= {}
|
1687
|
-
end
|
1688
|
-
|
1689
|
-
public
|
1690
|
-
|
1691
|
-
def self.has?(key)
|
1692
|
-
#; [!dv87n] returns true iff key is set.
|
1693
|
-
return __all().key?(key)
|
1694
|
-
end
|
1695
|
-
|
1696
|
-
def self.put(key, value, desc=nil)
|
1697
|
-
#; [!h9b47] defines getter method.
|
1698
|
-
attr_reader key
|
1699
|
-
d = __all()
|
1700
|
-
#; [!mun1v] keeps secret flag.
|
1701
|
-
if d[key]
|
1702
|
-
desc ||= d[key][1]
|
1703
|
-
secret = d[key][2]
|
1704
|
-
else
|
1705
|
-
secret = value == SECRET
|
1706
|
-
end
|
1707
|
-
#; [!ncwzt] stores key with value, description and secret flag.
|
1708
|
-
d[key] = [value, desc, secret]
|
1709
|
-
nil
|
1710
|
-
end
|
1711
|
-
|
1712
|
-
def self.add(key, value, desc=nil)
|
1713
|
-
#; [!envke] raises error when already added.
|
1714
|
-
! self.has?(key) or
|
1715
|
-
raise K8::ConfigError.new("add(#{key.inspect}, #{value.inspect}): cannot add because already added; use set() or put() instead.")
|
1716
|
-
#; [!6cmb4] adds new key, value and desc.
|
1717
|
-
self.put(key, value, desc)
|
1718
|
-
end
|
1719
|
-
|
1720
|
-
def self.set(key, value, desc=nil)
|
1721
|
-
#; [!2yis0] raises error when not added yet.
|
1722
|
-
self.has?(key) or
|
1723
|
-
raise K8::ConfigError.new("set(#{key.inspect}, #{value.inspect}): cannot set because not added yet; use add() or put() instead.")
|
1724
|
-
#; [!3060g] sets key, value and desc.
|
1725
|
-
self.put(key, value, desc)
|
1726
|
-
end
|
1727
|
-
|
1728
|
-
def self.each
|
1729
|
-
#; [!iu88i] yields with key, value, desc and secret flag.
|
1730
|
-
__all().each do |key, (value, desc, secret)|
|
1731
|
-
yield key, value, desc, secret
|
1732
|
-
end
|
1733
|
-
nil
|
1734
|
-
end
|
1735
|
-
|
1736
|
-
def self.get(key, default=nil)
|
1737
|
-
#; [!zlhnp] returns value corresponding to key.
|
1738
|
-
#; [!o0k05] returns default value (=nil) when key is not added.
|
1739
|
-
tuple = __all()[key]
|
1740
|
-
return tuple ? tuple.first : default
|
1741
|
-
end
|
1742
|
-
|
1743
|
-
def [](key)
|
1744
|
-
#; [!jn9l5] returns value corresponding to key.
|
1745
|
-
return __send__(key)
|
1746
|
-
end
|
1747
|
-
|
1748
|
-
def get_all(prefix_key)
|
1749
|
-
#; [!4ik3c] returns all keys and values which keys start with prefix as hash object.
|
1750
|
-
prefix = "@#{prefix_key}"
|
1751
|
-
symbol_p = prefix_key.is_a?(Symbol)
|
1752
|
-
range = prefix.length..-1
|
1753
|
-
d = {}
|
1754
|
-
self.instance_variables.each do |ivar|
|
1755
|
-
if ivar.to_s.start_with?(prefix)
|
1756
|
-
val = self.instance_variable_get(ivar)
|
1757
|
-
key = ivar[range].intern
|
1758
|
-
key = key.intern if symbol_p
|
1759
|
-
d[key] = val
|
1760
|
-
end
|
1761
|
-
end
|
1762
|
-
return d
|
1763
|
-
end
|
1764
|
-
|
1765
|
-
end
|
1766
|
-
|
1767
|
-
|
1768
|
-
class ConfigError < StandardError
|
1769
|
-
end
|
1770
|
-
|
1771
|
-
|
1772
1810
|
end
|