keight 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|