keight 0.1.0 → 0.2.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.txt +64 -0
- data/MIT-LICENSE +1 -1
- data/README.md +180 -55
- data/Rakefile +1 -1
- data/bench/bench.rb +219 -119
- data/bench/benchmarker.rb +5 -3
- data/bin/k8rb +237 -20
- data/keight.gemspec +3 -3
- data/lib/keight.rb +373 -634
- data/lib/keight/skeleton/README.txt +13 -0
- data/lib/keight/skeleton/config.ru +3 -18
- data/lib/keight/skeleton/index.txt +7 -6
- data/lib/keight/skeleton/main.rb +22 -0
- data/lib/keight/skeleton/static/lib/.keep +0 -0
- data/lib/keight/skeleton/test/api/hello_test.rb +27 -0
- data/lib/keight/skeleton/test/test_helper.rb +9 -0
- data/test/keight_test.rb +783 -938
- data/test/oktest.rb +3 -3
- metadata +29 -7
- data/lib/keight/skeleton/static/lib/jquery/1.11.3/jquery.min.js +0 -6
- data/lib/keight/skeleton/static/lib/jquery/1.11.3/jquery.min.js.gz +0 -0
- data/lib/keight/skeleton/static/lib/modernizr/2.8.3/modernizr.min.js +0 -4
- data/lib/keight/skeleton/static/lib/modernizr/2.8.3/modernizr.min.js.gz +0 -0
data/bench/benchmarker.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
###
|
2
|
-
### $Release: 0.
|
3
|
-
### $Copyright: copyright(c) 2014-
|
2
|
+
### $Release: 0.2.0 $
|
3
|
+
### $Copyright: copyright(c) 2014-2016 kuwata-lab.com all rights reserved $
|
4
4
|
### $License: MIT License $
|
5
5
|
###
|
6
6
|
|
7
7
|
|
8
8
|
module Benchmarker
|
9
9
|
|
10
|
-
VERSION = "$Release: 0.
|
10
|
+
VERSION = "$Release: 0.2.0 $".split(/ /)[1]
|
11
11
|
|
12
12
|
def self.new(opts={}, &block)
|
13
13
|
#: creates runner object and returns it.
|
@@ -240,6 +240,8 @@ END
|
|
240
240
|
attr_accessor :label, :loop, :user, :sys, :total, :real
|
241
241
|
|
242
242
|
def run
|
243
|
+
#: starts GC before running benchmark.
|
244
|
+
GC.start
|
243
245
|
#: yields block for @loop times.
|
244
246
|
ntimes = @loop || 1
|
245
247
|
pt1 = Process.times
|
data/bin/k8rb
CHANGED
@@ -2,14 +2,12 @@
|
|
2
2
|
# -*- coding: utf-8 -*-
|
3
3
|
|
4
4
|
###
|
5
|
-
### $Release: 0.
|
6
|
-
### $Copyright: copyright(c) 2014-
|
5
|
+
### $Release: 0.2.0 $
|
6
|
+
### $Copyright: copyright(c) 2014-2016 kuwata-lab.com all rights reserved $
|
7
7
|
### $License: MIT License $
|
8
8
|
###
|
9
9
|
|
10
10
|
|
11
|
-
require 'digest/sha1'
|
12
|
-
|
13
11
|
require 'keight'
|
14
12
|
require 'baby_erubis'
|
15
13
|
|
@@ -22,15 +20,16 @@ module K8
|
|
22
20
|
@__current = nil
|
23
21
|
@__actions = []
|
24
22
|
|
25
|
-
@action = proc do |action_name, desc|
|
23
|
+
@action = proc do |action_name, desc, dict|
|
26
24
|
options = []
|
27
25
|
method_name = nil
|
28
|
-
|
26
|
+
(dict ||= {})[:desc] = desc
|
27
|
+
tuple = [action_name, dict, options, method_name]
|
29
28
|
@__current = tuple
|
30
29
|
@__actions << tuple
|
31
30
|
end
|
32
31
|
|
33
|
-
@option = proc do |optdef_str, desc|
|
32
|
+
@option = proc do |optdef_str, desc, opts|
|
34
33
|
tuple = @__current or
|
35
34
|
raise "@option.() is called without @action.()"
|
36
35
|
tuple[2] << [optdef_str, desc]
|
@@ -72,7 +71,8 @@ module K8
|
|
72
71
|
sb << " #{script} ACTION [..OPTIONS..]"
|
73
72
|
sb << ""
|
74
73
|
sb << "Actions:"
|
75
|
-
self.class.each_action do |action_name,
|
74
|
+
self.class.each_action do |action_name, dict, options, _|
|
75
|
+
desc = dict[:desc]
|
76
76
|
next unless desc
|
77
77
|
help = desc.lines.first.chomp()
|
78
78
|
sb << " #{script} %-15s # #{help}" % [action_name, help]
|
@@ -82,7 +82,7 @@ module K8
|
|
82
82
|
else
|
83
83
|
tuple = self.class.find_action(action_name) or
|
84
84
|
raise K8::OptionError.new("help #{action_name}: no such action.")
|
85
|
-
action_name,
|
85
|
+
action_name, dict, options, method_name = tuple
|
86
86
|
pnames = self.method(method_name).parameters.collect {|kind, pname|
|
87
87
|
case kind
|
88
88
|
when :req ; pname
|
@@ -95,7 +95,7 @@ module K8
|
|
95
95
|
}.compact()
|
96
96
|
pnames.unshift('') unless pnames.empty?
|
97
97
|
argstr = pnames.join(' ')
|
98
|
-
sb << "#{script} #{action_name} - #{desc}"
|
98
|
+
sb << "#{script} #{action_name} - #{dict[:desc]}"
|
99
99
|
sb << ""
|
100
100
|
sb << "Usage:"
|
101
101
|
if options.empty?
|
@@ -109,15 +109,21 @@ module K8
|
|
109
109
|
sb << " %-20s # %s" % [optstr, optdesc] if optdesc
|
110
110
|
end
|
111
111
|
end
|
112
|
+
if dict[:example]
|
113
|
+
sb << ""
|
114
|
+
sb << "Examples:"
|
115
|
+
sb << dict[:example].gsub(/%SCRIPT%/, script)
|
116
|
+
end
|
112
117
|
end
|
113
118
|
sb << ""
|
114
119
|
sb << ""
|
115
120
|
return sb.join("\n")
|
116
121
|
end
|
117
122
|
|
118
|
-
@action.(:
|
123
|
+
@action.(:project, "create project files")
|
119
124
|
@option.("-s, --skeleton=DIR", "directory of skeleton files")
|
120
|
-
def
|
125
|
+
def do_project(name, dir: nil)
|
126
|
+
project_name = name
|
121
127
|
dir ||= K8::FILEPATH.sub(/\.rb\z/, '') + "/skeleton"
|
122
128
|
if dir
|
123
129
|
File.directory?(dir) or
|
@@ -137,30 +143,139 @@ module K8
|
|
137
143
|
o.mkdir("#{project_name}/#{base}", descs[base+"/"])
|
138
144
|
elsif File.file?(path)
|
139
145
|
content = path =~ /\Astatic\/lib|\.gz\z/ \
|
140
|
-
|
141
|
-
|
146
|
+
? File.read(path, encoding: 'ascii-8bit') \
|
147
|
+
: K8::SkeletonTemplate.new.from_file(path, 'ascii-8bit').render()
|
142
148
|
o.mkfile("#{project_name}/#{base}", content, descs[base])
|
143
149
|
else
|
144
150
|
next
|
145
151
|
end
|
146
152
|
end
|
147
|
-
|
153
|
+
#
|
154
|
+
msg = <<-END
|
155
|
+
|
156
|
+
##
|
157
|
+
## File copied.
|
158
|
+
## Please run 'rake setup' in '#{project_name}' directory, for example:
|
159
|
+
##
|
160
|
+
## $ cd #{project_name}
|
161
|
+
## $ rake setup
|
162
|
+
## $ export APP_ENV='dev' # or 'prod', 'stg', 'test'
|
163
|
+
## $ rake server port=8000
|
164
|
+
## $ open http://localhost:8000/
|
165
|
+
##
|
166
|
+
END
|
167
|
+
return msg.gsub(/^ /, '')
|
148
168
|
end
|
149
169
|
0
|
150
170
|
end
|
151
171
|
|
152
172
|
@action.(:mapping, "show action mappings")
|
153
|
-
|
173
|
+
@option.("--format={text|json|yaml|javascript|jquery|angular}", "output format")
|
174
|
+
def do_mapping(format: nil)
|
154
175
|
ENV['APP_MODE'] ||= 'dev'
|
155
176
|
load_config()
|
156
177
|
require './config/urlpath_mapping'
|
157
|
-
|
158
|
-
|
159
|
-
|
178
|
+
return \
|
179
|
+
case format
|
180
|
+
when nil ; _do_mapping_in_text_format()
|
181
|
+
when 'text' ; _do_mapping_in_text_format()
|
182
|
+
when 'json' ; _do_mapping_in_json_format()
|
183
|
+
when 'yaml' ; _do_mapping_in_yaml_format()
|
184
|
+
when 'javascript'; _do_mapping_in_javascript_format(nil)
|
185
|
+
when 'jquery' ; _do_mapping_in_javascript_format('type')
|
186
|
+
when 'angular' ; _do_mapping_in_javascript_format('method')
|
187
|
+
else
|
188
|
+
raise OptionError.new("#{format}: expected one of text/json/yaml/javascript/jquery/angular.")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def _do_mapping(&block)
|
195
|
+
app = K8::RackApplication.new($urlpath_mapping)
|
196
|
+
app.each_mapping do |urlpath_pat, action_class, action_methods, |
|
197
|
+
yield urlpath_pat, action_class, action_methods
|
198
|
+
end
|
199
|
+
end
|
200
|
+
private :_do_mapping
|
201
|
+
|
202
|
+
def _do_mapping_in_json_format()
|
203
|
+
s = %Q`{ "mappings": [\n`.dup()
|
204
|
+
i = 0
|
205
|
+
_do_mapping do |urlpath_pat, action_class, action_methods|
|
206
|
+
i += 1
|
207
|
+
arr = action_methods.map {|k, v| %Q`"#{k}": "#{v}"` }
|
208
|
+
s << (i == 1 ? ' ' : ', ')
|
209
|
+
s << %Q`{"urlpath": "#{urlpath_pat}"`
|
210
|
+
s << %Q`, "class": \"#{action_class.name}"`
|
211
|
+
s << %Q`, "method": {#{arr.join(', ')}}}\n`
|
212
|
+
end
|
213
|
+
s << "] }\n"
|
214
|
+
return s
|
215
|
+
end
|
216
|
+
|
217
|
+
def _do_mapping_in_yaml_format()
|
218
|
+
s = ""
|
219
|
+
_do_mapping do |urlpath_pat, action_class, action_methods|
|
220
|
+
arr = action_methods.map {|k, v| "#{k}: #{v}" }
|
221
|
+
s << "- urlpath: '#{urlpath_pat}'\n"
|
222
|
+
s << " class: #{action_class.name}\n"
|
223
|
+
s << " method: {#{arr.join(', ')}}\n"
|
224
|
+
s << "\n"
|
225
|
+
end
|
226
|
+
return s
|
227
|
+
end
|
228
|
+
|
229
|
+
def _do_mapping_in_javascript_format(attr)
|
230
|
+
rexp = K8::ActionMapping::URLPATH_PARAM_REXP
|
231
|
+
defs = {}
|
232
|
+
_do_mapping do |urlpath_pat, action_class, action_methods|
|
233
|
+
(defs[action_class.name] ||= []) << [urlpath_pat, action_methods]
|
234
|
+
end
|
235
|
+
buf = []
|
236
|
+
defs.each do |action_class_name, pairs|
|
237
|
+
if action_class_name =~ /\A\w+\z/
|
238
|
+
buf << "#{action_class_name}:{\n"
|
239
|
+
else
|
240
|
+
buf << "'#{action_class_name}':{\n"
|
241
|
+
end
|
242
|
+
sep = ' '
|
243
|
+
pairs.each do |urlpath_pat, action_methods|
|
244
|
+
args = []; i = 0
|
245
|
+
urlpath_pat.scan(rexp) {|x, _| args << (x.empty? ? "_#{i+=1}" : x) }
|
246
|
+
a = args.dup()
|
247
|
+
js_urlpath = "'" + urlpath_pat.gsub(rexp) {|x, _| "'+#{a.shift}+'" } + "'"
|
248
|
+
js_urlpath.gsub!(/\+''/, '')
|
249
|
+
#
|
250
|
+
action_methods.each do |req_meth, action_method|
|
251
|
+
buf << sep; sep = ','
|
252
|
+
if attr
|
253
|
+
js_code = "return{#{attr}:'#{req_meth}',url:#{js_urlpath}}"
|
254
|
+
buf << "#{action_method}:function(#{args.join(',')}){#{js_code}}\n"
|
255
|
+
else
|
256
|
+
js_code = "function(#{args.join(',')}){return#{js_urlpath}}"
|
257
|
+
buf << "#{action_method}:{method:'#{req_meth}',urlpath:#{js_code}}\n"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
buf << "},\n"
|
262
|
+
end
|
263
|
+
buf[-1] = "}\n" if buf[-1] == "},\n"
|
264
|
+
return buf.join()
|
265
|
+
end
|
266
|
+
|
267
|
+
def _do_mapping_in_text_format()
|
268
|
+
s = ""
|
269
|
+
_do_mapping do |urlpath_pat, action_class, action_methods|
|
270
|
+
action_methods.each do |req_meth, action_method|
|
271
|
+
s << "%-6s %-30s # %s\#%s\n" % [req_meth, urlpath_pat, action_class.name, action_method]
|
272
|
+
end
|
160
273
|
end
|
161
|
-
return
|
274
|
+
return s
|
162
275
|
end
|
163
276
|
|
277
|
+
public
|
278
|
+
|
164
279
|
@action.(:configs, "list config keys and values")
|
165
280
|
@option.("--getenv", "show actual ENV value")
|
166
281
|
def do_configs(getenv: false)
|
@@ -245,6 +360,108 @@ module K8
|
|
245
360
|
end
|
246
361
|
private :__var_dump
|
247
362
|
|
363
|
+
@action.(:cdnjs, "search or download libraries from cdnjs.com")
|
364
|
+
@option.("-d, --basedir=DIR", "base directory (default: 'static/lib')")
|
365
|
+
def do_cdnjs(library=nil, version=nil, basedir: nil)
|
366
|
+
require 'open-uri'
|
367
|
+
require 'fileutils'
|
368
|
+
library.nil? || library =~ /\A\*?[-.\w]+\*?\z/ or
|
369
|
+
raise OptionError.new("#{library}: invalid library name.")
|
370
|
+
version.nil? || version =~ /\A\d+(\.\d+)+[-.\w]+\z/ or
|
371
|
+
raise OptionError.new("#{version}: version number expected.")
|
372
|
+
## list or search libraries
|
373
|
+
if library.nil? || library.include?('*')
|
374
|
+
return _cdnjs_list(library)
|
375
|
+
## list versions
|
376
|
+
elsif version.nil?
|
377
|
+
return _cdnjs_find(library)
|
378
|
+
## download files
|
379
|
+
else
|
380
|
+
return _cdnjs_download(library, version, basedir)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def _cdnjs_list(pattern)
|
385
|
+
url = "https://cdnjs.com/libraries"
|
386
|
+
rexp = %r'href="/libraries/([-.\w]+)"'
|
387
|
+
libs = open(url) {|f|
|
388
|
+
#f.grep(rexp) { $1 } # not work
|
389
|
+
f.collect {|s| $1 if s =~ rexp }.compact.sort.uniq
|
390
|
+
}
|
391
|
+
if pattern
|
392
|
+
s = Regexp.escape(pattern) # ex: '*foo.js*' -> '\*foo\.js\*'
|
393
|
+
s = s.gsub(/\\\*/, '.*') # ex: '\*foo\.js\*' -> '.*foo\.js.*'
|
394
|
+
s = "\\A#{s}\\z" # ex: '.*foo\.js.*' -> '\A.*foo\.js.*\z'
|
395
|
+
rexp = Regexp.compile(s)
|
396
|
+
libs = libs.grep(rexp)
|
397
|
+
end
|
398
|
+
return (libs << "").join("\n")
|
399
|
+
end
|
400
|
+
|
401
|
+
def _cdnjs_find(library)
|
402
|
+
url = "https://cdnjs.com/libraries/#{library}"
|
403
|
+
rexp = %r'<option value="(.*?)".*?>\1</option>'
|
404
|
+
versions = open(url) {|f|
|
405
|
+
f.collect {|s| $1 if s =~ rexp }.compact()
|
406
|
+
}.sort_by {|verstr| _normalized_version(verstr) }
|
407
|
+
if versions.empty?
|
408
|
+
$stderr.puts "#{library}: no such library (GET #{url})."
|
409
|
+
return 1
|
410
|
+
end
|
411
|
+
return (versions << "").join("\n")
|
412
|
+
end
|
413
|
+
|
414
|
+
def _cdnjs_download(library, version, basedir=nil)
|
415
|
+
basedir ||= "static/lib"
|
416
|
+
File.exist?(basedir) or
|
417
|
+
raise OptionError.new("#{basedir}: directory not exist.")
|
418
|
+
File.directory?(basedir) or
|
419
|
+
raise OptionError.new("#{basedir}: not a directory.")
|
420
|
+
url = "https://cdnjs.com/libraries/#{library}/#{version}"
|
421
|
+
rexp = %r">https://cdnjs\.cloudflare\.com/ajax/libs/#{library}/#{Regexp.escape(version)}/([-.\w]+(?:\&\#x2F;[-.\w]+)*)<"
|
422
|
+
filenames = open(url) {|f|
|
423
|
+
f.collect {|s| $1.gsub(/\&\#x2F;/, '/') if s =~ rexp }.compact()
|
424
|
+
}
|
425
|
+
if filenames.empty?
|
426
|
+
$stderr.puts "#{library} #{version}: no files (GET #{url})."
|
427
|
+
return 1
|
428
|
+
end
|
429
|
+
baseurl = "https://cdnjs.cloudflare.com/ajax/libs"
|
430
|
+
filenames.each do |filename|
|
431
|
+
fileurl = "#{baseurl}/#{library}/#{version}/#{filename}"
|
432
|
+
filepath = "#{basedir}/#{library}/#{version}/#{filename}"
|
433
|
+
FileUtils.mkdir_p(File.dirname(filepath))
|
434
|
+
print filepath, ' '
|
435
|
+
content = open(fileurl) {|f| f.read }
|
436
|
+
if _file_changed(filepath, content)
|
437
|
+
File.open(filepath, 'w') {|f| f.write(content) }
|
438
|
+
s = ""
|
439
|
+
else
|
440
|
+
s = " (not changed)"
|
441
|
+
end
|
442
|
+
puts "(#{_bytesize(content)} byte)#{s}"
|
443
|
+
end
|
444
|
+
return 0
|
445
|
+
end
|
446
|
+
|
447
|
+
def _normalized_version(version_str)
|
448
|
+
if version_str =~ /\A(\d+(?:\.\d+)*)(.*)/
|
449
|
+
return $1.split('.').map {|n| "%05d" % n.to_i }.join('.') + $2
|
450
|
+
else
|
451
|
+
return version_str
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def _file_changed(filepath, expected)
|
456
|
+
return true unless File.exist?(filepath)
|
457
|
+
actual = File.read(filepath, encoding: expected.encoding)
|
458
|
+
return actual != expected
|
459
|
+
end
|
460
|
+
|
461
|
+
def _bytesize(string)
|
462
|
+
return string.bytesize.to_s.reverse.gsub(/(...)/, '\1,').sub(/,\z/, '').reverse
|
463
|
+
end
|
464
|
+
|
248
465
|
|
249
466
|
if ENV['DEBUG']
|
250
467
|
@action.(:debug, "debug")
|
data/keight.gemspec
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |o|
|
5
5
|
o.name = "keight"
|
6
|
-
o.version = "$Release: 0.
|
6
|
+
o.version = "$Release: 0.2.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
|
@@ -18,13 +18,12 @@ See https://github.com/kwatch/keight/tree/ruby for details.
|
|
18
18
|
END
|
19
19
|
|
20
20
|
o.files = Dir[*%w[
|
21
|
-
README.md MIT-LICENSE keight.gemspec setup.rb Rakefile
|
21
|
+
README.md CHANGES.txt MIT-LICENSE keight.gemspec setup.rb Rakefile
|
22
22
|
bin/k8rb
|
23
23
|
lib/keight.rb lib/keight/**/*.{[a-z]*} lib/keight/**/.{[a-z]*}
|
24
24
|
test/*_test.rb test/data/* test/oktest.rb
|
25
25
|
bench/bench.rb bench/benchmarker.rb
|
26
26
|
]]
|
27
|
-
#lib/keight.rb lib/keight/**/*
|
28
27
|
o.executables = ["k8rb"]
|
29
28
|
o.bindir = ["bin"]
|
30
29
|
#o.test_files = o.files.grep(/^test\//)
|
@@ -34,4 +33,5 @@ END
|
|
34
33
|
o.add_runtime_dependency 'rack', '~> 1.6'
|
35
34
|
o.add_runtime_dependency 'baby_erubis', '~> 2.1', '>= 2.1.1'
|
36
35
|
#o.add_development_dependency "oktest", "~> 0"
|
36
|
+
o.add_development_dependency 'rack-test_app', '~> 1.0', '>= 1.0.0'
|
37
37
|
end
|
data/lib/keight.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
###
|
4
|
-
### $Release: 0.
|
5
|
-
### $Copyright: copyright(c) 2014-
|
4
|
+
### $Release: 0.2.0 $
|
5
|
+
### $Copyright: copyright(c) 2014-2016 kuwata-lab.com all rights reserved $
|
6
6
|
### $License: MIT License $
|
7
7
|
###
|
8
8
|
|
@@ -15,6 +15,8 @@ require 'digest/sha1'
|
|
15
15
|
|
16
16
|
module K8
|
17
17
|
|
18
|
+
RELEASE = '$Release: 0.2.0 $'.split()[1]
|
19
|
+
|
18
20
|
FILEPATH = __FILE__
|
19
21
|
|
20
22
|
HTTP_REQUEST_METHODS = {
|
@@ -26,7 +28,7 @@ module K8
|
|
26
28
|
"PATCH" => :PATCH,
|
27
29
|
"OPTIONS" => :OPTIONS,
|
28
30
|
"TRACE" => :TRACE,
|
29
|
-
}
|
31
|
+
}
|
30
32
|
|
31
33
|
HTTP_RESPONSE_STATUS = {
|
32
34
|
100 => "Continue",
|
@@ -169,7 +171,7 @@ module K8
|
|
169
171
|
'.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
170
172
|
'.xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
171
173
|
'.pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
172
|
-
}.each {|
|
174
|
+
}.each {|_, v| v.freeze } # hash keys are always frozen
|
173
175
|
|
174
176
|
|
175
177
|
module Util
|
@@ -456,17 +458,17 @@ module K8
|
|
456
458
|
|
457
459
|
attr_reader :env, :method, :path
|
458
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
|
+
|
459
467
|
def header(name)
|
460
468
|
#; [!1z7wj] returns http header value from environment.
|
461
469
|
return @env["HTTP_#{name.upcase.sub('-', '_')}"]
|
462
470
|
end
|
463
471
|
|
464
|
-
def method(name=nil)
|
465
|
-
#; [!tp595] returns :GET, :POST, :PUT, ... when argument is not passed.
|
466
|
-
#; [!49f51] returns Method object when argument is passed.
|
467
|
-
return name.nil? ? @method : super(name)
|
468
|
-
end
|
469
|
-
|
470
472
|
def request_method
|
471
473
|
#; [!y8eos] returns env['REQUEST_METHOD'] as string.
|
472
474
|
return @env['REQUEST_METHOD']
|
@@ -783,6 +785,25 @@ module K8
|
|
783
785
|
return @action_method_mapping ||= ActionMethodMapping.new
|
784
786
|
end
|
785
787
|
|
788
|
+
def self._build_action_info(full_urlpath_pattern) # :nodoc:
|
789
|
+
#; [!ordhc] build ActionInfo objects for each action methods.
|
790
|
+
parent = full_urlpath_pattern
|
791
|
+
@action_infos = {}
|
792
|
+
_action_method_mapping().each do |urlpath_pat, methods|
|
793
|
+
methods.each do |req_meth, action_method_name|
|
794
|
+
info = ActionInfo.create(req_meth, "#{parent}#{urlpath_pat}")
|
795
|
+
@action_infos[action_method_name] = info
|
796
|
+
end
|
797
|
+
end
|
798
|
+
@action_infos
|
799
|
+
end
|
800
|
+
|
801
|
+
def self.[](action_method_name)
|
802
|
+
#; [!1tq8z] returns ActionInfo object corresponding to action method.
|
803
|
+
#; [!6g2iw] returns nil when not mounted yet.
|
804
|
+
return (@action_infos || {})[action_method_name]
|
805
|
+
end
|
806
|
+
|
786
807
|
end
|
787
808
|
|
788
809
|
|
@@ -1001,6 +1022,85 @@ module K8
|
|
1001
1022
|
end
|
1002
1023
|
|
1003
1024
|
|
1025
|
+
##
|
1026
|
+
## ex:
|
1027
|
+
## info = ActionInfo.new('PUT', '/api/books/{id}')
|
1028
|
+
## p info.method #=> "PUT"
|
1029
|
+
## p info.urlpath(123) #=> "/api/books/123"
|
1030
|
+
## p info.form_action_attr(123) #=> "/api/books/123?_method=PUT"
|
1031
|
+
##
|
1032
|
+
class ActionInfo
|
1033
|
+
|
1034
|
+
def initialize(method, urlpath_format)
|
1035
|
+
@method = method
|
1036
|
+
@urlpath_format = urlpath_format # ex: '/books/%s/comments/%s'
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
attr_reader :method
|
1040
|
+
|
1041
|
+
def method(name=nil)
|
1042
|
+
return name ? super : @method
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
def urlpath(*args)
|
1046
|
+
return @urlpath_format % args
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def form_action_attr(*args)
|
1050
|
+
#; [!qyhkm] returns '/api/books/123' when method is POST.
|
1051
|
+
#; [!kogyx] returns '/api/books/123?_method=PUT' when method is not POST.
|
1052
|
+
if @method == 'POST'
|
1053
|
+
return urlpath(*args)
|
1054
|
+
else
|
1055
|
+
return "#{urlpath(*args)}?_method=#{@method}"
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
def self.create(meth, urlpath_pattern)
|
1060
|
+
## ex: '/books/{id}' -> '/books/%s'
|
1061
|
+
#; [!1nk0i] replaces urlpath parameters with '%s'.
|
1062
|
+
#; [!a7fqv] replaces '%' with'%%'.
|
1063
|
+
rexp = /(.*?)\{(\w*?)(?::[^{}]*(?:\{[^{}]*?\}[^{}]*?)*)?\}/
|
1064
|
+
urlpath_format = ''; n = 0
|
1065
|
+
urlpath_pattern.scan(rexp) do |text, pname|
|
1066
|
+
next if pname == 'ext' # ignore '.html' or '.json'
|
1067
|
+
urlpath_format << text.gsub(/%/, '%%') << '%s'
|
1068
|
+
n += 1
|
1069
|
+
end
|
1070
|
+
rest = n > 0 ? Regexp.last_match.post_match : urlpath_pattern
|
1071
|
+
urlpath_format << rest.gsub(/%/, '%%')
|
1072
|
+
#; [!btt2g] returns ActionInfoN object when number of urlpath parameter <= 4.
|
1073
|
+
#; [!x5yx2] returns ActionInfo object when number of urlpath parameter > 4.
|
1074
|
+
return (SUBCLASSES[n] || ActionInfo).new(meth, urlpath_format)
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
SUBCLASSES = [] # :nodoc:
|
1078
|
+
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
class ActionInfo0 < ActionInfo # :nodoc:
|
1082
|
+
def urlpath(); @urlpath_format; end
|
1083
|
+
end
|
1084
|
+
|
1085
|
+
class ActionInfo1 < ActionInfo # :nodoc:
|
1086
|
+
def urlpath(a); @urlpath_format % [a]; end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
class ActionInfo2 < ActionInfo # :nodoc:
|
1090
|
+
def urlpath(a, b); @urlpath_format % [a, b]; end
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
class ActionInfo3 < ActionInfo # :nodoc:
|
1094
|
+
def urlpath(a, b, c); @urlpath_format % [a, b, c]; end
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
class ActionInfo4 < ActionInfo # :nodoc:
|
1098
|
+
def urlpath(a, b, c, d); @urlpath_format % [a, b, c, d]; end
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
ActionInfo::SUBCLASSES << ActionInfo0 << ActionInfo1 << ActionInfo2 << ActionInfo3 << ActionInfo4
|
1102
|
+
|
1103
|
+
|
1004
1104
|
class DefaultPatterns
|
1005
1105
|
|
1006
1106
|
def initialize
|
@@ -1031,6 +1131,26 @@ module K8
|
|
1031
1131
|
end
|
1032
1132
|
|
1033
1133
|
|
1134
|
+
DEFAULT_PATTERNS = proc {
|
1135
|
+
x = DefaultPatterns.new
|
1136
|
+
#; [!i51id] registers '\d+' as default pattern of param 'id' or /_id\z/.
|
1137
|
+
x.register('id', '\d+') {|val| val.to_i }
|
1138
|
+
x.register(/_id\z/, '\d+') {|val| val.to_i }
|
1139
|
+
#; [!2g08b] registers '(?:\.\w+)?' as default pattern of param 'ext'.
|
1140
|
+
x.register('ext', '(?:\.\w+)?')
|
1141
|
+
#; [!8x5mp] registers '\d\d\d\d-\d\d-\d\d' as default pattern of param 'date' or /_date\z/.
|
1142
|
+
to_date = proc {|val|
|
1143
|
+
#; [!wg9vl] raises 404 error when invalid date (such as 2012-02-30).
|
1144
|
+
yr, mo, dy = val.split(/-/).map(&:to_i)
|
1145
|
+
Date.new(yr, mo, dy) rescue
|
1146
|
+
raise HttpException.new(404, "#{val}: invalid date.")
|
1147
|
+
}
|
1148
|
+
x.register('date', '\d\d\d\d-\d\d-\d\d', &to_date)
|
1149
|
+
x.register(/_date\z/, '\d\d\d\d-\d\d-\d\d', &to_date)
|
1150
|
+
x
|
1151
|
+
}.call()
|
1152
|
+
|
1153
|
+
|
1034
1154
|
class ActionMethodMapping
|
1035
1155
|
|
1036
1156
|
def initialize
|
@@ -1073,370 +1193,248 @@ module K8
|
|
1073
1193
|
end
|
1074
1194
|
|
1075
1195
|
|
1076
|
-
class
|
1077
|
-
|
1078
|
-
def initialize
|
1079
|
-
@mappings = []
|
1080
|
-
end
|
1196
|
+
class ActionMapping
|
1081
1197
|
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1198
|
+
def initialize(urlpath_mapping, default_patterns: DEFAULT_PATTERNS, urlpath_cache_size: 0,
|
1199
|
+
enable_urlpath_param_range: true)
|
1200
|
+
@default_patterns = default_patterns || DefaultPatterns.new
|
1201
|
+
#; [!34o67] keyword arg 'enable_urlpath_param_range' controls to generate range object or not.
|
1202
|
+
@enable_urlpath_param_range = enable_urlpath_param_range
|
1203
|
+
#; [!buj0d] prepares LRU cache if cache size specified.
|
1204
|
+
@urlpath_cache_size = urlpath_cache_size
|
1205
|
+
@urlpath_lru_cache = urlpath_cache_size > 0 ? {} : nil
|
1206
|
+
#; [!wsz8g] compiles urlpath mapping passed.
|
1207
|
+
compile(urlpath_mapping)
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
def compile(urlpath_mapping)
|
1211
|
+
#; [!6f3vl] compiles urlpath mapping.
|
1212
|
+
@fixed_endpoints = {} # urlpath patterns which have no urlpath params
|
1213
|
+
@variable_endpoints = [] # urlpath patterns which have any ulrpath param
|
1214
|
+
@all_endpoints = [] # all urlpath patterns (fixed + variable)
|
1215
|
+
rexp_str = _compile_array(urlpath_mapping, '', '')
|
1216
|
+
@urlpath_rexp = Regexp.compile("\\A#{rexp_str}\\z")
|
1094
1217
|
return self
|
1095
1218
|
end
|
1096
1219
|
|
1097
|
-
|
1098
|
-
child_mappings = nil
|
1099
|
-
#; [!4l8xl] can accept array of pairs of urlpath and action class.
|
1100
|
-
if action_class.is_a?(Array)
|
1101
|
-
array = action_class
|
1102
|
-
child_mappings = []
|
1103
|
-
array.each {|upath, klass| _mount(child_mappings, upath, klass) }
|
1104
|
-
action_class = nil
|
1105
|
-
#; [!ne804] when target class name is string...
|
1106
|
-
elsif action_class.is_a?(String)
|
1107
|
-
str = action_class
|
1108
|
-
action_class = _load_action_class(str, "mount('#{str}')")
|
1109
|
-
#; [!lvxyx] raises error when not an action class.
|
1110
|
-
else
|
1111
|
-
action_class.is_a?(Class) && action_class < BaseAction or
|
1112
|
-
raise ArgumentError.new("mount('#{urlpath_pattern}'): Action class expected but got: #{action_class.inspect}")
|
1113
|
-
end
|
1114
|
-
#; [!30cib] raises error when action method is not defined in action class.
|
1115
|
-
_validate_action_method_existence(action_class) if action_class
|
1116
|
-
#; [!flb11] mounts action class to urlpath.
|
1117
|
-
mappings << [urlpath_pattern, action_class || child_mappings]
|
1118
|
-
end
|
1119
|
-
private :_mount
|
1120
|
-
|
1121
|
-
def _validate_action_method_existence(action_class)
|
1122
|
-
action_class._action_method_mapping.each do |_, action_methods|
|
1123
|
-
action_methods.each do |req_meth, action_method_name|
|
1124
|
-
action_class.method_defined?(action_method_name) or
|
1125
|
-
raise UnknownActionMethodError.new("#{req_meth.inspect}=>#{action_method_name.inspect}: unknown action method in #{action_class}.")
|
1126
|
-
end
|
1127
|
-
end
|
1128
|
-
end
|
1220
|
+
EMPTY_ARRAY = [].freeze # :nodoc:
|
1129
1221
|
|
1130
|
-
def
|
1131
|
-
#; [!
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
#; [!vaazw] raises error when failed to load file.
|
1137
|
-
#; [!eiovd] raises original LoadError when it raises in loading file.
|
1138
|
-
begin
|
1139
|
-
require filepath
|
1140
|
-
rescue LoadError => ex
|
1141
|
-
raise unless ex.path == filepath
|
1142
|
-
raise ArgumentError.new("#{error}: failed to require file.")
|
1222
|
+
def lookup(req_urlpath)
|
1223
|
+
#; [!j34yh] finds from fixed urlpaths at first.
|
1224
|
+
if (tuple = @fixed_endpoints[req_urlpath])
|
1225
|
+
_, action_class, action_methods = tuple
|
1226
|
+
pnames = pvalues = EMPTY_ARRAY
|
1227
|
+
return action_class, action_methods, pnames, pvalues
|
1143
1228
|
end
|
1144
|
-
#; [!
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
raise ArgumentError.new("#{error}: no such action class.")
|
1229
|
+
#; [!uqwr7] uses LRU as cache algorithm.
|
1230
|
+
cache = @urlpath_lru_cache
|
1231
|
+
if cache && (result = cache.delete(req_urlpath))
|
1232
|
+
cache[req_urlpath] = result # delete & append to simulate LRU
|
1233
|
+
return result
|
1150
1234
|
end
|
1151
|
-
#; [!
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
yield :map, curr_urlpath_pat, upath_pat, action_class, action_methods
|
1175
|
-
end
|
1176
|
-
end
|
1177
|
-
yield :exit, base_urlpath_pat, urlpath_pattern, action_class, nil
|
1235
|
+
#; [!sos5i] returns nil when request path not matched to urlpath patterns.
|
1236
|
+
m = @urlpath_rexp.match(req_urlpath)
|
1237
|
+
return nil unless m
|
1238
|
+
#; [!95q61] finds from variable urlpath patterns when not found in fixed ones.
|
1239
|
+
index = m.captures.find_index('')
|
1240
|
+
tuple = @variable_endpoints[index]
|
1241
|
+
_, action_class, action_methods, urlpath_rexp, pnames, procs, range = tuple
|
1242
|
+
#; [!1k1k5] converts urlpath param values by converter procs.
|
1243
|
+
if range
|
1244
|
+
str = req_urlpath[range]
|
1245
|
+
pvalues = [procs[0] ? procs[0].call(str) : str]
|
1246
|
+
else
|
1247
|
+
strs = urlpath_rexp.match(req_urlpath).captures
|
1248
|
+
pvalues = \
|
1249
|
+
case procs.length
|
1250
|
+
when 1; [procs[0] ? procs[0].call(strs[0]) : strs[0]]
|
1251
|
+
when 2; [procs[0] ? procs[0].call(strs[0]) : strs[0],
|
1252
|
+
procs[1] ? procs[1].call(strs[1]) : strs[1]]
|
1253
|
+
when 3; [procs[0] ? procs[0].call(strs[0]) : strs[0],
|
1254
|
+
procs[1] ? procs[1].call(strs[1]) : strs[1],
|
1255
|
+
procs[2] ? procs[2].call(strs[2]) : strs[2]]
|
1256
|
+
else ; procs.zip(strs).map {|pr, v| pr ? pr.call(v) : v }
|
1257
|
+
end # ex: ["123"] -> [123]
|
1178
1258
|
end
|
1259
|
+
#; [!jyxlm] returns action class and methods, parameter names and values.
|
1260
|
+
result = [action_class, action_methods, pnames, pvalues]
|
1261
|
+
#; [!uqwr7] stores result into cache if cache is enabled.
|
1262
|
+
if cache
|
1263
|
+
cache[req_urlpath] = result
|
1264
|
+
#; [!3ps5g] deletes item from cache when cache size over limit.
|
1265
|
+
cache.shift() if cache.length > @urlpath_cache_size
|
1266
|
+
end
|
1267
|
+
#
|
1268
|
+
return result
|
1179
1269
|
end
|
1180
|
-
private :_traverse
|
1181
1270
|
|
1182
|
-
def
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
yield
|
1271
|
+
def each
|
1272
|
+
#; [!2gwru] returns Enumerator if block is not provided.
|
1273
|
+
return to_enum(:each) unless block_given?
|
1274
|
+
#; [!7ynne] yields each urlpath pattern, action class and action methods.
|
1275
|
+
@all_endpoints.each do |tuple|
|
1276
|
+
urlpath_pat, action_class, action_methods, _ = tuple
|
1277
|
+
yield urlpath_pat, action_class, action_methods
|
1189
1278
|
end
|
1190
1279
|
self
|
1191
1280
|
end
|
1192
1281
|
|
1193
|
-
end
|
1194
|
-
|
1195
|
-
|
1196
|
-
class ActionFinder
|
1197
|
-
|
1198
|
-
def initialize(action_class_mapping, default_patterns=nil, urlpath_cache_size: 0)
|
1199
|
-
@default_patterns = default_patterns || K8::DefaultPatterns.new
|
1200
|
-
@urlpath_cache_size = urlpath_cache_size
|
1201
|
-
#; [!wb9l8] enables urlpath cache when urlpath_cache_size > 0.
|
1202
|
-
@urlpath_cache = urlpath_cache_size > 0 ? {} : nil # LRU cache of variable urlpath
|
1203
|
-
#; [!dnu4q] calls '#_construct()'.
|
1204
|
-
_construct(action_class_mapping)
|
1205
|
-
end
|
1206
|
-
|
1207
1282
|
private
|
1208
1283
|
|
1209
|
-
def
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
## /\d+(\z) # ...(4)
|
1226
|
-
## | # ...(5)
|
1227
|
-
## /\d+/edit(\z) # ...(4)
|
1228
|
-
## ) # ...(6)
|
1229
|
-
## ) # ...(6)
|
1230
|
-
## | # ...(7)
|
1231
|
-
## /admin # ...(2)
|
1232
|
-
## (:? # ...(3)
|
1233
|
-
## ....
|
1234
|
-
## ) # ...(6)
|
1235
|
-
## ) # ...(8)
|
1236
|
-
##
|
1237
|
-
## Example of @dict (fixed urlpaths):
|
1238
|
-
## {
|
1239
|
-
## "/api/books" # ...(9)
|
1240
|
-
## => [BooksAction, {:GET=>:do_index, :POST=>:do_create}],
|
1241
|
-
## "/api/books/new"
|
1242
|
-
## => [BooksAction, {:GET=>:do_new}],
|
1243
|
-
## "/api/authors"
|
1244
|
-
## => [AuthorsAction, {:GET=>:do_index, :POST=>:do_create}],
|
1245
|
-
## "/api/authors/new"
|
1246
|
-
## => [AuthorsAction, {:GET=>:do_new}],
|
1247
|
-
## "/admin/books"
|
1248
|
-
## => ...
|
1249
|
-
## ...
|
1250
|
-
## }
|
1251
|
-
##
|
1252
|
-
## Example of @list (variable urlpaths):
|
1253
|
-
## [
|
1254
|
-
## [ # ...(10)
|
1255
|
-
## %r'\A/api/books/(\d+)\z',
|
1256
|
-
## ["id"], [proc {|x| x.to_i }],
|
1257
|
-
## BooksAction,
|
1258
|
-
## {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
|
1259
|
-
## ],
|
1260
|
-
## [
|
1261
|
-
## %r'\A/api/books/(\d+)/edit\z',
|
1262
|
-
## ["id"], [proc {|x| x.to_i }],
|
1263
|
-
## BooksAction,
|
1264
|
-
## {:GET=>:do_edit},
|
1265
|
-
## ],
|
1266
|
-
## ...
|
1267
|
-
## ]
|
1268
|
-
##
|
1269
|
-
@dict = {}
|
1270
|
-
@list = []
|
1271
|
-
#; [!956fi] builds regexp object for variable urlpaths (= containing urlpath params).
|
1272
|
-
buf = ['\A'] # ...(0)
|
1273
|
-
buf << '(?:' # ...(1)
|
1274
|
-
action_class_mapping.traverse do
|
1275
|
-
|event, base_urlpath_pat, urlpath_pat, action_class, action_methods|
|
1276
|
-
first_p = buf[-1] == '(?:'
|
1277
|
-
case event
|
1278
|
-
when :map
|
1279
|
-
full_urlpath_pat = "#{base_urlpath_pat}#{urlpath_pat}"
|
1280
|
-
if full_urlpath_pat =~ /\{.*?\}/
|
1281
|
-
buf << '|' unless first_p # ...(5)
|
1282
|
-
buf << _compile(urlpath_pat, '', '(\z)').first # ...(4)
|
1283
|
-
full_urlpath_rexp_str, param_names, param_converters = \
|
1284
|
-
_compile(full_urlpath_pat, '\A', '\z', true)
|
1285
|
-
#; [!sl9em] builds list of variable urlpaths (= containing urlpath params).
|
1286
|
-
@list << [Regexp.compile(full_urlpath_rexp_str),
|
1287
|
-
param_names, param_converters,
|
1288
|
-
action_class, action_methods] # ...(9)
|
1289
|
-
else
|
1290
|
-
#; [!6tgj5] builds dict of fixed urlpaths (= no urlpath params).
|
1291
|
-
@dict[full_urlpath_pat] = [action_class, action_methods] # ...(10)
|
1292
|
-
end
|
1293
|
-
when :enter
|
1294
|
-
buf << '|' unless first_p # ...(7)
|
1295
|
-
buf << _compile(urlpath_pat, '', '').first # ...(2)
|
1296
|
-
buf << '(?:' # ...(3)
|
1297
|
-
when :exit
|
1298
|
-
if first_p
|
1299
|
-
buf.pop() # '(?:'
|
1300
|
-
buf.pop() # urlpath
|
1301
|
-
buf.pop() if buf[-1] == '|'
|
1302
|
-
else
|
1303
|
-
buf << ')' # ...(6)
|
1304
|
-
end
|
1284
|
+
def _compile_array(mapping, base_urlpath_pat, urlpath_pat)
|
1285
|
+
buf = []
|
1286
|
+
curr_urlpath_pat = "#{base_urlpath_pat}#{urlpath_pat}"
|
1287
|
+
mapping.each do |child_urlpath_pat, target|
|
1288
|
+
child = child_urlpath_pat
|
1289
|
+
#; [!w45ad] can compile nested array.
|
1290
|
+
if target.is_a?(Array)
|
1291
|
+
buf << _compile_array(target, curr_urlpath_pat, child)
|
1292
|
+
#; [!wd2eb] accepts subclass of Action class.
|
1293
|
+
elsif target.is_a?(Class) && target < Action
|
1294
|
+
buf << _compile_class(target, curr_urlpath_pat, child)
|
1295
|
+
#; [!l2kz5] requires library when filepath and classname specified.
|
1296
|
+
elsif target.is_a?(String)
|
1297
|
+
klass = _require_action_class(target)
|
1298
|
+
buf << _compile_class(klass, curr_urlpath_pat, child)
|
1299
|
+
#; [!irt5g] raises TypeError when unknown object specified.
|
1305
1300
|
else
|
1306
|
-
raise "
|
1301
|
+
raise TypeError.new("Action class or nested array expected, but got #{target.inspect}")
|
1307
1302
|
end
|
1308
1303
|
end
|
1309
|
-
|
1310
|
-
|
1311
|
-
buf
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
#; [!
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
#; [!hy3y5] ex: '/{:xx|yy}' -> '/(?:xx|yy)'
|
1336
|
-
#; [!gunsm] ex: '/{id:xx|yy}' -> '/(xx|yy)'
|
1337
|
-
if named && grouping
|
1338
|
-
pat = "(#{pat})"
|
1339
|
-
elsif pat =~ /\|/
|
1340
|
-
pat = "(?:#{pat})"
|
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))'
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
def _compile_class(action_class, base_urlpath_pat, urlpath_pat)
|
1311
|
+
buf = []
|
1312
|
+
curr_urlpath_pat = "#{base_urlpath_pat}#{urlpath_pat}"
|
1313
|
+
action_class._action_method_mapping.each do |child_urlpath_pat, methods|
|
1314
|
+
#; [!ue766] raises error when action method is not defined in action class.
|
1315
|
+
_validate_action_method_existence(action_class, methods)
|
1316
|
+
#; ex: '/api/books/{id}' -> '\A/api/books/(\d+)\z', ['id'], [proc{|x| x.to_i}]
|
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
|
1341
1330
|
end
|
1342
|
-
|
1331
|
+
#
|
1332
|
+
@all_endpoints << tuple
|
1343
1333
|
end
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
return
|
1349
|
-
end
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
#; [!
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
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))'
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
## ex: '/books', ['/\d+', '/\d+/edit'] -> '/books(?:/\d+|/\d+/edit)'
|
1342
|
+
def _build_rexp_str(urlpath_pat, buf)
|
1343
|
+
return nil if buf.empty?
|
1344
|
+
prefix = _compile_urlpath_pat(urlpath_pat).first
|
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
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
#; [!92jcn] '{' and '}' are available in urlpath param pattern.
|
1353
|
+
#URLPATH_PARAM_REXP = /\{(\w*)(?::(.*?))?\}/
|
1354
|
+
#URLPATH_PARAM_REXP = /\{(\w*)(?::(.*?(?:\{.*?\}.*?)*))?\}/
|
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})"
|
1392
1382
|
end
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
return
|
1397
|
-
end
|
1398
|
-
|
1399
|
-
end
|
1400
|
-
|
1401
|
-
|
1402
|
-
## Router consists of urlpath mapping and finder.
|
1403
|
-
class ActionRouter
|
1404
|
-
|
1405
|
-
def initialize(urlpath_cache_size: 0)
|
1406
|
-
@mapping = ActionClassMapping.new
|
1407
|
-
@default_patterns = DefaultPatterns.new
|
1408
|
-
@finder = nil
|
1409
|
-
#; [!l1elt] saves finder options.
|
1410
|
-
@finder_opts = {:urlpath_cache_size=>urlpath_cache_size}
|
1411
|
-
end
|
1412
|
-
|
1413
|
-
attr_reader :default_patterns
|
1414
|
-
|
1415
|
-
def register(urlpath_param_name, default_pattern='[^/]*?', &converter)
|
1416
|
-
#; [!boq80] registers urlpath param pattern and converter.
|
1417
|
-
@default_patterns.register(urlpath_param_name, default_pattern, &converter)
|
1418
|
-
self
|
1419
|
-
end
|
1420
|
-
|
1421
|
-
def mount(urlpath_pattern, action_class)
|
1422
|
-
#; [!uc996] mouts action class to urlpath.
|
1423
|
-
@mapping.mount(urlpath_pattern, action_class)
|
1424
|
-
#; [!trs6w] removes finder object.
|
1425
|
-
@finder = nil
|
1426
|
-
self
|
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}]}]
|
1427
1387
|
end
|
1428
1388
|
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1389
|
+
## raises error when action method is not defined in action class
|
1390
|
+
def _validate_action_method_existence(action_class, action_methods)
|
1391
|
+
action_methods.each do |req_meth, action_method_name|
|
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
|
1432
1395
|
end
|
1433
1396
|
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1397
|
+
## range object to retrieve urlpath parameter value faster than Regexp matching
|
1398
|
+
## ex:
|
1399
|
+
## urlpath_pat == '/books/{id}/edit'
|
1400
|
+
## arr = urlpath_pat.split(/\{.*?\}/, -1) #=> ['/books/', '/edit']
|
1401
|
+
## range = (arr[0].length .. - (arr[01].length+1)) #=> 7..-6 (Range object)
|
1402
|
+
## p "/books/123/edit"[range] #=> '123'
|
1403
|
+
def _range_of_urlpath_param(urlpath_pattern)
|
1404
|
+
rexp = URLPATH_PARAM_REXP_NOGROUP
|
1405
|
+
arr = urlpath_pattern.split(rexp, -1) # ex: ['/books/', '/edit']
|
1406
|
+
return nil unless arr.length == 2
|
1407
|
+
return (arr[0].length .. - (arr[1].length+1)) # ex: 7..-6 (Range object)
|
1408
|
+
end
|
1409
|
+
|
1410
|
+
## ex: './api/admin/books:Admin::BookAPI' -> Admin::BookAPI
|
1411
|
+
def _require_action_class(filepath_and_classname)
|
1412
|
+
#; [!px9jy] requires file and finds class object.
|
1413
|
+
str = filepath_and_classname # ex: './admin/api/book:Admin::BookAPI'
|
1414
|
+
filepath, classname = filepath_and_classname.split(':', 2)
|
1415
|
+
begin
|
1416
|
+
require filepath
|
1417
|
+
rescue LoadError => ex
|
1418
|
+
#; [!dlcks] don't rescue LoadError when it is not related to argument.
|
1419
|
+
raise unless ex.path == filepath
|
1420
|
+
#; [!mngjz] raises error when failed to load file.
|
1421
|
+
raise LoadError.new("'#{str}': cannot load '#{filepath}'.")
|
1422
|
+
end
|
1423
|
+
#; [!8n6pf] class name may have module prefix name.
|
1424
|
+
#; [!6lv7l] raises error when action class not found.
|
1425
|
+
begin
|
1426
|
+
klass = classname.split('::').inject(Object) {|cls, x| cls.const_get(x) }
|
1427
|
+
rescue NameError
|
1428
|
+
raise NameError.new("'#{str}': class not found (#{classname}).")
|
1429
|
+
end
|
1430
|
+
#; [!thf7t] raises TypeError when not a class.
|
1431
|
+
klass.is_a?(Class) or
|
1432
|
+
raise TypeError.new("'#{str}': class name expected but got #{klass.inspect}.")
|
1433
|
+
#; [!yqcgx] raises TypeError when not a subclass of K8::Action.
|
1434
|
+
klass < Action or
|
1435
|
+
raise TypeError.new("'#{str}': expected subclass of K8::Action but not.")
|
1436
|
+
#
|
1437
|
+
return klass
|
1440
1438
|
end
|
1441
1439
|
|
1442
1440
|
end
|
@@ -1444,52 +1442,18 @@ module K8
|
|
1444
1442
|
|
1445
1443
|
class RackApplication
|
1446
1444
|
|
1447
|
-
def initialize(urlpath_mapping=[], urlpath_cache_size: 0
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
end
|
1455
|
-
|
1456
|
-
def init_default_param_patterns(default_patterns)
|
1457
|
-
#; [!i51id] registers '\d+' as default pattern of param 'id' or /_id\z/.
|
1458
|
-
x = default_patterns
|
1459
|
-
x.register('id', '\d+') {|val| val.to_i }
|
1460
|
-
x.register(/_id\z/, '\d+') {|val| val.to_i }
|
1461
|
-
#; [!2g08b] registers '(?:\.\w+)?' as default pattern of param 'ext'.
|
1462
|
-
x.register('ext', '(?:\.\w+)?')
|
1463
|
-
#; [!8x5mp] registers '\d\d\d\d-\d\d-\d\d' as default pattern of param 'date' or /_date\z/.
|
1464
|
-
to_date = proc {|val|
|
1465
|
-
#; [!wg9vl] raises 404 error when invalid date (such as 2012-02-30).
|
1466
|
-
yr, mo, dy = val.split(/-/).map(&:to_i)
|
1467
|
-
Date.new(yr, mo, dy) rescue
|
1468
|
-
raise HttpException.new(404, "#{val}: invalid date.")
|
1469
|
-
}
|
1470
|
-
x.register('date', '\d\d\d\d-\d\d-\d\d', &to_date)
|
1471
|
-
x.register(/_date\z/, '\d\d\d\d-\d\d-\d\d', &to_date)
|
1445
|
+
def initialize(urlpath_mapping=[], default_patterns: DEFAULT_PATTERNS, urlpath_cache_size: 0,
|
1446
|
+
enable_urlpath_param_range: true)
|
1447
|
+
#; [!vkp65] mounts urlpath mappings.
|
1448
|
+
@mapping = ActionMapping.new(urlpath_mapping,
|
1449
|
+
default_patterns: default_patterns,
|
1450
|
+
urlpath_cache_size: urlpath_cache_size,
|
1451
|
+
enable_urlpath_param_range: enable_urlpath_param_range)
|
1472
1452
|
end
|
1473
|
-
protected :init_default_param_patterns
|
1474
1453
|
|
1475
|
-
|
1476
|
-
## ex:
|
1477
|
-
## mount '/', WelcomeAction
|
1478
|
-
## mount '/books', BooksAction
|
1479
|
-
## mount '/admin', [
|
1480
|
-
## ['/session', AdminSessionAction],
|
1481
|
-
## ['/books', AdminBooksAction],
|
1482
|
-
## ]
|
1483
|
-
##
|
1484
|
-
def mount(urlpath_pattern, action_class_or_array)
|
1485
|
-
#; [!zwva6] mounts action class to urlpath pattern.
|
1486
|
-
@router.mount(urlpath_pattern, action_class_or_array)
|
1487
|
-
return self
|
1488
|
-
end
|
1489
|
-
|
1490
|
-
def find(req_path)
|
1454
|
+
def lookup(req_path)
|
1491
1455
|
#; [!o0rnr] returns action class, action methods, urlpath names and values.
|
1492
|
-
return @
|
1456
|
+
return @mapping.lookup(req_path)
|
1493
1457
|
end
|
1494
1458
|
|
1495
1459
|
def call(env)
|
@@ -1512,8 +1476,16 @@ module K8
|
|
1512
1476
|
req_meth_ = req_meth
|
1513
1477
|
end
|
1514
1478
|
begin
|
1479
|
+
tuple4 = lookup(req.path)
|
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
|
1515
1487
|
#; [!rz13i] returns HTTP 404 when urlpath not found.
|
1516
|
-
tuple4
|
1488
|
+
tuple4 or
|
1517
1489
|
raise HttpException.new(404)
|
1518
1490
|
action_class, action_methods, urlpath_param_names, urlpath_param_values = tuple4
|
1519
1491
|
#; [!rv3cf] returns HTTP 405 when urlpath found but request method not allowed.
|
@@ -1579,11 +1551,19 @@ END
|
|
1579
1551
|
return false
|
1580
1552
|
end
|
1581
1553
|
|
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
|
+
|
1582
1562
|
public
|
1583
1563
|
|
1584
1564
|
def each_mapping(&block)
|
1585
1565
|
#; [!cgjyv] yields full urlpath pattern, action class and action methods.
|
1586
|
-
@
|
1566
|
+
@mapping.each(&block)
|
1587
1567
|
self
|
1588
1568
|
end
|
1589
1569
|
|
@@ -1789,245 +1769,4 @@ END
|
|
1789
1769
|
end
|
1790
1770
|
|
1791
1771
|
|
1792
|
-
module Mock
|
1793
|
-
|
1794
|
-
|
1795
|
-
module_function
|
1796
|
-
|
1797
|
-
def new_env(meth=:GET, path="/", query: nil, form: nil, multipart: nil, json: nil, input: nil, headers: nil, cookie: nil, env: nil)
|
1798
|
-
#uri = "http://localhost:80#{path}"
|
1799
|
-
#opts["REQUEST_METHOD"] = meth
|
1800
|
-
#env = Rack::MockRequest.env_for(uri, opts)
|
1801
|
-
require 'stringio' unless defined?(StringIO)
|
1802
|
-
https = env && (env['rack.url_scheme'] == 'https' || env['HTTPS'] == 'on')
|
1803
|
-
#
|
1804
|
-
err = proc {|a, b|
|
1805
|
-
ArgumentError.new("new_env(): not allowed both '#{a}' and '#{b}' at a time.")
|
1806
|
-
}
|
1807
|
-
ctype = nil
|
1808
|
-
if form
|
1809
|
-
#; [!c779l] raises ArgumentError when both form and json are specified.
|
1810
|
-
! json or raise err.call('form', 'json')
|
1811
|
-
input = Util.build_query_string(form)
|
1812
|
-
ctype = "application/x-www-form-urlencoded"
|
1813
|
-
end
|
1814
|
-
if json
|
1815
|
-
! multipart or raise err.call('json', 'multipart')
|
1816
|
-
input = json.is_a?(String) ? json : JSON.dump(json)
|
1817
|
-
ctype = "application/json"
|
1818
|
-
end
|
1819
|
-
if multipart
|
1820
|
-
! form or raise err.call('multipart', 'form')
|
1821
|
-
#; [!gko8g] 'multipart:' kwarg accepts Hash object (which is converted into multipart data).
|
1822
|
-
if multipart.is_a?(Hash)
|
1823
|
-
dict = multipart
|
1824
|
-
multipart = dict.each_with_object(MultipartBuilder.new) do |(k, v), mp|
|
1825
|
-
v.is_a?(File) ? mp.add_file(k, v) : mp.add(k, v.to_s)
|
1826
|
-
end
|
1827
|
-
end
|
1828
|
-
input = multipart.to_s
|
1829
|
-
m = /\A--(\S+)\r\n/.match(input) or
|
1830
|
-
raise ArgumentError.new("invalid multipart format.")
|
1831
|
-
boundary = $1
|
1832
|
-
ctype = "multipart/form-data; boundary=#{boundary}"
|
1833
|
-
end
|
1834
|
-
environ = {
|
1835
|
-
"rack.version" => [1, 3],
|
1836
|
-
"rack.input" => StringIO.new(input || ""),
|
1837
|
-
"rack.errors" => StringIO.new,
|
1838
|
-
"rack.multithread" => true,
|
1839
|
-
"rack.multiprocess" => true,
|
1840
|
-
"rack.run_once" => false,
|
1841
|
-
"rack.url_scheme" => https ? "https" : "http",
|
1842
|
-
"REQUEST_METHOD" => meth.to_s,
|
1843
|
-
"SERVER_NAME" => "localhost",
|
1844
|
-
"SERVER_PORT" => https ? "443" : "80",
|
1845
|
-
"QUERY_STRING" => Util.build_query_string(query || ""),
|
1846
|
-
"PATH_INFO" => path,
|
1847
|
-
"HTTPS" => https ? "on" : "off",
|
1848
|
-
"SCRIPT_NAME" => "",
|
1849
|
-
"CONTENT_LENGTH" => (input ? input.bytesize.to_s : "0"),
|
1850
|
-
"CONTENT_TYPE" => ctype,
|
1851
|
-
}
|
1852
|
-
environ.delete("CONTENT_TYPE") if environ["CONTENT_TYPE"].nil?
|
1853
|
-
headers.each do |name, value|
|
1854
|
-
name =~ /\A[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\z/ or
|
1855
|
-
raise ArgumentError.new("invalid http header name: #{name.inspect}")
|
1856
|
-
value.is_a?(String) or
|
1857
|
-
raise ArgumentError.new("http header value should be a string but got: #{value.inspect}")
|
1858
|
-
## ex: 'X-Requested-With' -> 'HTTP_X_REQUESTED_WITH'
|
1859
|
-
k = "HTTP_#{name.upcase.gsub(/-/, '_')}"
|
1860
|
-
environ[k] = value
|
1861
|
-
end if headers
|
1862
|
-
env.each do |name, value|
|
1863
|
-
case name
|
1864
|
-
when /\Arack\./
|
1865
|
-
# ok
|
1866
|
-
when /\A[A-Z]+(_[A-Z0-9]+)*\z/
|
1867
|
-
value.is_a?(String) or
|
1868
|
-
raise ArgumentError.new("rack env value should be a string but got: #{value.inspect}")
|
1869
|
-
else
|
1870
|
-
raise ArgumentError.new("invalid rack env key: #{name}")
|
1871
|
-
end
|
1872
|
-
environ[name] = value
|
1873
|
-
end if env
|
1874
|
-
if cookie
|
1875
|
-
s = ! cookie.is_a?(Hash) ? cookie.to_s : cookie.map {|k, v|
|
1876
|
-
"#{Util.percent_encode(k)}=#{Util.percent_encode(v)}"
|
1877
|
-
}.join('; ')
|
1878
|
-
s = "#{environ['HTTP_COOKIE']}; #{s}" if environ['HTTP_COOKIE']
|
1879
|
-
environ['HTTP_COOKIE'] = s
|
1880
|
-
end
|
1881
|
-
return environ
|
1882
|
-
end
|
1883
|
-
|
1884
|
-
|
1885
|
-
class MultipartBuilder
|
1886
|
-
|
1887
|
-
def initialize(boundary=nil)
|
1888
|
-
#; [!ajfgl] sets random string as boundary when boundary is nil.
|
1889
|
-
@boundary = boundary || Util.randstr_b64()
|
1890
|
-
@params = []
|
1891
|
-
end
|
1892
|
-
|
1893
|
-
attr_reader :boundary
|
1894
|
-
|
1895
|
-
def add(name, value, filename=nil, content_type=nil)
|
1896
|
-
#; [!tp4bk] detects content type from filename when filename is not nil.
|
1897
|
-
content_type ||= Util.guess_content_type(filename) if filename
|
1898
|
-
@params << [name, value, filename, content_type]
|
1899
|
-
self
|
1900
|
-
end
|
1901
|
-
|
1902
|
-
def add_file(name, file, content_type=nil)
|
1903
|
-
#; [!uafqa] detects content type from filename when content type is not provided.
|
1904
|
-
content_type ||= Util.guess_content_type(file.path)
|
1905
|
-
#; [!b5811] reads file content and adds it as param value.
|
1906
|
-
add(name, file.read(), File.basename(file.path), content_type)
|
1907
|
-
#; [!36bsu] closes opened file automatically.
|
1908
|
-
file.close()
|
1909
|
-
self
|
1910
|
-
end
|
1911
|
-
|
1912
|
-
def to_s
|
1913
|
-
#; [!61gc4] returns multipart form string.
|
1914
|
-
boundary = @boundary
|
1915
|
-
s = "".force_encoding('ASCII-8BIT')
|
1916
|
-
@params.each do |name, value, filename, content_type|
|
1917
|
-
s << "--#{boundary}\r\n"
|
1918
|
-
if filename
|
1919
|
-
s << "Content-Disposition: form-data; name=\"#{name}\"; filename=\"#{filename}\"\r\n"
|
1920
|
-
else
|
1921
|
-
s << "Content-Disposition: form-data; name=\"#{name}\"\r\n"
|
1922
|
-
end
|
1923
|
-
s << "Content-Type: #{content_type}\r\n" if content_type
|
1924
|
-
s << "\r\n"
|
1925
|
-
s << value.force_encoding('ASCII-8BIT')
|
1926
|
-
s << "\r\n"
|
1927
|
-
end
|
1928
|
-
s << "--#{boundary}--\r\n"
|
1929
|
-
return s
|
1930
|
-
end
|
1931
|
-
|
1932
|
-
end
|
1933
|
-
|
1934
|
-
|
1935
|
-
##
|
1936
|
-
## Wrapper class to test Rack application.
|
1937
|
-
##
|
1938
|
-
## ex:
|
1939
|
-
## http = K8::Mock::TestApp.new(app)
|
1940
|
-
## resp = http.GET('/api/hello', query={'name'=>'World'})
|
1941
|
-
## assert_equal 200, resp.status
|
1942
|
-
## assert_equal "application/json", resp.headers['Content-Type']
|
1943
|
-
## assert_equal {"message"=>"Hello World!"}, resp.body_json
|
1944
|
-
##
|
1945
|
-
class TestApp
|
1946
|
-
|
1947
|
-
def initialize(app=nil)
|
1948
|
-
@app = app
|
1949
|
-
end
|
1950
|
-
|
1951
|
-
def request(meth, path, query: nil, form: nil, multipart: nil, json: nil, input: nil, headers: nil, cookie: nil, env: nil)
|
1952
|
-
#; [!4xpwa] creates env object and calls app with it.
|
1953
|
-
env = K8::Mock.new_env(meth, path, query: query, form: form, multipart: multipart, json: json, input: input, headers: headers, cookie: cookie, env: env)
|
1954
|
-
status, headers, body = @app.call(env)
|
1955
|
-
return TestResponse.new(status, headers, body)
|
1956
|
-
end
|
1957
|
-
|
1958
|
-
def GET path, kwargs={}; request(:GET , path, kwargs); end
|
1959
|
-
def POST path, kwargs={}; request(:POST , path, kwargs); end
|
1960
|
-
def PUT path, kwargs={}; request(:PUT , path, kwargs); end
|
1961
|
-
def DELETE path, kwargs={}; request(:DELETE , path, kwargs); end
|
1962
|
-
def HEAD path, kwargs={}; request(:HEAD , path, kwargs); end
|
1963
|
-
def PATCH path, kwargs={}; request(:PATCH , path, kwargs); end
|
1964
|
-
def OPTIONS path, kwargs={}; request(:OPTIONS, path, kwargs); end
|
1965
|
-
def TRACE path, kwargs={}; request(:TRACE , path, kwargs); end
|
1966
|
-
|
1967
|
-
end
|
1968
|
-
|
1969
|
-
|
1970
|
-
class TestResponse
|
1971
|
-
|
1972
|
-
def initialize(status, headers, body)
|
1973
|
-
@status = status
|
1974
|
-
@headers = headers
|
1975
|
-
@body = body
|
1976
|
-
end
|
1977
|
-
|
1978
|
-
attr_accessor :status, :headers, :body
|
1979
|
-
|
1980
|
-
def body_binary
|
1981
|
-
#; [!mb0i4] returns body as binary string.
|
1982
|
-
s = @body.join()
|
1983
|
-
@body.close() if @body.respond_to?(:close)
|
1984
|
-
return s
|
1985
|
-
end
|
1986
|
-
|
1987
|
-
def body_text
|
1988
|
-
#; [!rr18d] error when 'Content-Type' header is missing.
|
1989
|
-
ctype = @headers['Content-Type'] or
|
1990
|
-
raise "body_text(): missing 'Content-Type' header."
|
1991
|
-
#; [!dou1n] converts body text according to 'charset' in 'Content-Type' header.
|
1992
|
-
if ctype =~ /; *charset=(\w[-\w]*)/
|
1993
|
-
charset = $1
|
1994
|
-
#; [!cxje7] assumes charset as 'utf-8' when 'Content-Type' is json.
|
1995
|
-
elsif ctype == "application/json"
|
1996
|
-
charset = 'utf-8'
|
1997
|
-
#; [!n4c71] error when non-json 'Content-Type' header has no 'charset'.
|
1998
|
-
else
|
1999
|
-
raise "body_text(): missing 'charset' in 'Content-Type' header."
|
2000
|
-
end
|
2001
|
-
#; [!vkj9h] returns body as text string, according to 'charset' in 'Content-Type'.
|
2002
|
-
return body_binary().force_encoding(charset)
|
2003
|
-
end
|
2004
|
-
|
2005
|
-
def body_json
|
2006
|
-
#; [!qnic1] returns Hash object representing JSON string.
|
2007
|
-
return JSON.parse(body_text())
|
2008
|
-
end
|
2009
|
-
|
2010
|
-
def content_type
|
2011
|
-
#; [!40hcz] returns 'Content-Type' header value.
|
2012
|
-
return @headers['Content-Type']
|
2013
|
-
end
|
2014
|
-
|
2015
|
-
def content_length
|
2016
|
-
#; [!5lb19] returns 'Content-Length' header value as integer.
|
2017
|
-
#; [!qjktz] returns nil when 'Content-Length' is not set.
|
2018
|
-
len = @headers['Content-Length']
|
2019
|
-
return len ? len.to_i : len
|
2020
|
-
end
|
2021
|
-
|
2022
|
-
def location
|
2023
|
-
#; [!8y8lg] returns 'Location' header value.p
|
2024
|
-
return @headers['Location']
|
2025
|
-
end
|
2026
|
-
|
2027
|
-
end
|
2028
|
-
|
2029
|
-
|
2030
|
-
end
|
2031
|
-
|
2032
|
-
|
2033
1772
|
end
|