rest-core 1.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +6 -7
- data/CHANGES.md +137 -0
- data/Gemfile +1 -1
- data/README.md +183 -191
- data/TODO.md +5 -8
- data/example/multi.rb +31 -24
- data/example/simple.rb +28 -0
- data/example/use-cases.rb +194 -0
- data/lib/rest-core.rb +26 -19
- data/lib/rest-core/builder.rb +2 -2
- data/lib/rest-core/client.rb +40 -27
- data/lib/rest-core/client/universal.rb +16 -13
- data/lib/rest-core/client_oauth1.rb +5 -5
- data/lib/rest-core/engine/auto.rb +25 -0
- data/lib/rest-core/{app → engine}/dry.rb +1 -2
- data/lib/rest-core/engine/em-http-request.rb +39 -0
- data/lib/rest-core/engine/future/future.rb +106 -0
- data/lib/rest-core/engine/future/future_fiber.rb +39 -0
- data/lib/rest-core/engine/future/future_thread.rb +29 -0
- data/lib/rest-core/engine/rest-client.rb +56 -0
- data/lib/rest-core/middleware.rb +27 -5
- data/lib/rest-core/middleware/auth_basic.rb +5 -5
- data/lib/rest-core/middleware/bypass.rb +2 -2
- data/lib/rest-core/middleware/cache.rb +67 -54
- data/lib/rest-core/middleware/common_logger.rb +5 -8
- data/lib/rest-core/middleware/default_headers.rb +2 -2
- data/lib/rest-core/middleware/default_payload.rb +26 -2
- data/lib/rest-core/middleware/default_query.rb +4 -2
- data/lib/rest-core/middleware/default_site.rb +8 -6
- data/lib/rest-core/middleware/error_detector.rb +9 -16
- data/lib/rest-core/middleware/error_handler.rb +25 -11
- data/lib/rest-core/middleware/follow_redirect.rb +11 -14
- data/lib/rest-core/middleware/json_request.rb +19 -0
- data/lib/rest-core/middleware/json_response.rb +28 -0
- data/lib/rest-core/middleware/oauth1_header.rb +2 -7
- data/lib/rest-core/middleware/oauth2_header.rb +4 -7
- data/lib/rest-core/middleware/oauth2_query.rb +2 -2
- data/lib/rest-core/middleware/timeout.rb +21 -65
- data/lib/rest-core/middleware/timeout/{eventmachine_timer.rb → timer_em.rb} +3 -1
- data/lib/rest-core/middleware/timeout/timer_thread.rb +36 -0
- data/lib/rest-core/patch/multi_json.rb +8 -0
- data/lib/rest-core/test.rb +3 -12
- data/lib/rest-core/util/json.rb +65 -0
- data/lib/rest-core/util/parse_query.rb +2 -2
- data/lib/rest-core/version.rb +1 -1
- data/lib/rest-core/wrapper.rb +16 -16
- data/rest-core.gemspec +28 -27
- data/test/test_auth_basic.rb +14 -10
- data/test/test_builder.rb +7 -7
- data/test/test_cache.rb +126 -37
- data/test/test_client.rb +3 -1
- data/test/test_client_oauth1.rb +2 -3
- data/test/test_default_query.rb +17 -23
- data/test/test_em_http_request.rb +146 -0
- data/test/test_error_detector.rb +0 -1
- data/test/test_error_handler.rb +44 -0
- data/test/test_follow_redirect.rb +17 -19
- data/test/test_json_request.rb +28 -0
- data/test/test_json_response.rb +51 -0
- data/test/test_oauth1_header.rb +4 -4
- data/test/test_payload.rb +20 -12
- data/test/test_simple.rb +14 -0
- data/test/test_timeout.rb +11 -19
- data/test/test_universal.rb +5 -5
- data/test/test_wrapper.rb +19 -13
- metadata +28 -29
- data/doc/ToC.md +0 -7
- data/doc/dependency.md +0 -4
- data/doc/design.md +0 -4
- data/example/auto.rb +0 -51
- data/example/coolio.rb +0 -21
- data/example/eventmachine.rb +0 -30
- data/example/rest-client.rb +0 -16
- data/lib/rest-core/app/abstract/async_fiber.rb +0 -13
- data/lib/rest-core/app/auto.rb +0 -23
- data/lib/rest-core/app/coolio-async.rb +0 -32
- data/lib/rest-core/app/coolio-fiber.rb +0 -30
- data/lib/rest-core/app/coolio.rb +0 -9
- data/lib/rest-core/app/em-http-request-async.rb +0 -37
- data/lib/rest-core/app/em-http-request-fiber.rb +0 -45
- data/lib/rest-core/app/em-http-request.rb +0 -9
- data/lib/rest-core/app/rest-client.rb +0 -41
- data/lib/rest-core/middleware/json_decode.rb +0 -93
- data/lib/rest-core/middleware/timeout/coolio_timer.rb +0 -10
- data/pending/test_multi.rb +0 -123
- data/pending/test_test_util.rb +0 -86
- data/test/test_json_decode.rb +0 -24
@@ -1,18 +1,21 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
module RestCore
|
3
|
+
Universal = Builder.client do
|
4
|
+
use Timeout , 0
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
use DefaultSite , nil
|
7
|
+
use DefaultHeaders, {}
|
8
|
+
use DefaultQuery , {}
|
9
|
+
use DefaultPayload, {}
|
10
|
+
use JsonRequest , false
|
11
|
+
use AuthBasic , nil, nil
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
use FollowRedirect, 10
|
14
|
+
use CommonLogger , method(:puts)
|
15
|
+
use Cache , {}, 600 do
|
16
|
+
use ErrorHandler, nil
|
17
|
+
use ErrorDetectorHttp
|
18
|
+
use JsonResponse, false
|
19
|
+
end
|
17
20
|
end
|
18
21
|
end
|
@@ -6,7 +6,7 @@ module RestCore::ClientOauth1
|
|
6
6
|
|
7
7
|
def authorize_url! opts={}
|
8
8
|
self.data = ParseQuery.parse_query(
|
9
|
-
post(request_token_path, {}, {}, {:
|
9
|
+
post(request_token_path, {}, {}, {:json_response => false}.merge(opts)))
|
10
10
|
|
11
11
|
authorize_url
|
12
12
|
end
|
@@ -17,7 +17,7 @@ module RestCore::ClientOauth1
|
|
17
17
|
|
18
18
|
def authorize! opts={}
|
19
19
|
self.data = ParseQuery.parse_query(
|
20
|
-
post(access_token_path, {}, {}, {:
|
20
|
+
post(access_token_path, {}, {}, {:json_response => false}.merge(opts)))
|
21
21
|
|
22
22
|
data['authorized'] = 'true'
|
23
23
|
data
|
@@ -28,12 +28,12 @@ module RestCore::ClientOauth1
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def data_json
|
31
|
-
|
31
|
+
Json.encode(data.merge('sig' => calculate_sig))
|
32
32
|
end
|
33
33
|
|
34
34
|
def data_json= json
|
35
|
-
self.data = check_sig_and_return_data(
|
36
|
-
rescue
|
35
|
+
self.data = check_sig_and_return_data(Json.decode(json))
|
36
|
+
rescue Json.const_get(:ParseError)
|
37
37
|
self.data = nil
|
38
38
|
end
|
39
39
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
require 'rest-core/middleware'
|
3
|
+
|
4
|
+
class RestCore::Auto
|
5
|
+
include RestCore::Middleware
|
6
|
+
def call env, &k
|
7
|
+
client = http_client
|
8
|
+
client.call(log(env, "Auto picked: #{client.class}"), &k)
|
9
|
+
end
|
10
|
+
|
11
|
+
def http_client
|
12
|
+
if Object.const_defined?(:EventMachine) &&
|
13
|
+
::EventMachine.const_defined?(:HttpRequest) &&
|
14
|
+
::EventMachine.reactor_running? &&
|
15
|
+
# it should be either wrapped around a thread or a fiber
|
16
|
+
((Thread.main != Thread.current) ||
|
17
|
+
(Fiber.respond_to?(:current) && RootFiber != Fiber.current))
|
18
|
+
|
19
|
+
@emhttprequest ||= RestCore::EmHttpRequest.new
|
20
|
+
|
21
|
+
else
|
22
|
+
@restclient ||= RestCore::RestClient.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'em-http-request'
|
3
|
+
require 'restclient/payload'
|
4
|
+
|
5
|
+
require 'rest-core/engine/future/future'
|
6
|
+
require 'rest-core/middleware'
|
7
|
+
|
8
|
+
class RestCore::EmHttpRequest
|
9
|
+
include RestCore::Middleware
|
10
|
+
def call env, &k
|
11
|
+
future = Future.create(env, k, env[ASYNC])
|
12
|
+
payload = ::RestClient::Payload.generate(env[REQUEST_PAYLOAD])
|
13
|
+
client = ::EventMachine::HttpRequest.new(request_uri(env)).send(
|
14
|
+
env[REQUEST_METHOD],
|
15
|
+
:body => payload && payload.read,
|
16
|
+
:head => payload && payload.headers.
|
17
|
+
merge(env[REQUEST_HEADERS]))
|
18
|
+
|
19
|
+
client.callback{
|
20
|
+
future.wrap{ # callbacks are run in main thread, so we need to wrap it
|
21
|
+
future.on_load(client.response,
|
22
|
+
client.response_header.status,
|
23
|
+
client.response_header)}}
|
24
|
+
|
25
|
+
client.errback{future.wrap{ future.on_error(client.error) }}
|
26
|
+
|
27
|
+
env[TIMER].on_timeout{
|
28
|
+
(client.instance_variable_get(:@callbacks)||[]).clear
|
29
|
+
(client.instance_variable_get(:@errbacks )||[]).clear
|
30
|
+
client.close
|
31
|
+
future.wrap{ future.on_error(env[TIMER].error) }
|
32
|
+
} if env[TIMER]
|
33
|
+
|
34
|
+
env.merge(RESPONSE_BODY => future.proxy_body,
|
35
|
+
RESPONSE_STATUS => future.proxy_status,
|
36
|
+
RESPONSE_HEADERS => future.proxy_headers,
|
37
|
+
FUTURE => future)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
|
2
|
+
require 'rest-core'
|
3
|
+
|
4
|
+
class RestCore::Future
|
5
|
+
include RestCore
|
6
|
+
|
7
|
+
class Proxy < BasicObject
|
8
|
+
def initialize future, target
|
9
|
+
@future, @target = future, target
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing msg, *args, &block
|
13
|
+
@future.yield[@target].__send__(msg, *args, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create *args, &block
|
18
|
+
if Fiber.respond_to?(:current) && RootFiber != Fiber.current &&
|
19
|
+
# because under a thread, Fiber.current won't return the root fiber
|
20
|
+
Thread.main == Thread.current
|
21
|
+
FutureFiber .new(*args, &block)
|
22
|
+
else
|
23
|
+
FutureThread.new(*args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize env, k, immediate
|
28
|
+
self.env = env
|
29
|
+
self.k = k
|
30
|
+
self.immediate = immediate
|
31
|
+
self.response, self.body, self.status, self.headers, self.error = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def proxy_body ; Proxy.new(self, RESPONSE_BODY ); end
|
35
|
+
def proxy_status ; Proxy.new(self, RESPONSE_STATUS ); end
|
36
|
+
def proxy_headers; Proxy.new(self, RESPONSE_HEADERS); end
|
37
|
+
|
38
|
+
def wrap ; raise NotImplementedError; end
|
39
|
+
def wait ; raise NotImplementedError; end
|
40
|
+
def resume; raise NotImplementedError; end
|
41
|
+
|
42
|
+
def loaded?
|
43
|
+
!!status
|
44
|
+
end
|
45
|
+
|
46
|
+
def yield
|
47
|
+
wait
|
48
|
+
callback
|
49
|
+
end
|
50
|
+
|
51
|
+
def callback
|
52
|
+
self.response ||= k.call(
|
53
|
+
env.merge(RESPONSE_BODY => body ,
|
54
|
+
RESPONSE_STATUS => status,
|
55
|
+
RESPONSE_HEADERS => headers,
|
56
|
+
FAIL => ((env[FAIL]||[]) + [error]).compact,
|
57
|
+
LOG => (env[LOG] ||[]) +
|
58
|
+
["Future picked: #{self.class}"]))
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_load body, status, headers
|
62
|
+
env[TIMER].cancel if env[TIMER]
|
63
|
+
synchronize{
|
64
|
+
self.body, self.status, self.headers = body, status, headers
|
65
|
+
begin
|
66
|
+
# under ASYNC callback, should call immediate
|
67
|
+
next_tick{ callback } if immediate
|
68
|
+
rescue Exception => e
|
69
|
+
# nothing we can do here for an asynchronous exception,
|
70
|
+
# so we just log the error
|
71
|
+
logger = method(:warn) # TODO: add error_log_method
|
72
|
+
logger.call "RestCore: ERROR: #{e}\n" \
|
73
|
+
" from #{e.backtrace.inspect}"
|
74
|
+
end
|
75
|
+
}
|
76
|
+
resume # client or response might be waiting
|
77
|
+
end
|
78
|
+
|
79
|
+
def on_error error
|
80
|
+
self.error = if error.kind_of?(Exception)
|
81
|
+
error
|
82
|
+
else
|
83
|
+
Error.new(error || 'unknown')
|
84
|
+
end
|
85
|
+
on_load('', 0, {})
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
attr_accessor :env, :k, :immediate,
|
90
|
+
:response, :body, :status, :headers, :error
|
91
|
+
|
92
|
+
private
|
93
|
+
def synchronize; yield; end
|
94
|
+
# next_tick is used for telling the reactor that there's something else
|
95
|
+
# should be done, don't sleep and don't stop at the moment
|
96
|
+
def next_tick
|
97
|
+
if Object.const_defined?(:EventMachine) && EventMachine.reactor_running?
|
98
|
+
EventMachine.next_tick{ yield }
|
99
|
+
else
|
100
|
+
yield
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
autoload :FutureFiber , 'rest-core/engine/future/future_fiber'
|
105
|
+
autoload :FutureThread, 'rest-core/engine/future/future_thread'
|
106
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'fiber'
|
3
|
+
|
4
|
+
class RestCore::Future::FutureFiber < RestCore::Future
|
5
|
+
def initialize *args
|
6
|
+
super
|
7
|
+
self.fibers = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def wrap
|
11
|
+
Fiber.new{ yield }.resume
|
12
|
+
end
|
13
|
+
|
14
|
+
def wait
|
15
|
+
fibers << Fiber.current
|
16
|
+
Fiber.yield until loaded? # it might be resumed by some other futures!
|
17
|
+
end
|
18
|
+
|
19
|
+
def resume
|
20
|
+
return if fibers.empty?
|
21
|
+
current_fibers = fibers.dup
|
22
|
+
fibers.clear
|
23
|
+
current_fibers.each{ |f|
|
24
|
+
next unless f.alive?
|
25
|
+
next_tick{
|
26
|
+
begin
|
27
|
+
f.resume
|
28
|
+
rescue FiberError
|
29
|
+
# whenever timeout, it would be already resumed,
|
30
|
+
# and we have no way to tell if it's already resumed or not!
|
31
|
+
end
|
32
|
+
}
|
33
|
+
}
|
34
|
+
resume
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
attr_accessor :fibers
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
class RestCore::Future::FutureThread < RestCore::Future
|
5
|
+
def initialize *args
|
6
|
+
super
|
7
|
+
self.condv = ConditionVariable.new
|
8
|
+
self.mutex = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def wrap
|
12
|
+
Thread.new{ yield }
|
13
|
+
end
|
14
|
+
|
15
|
+
def wait
|
16
|
+
# it might be awaken by some other futures!
|
17
|
+
synchronize{ condv.wait(mutex) until loaded? } unless loaded?
|
18
|
+
end
|
19
|
+
|
20
|
+
def resume
|
21
|
+
condv.broadcast
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
attr_accessor :condv, :mutex
|
26
|
+
|
27
|
+
private
|
28
|
+
def synchronize; mutex.synchronize{ yield }; end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
require 'restclient'
|
3
|
+
require 'rest-core/patch/rest-client'
|
4
|
+
|
5
|
+
require 'rest-core/engine/future/future'
|
6
|
+
require 'rest-core/middleware'
|
7
|
+
|
8
|
+
class RestCore::RestClient
|
9
|
+
include RestCore::Middleware
|
10
|
+
def call env, &k
|
11
|
+
future = Future::FutureThread.new(env, k, env[ASYNC])
|
12
|
+
|
13
|
+
t = future.wrap{ # we can implement thread pool in the future
|
14
|
+
begin
|
15
|
+
res = ::RestClient::Request.execute(:method => env[REQUEST_METHOD ],
|
16
|
+
:url => request_uri(env) ,
|
17
|
+
:payload => env[REQUEST_PAYLOAD],
|
18
|
+
:headers => env[REQUEST_HEADERS],
|
19
|
+
:max_redirects => 0)
|
20
|
+
future.on_load(res.body, res.code, normalize_headers(res.raw_headers))
|
21
|
+
|
22
|
+
rescue ::RestClient::Exception => e
|
23
|
+
if res = e.response
|
24
|
+
# we don't want to raise an exception for 404 requests
|
25
|
+
future.on_load(res.body, res.code,
|
26
|
+
normalize_headers(res.raw_headers))
|
27
|
+
else
|
28
|
+
future.on_error(e)
|
29
|
+
end
|
30
|
+
rescue Exception => e
|
31
|
+
future.on_error(e)
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
env[TIMER].on_timeout{
|
36
|
+
t.kill
|
37
|
+
future.on_error(env[TIMER].error)
|
38
|
+
} if env[TIMER]
|
39
|
+
|
40
|
+
env.merge(RESPONSE_BODY => future.proxy_body,
|
41
|
+
RESPONSE_STATUS => future.proxy_status,
|
42
|
+
RESPONSE_HEADERS => future.proxy_headers,
|
43
|
+
FUTURE => future)
|
44
|
+
end
|
45
|
+
|
46
|
+
def normalize_headers raw_headers
|
47
|
+
raw_headers.inject({}){ |r, (k, v)|
|
48
|
+
r[k.to_s.upcase.tr('-', '_')] = if v.kind_of?(Array) && v.size == 1
|
49
|
+
v.first
|
50
|
+
else
|
51
|
+
v
|
52
|
+
end
|
53
|
+
r
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
data/lib/rest-core/middleware.rb
CHANGED
@@ -6,11 +6,16 @@ require 'cgi'
|
|
6
6
|
module RestCore::Middleware
|
7
7
|
include RestCore
|
8
8
|
|
9
|
+
# identity function
|
10
|
+
def self.id
|
11
|
+
@id ||= lambda{ |a| a }
|
12
|
+
end
|
13
|
+
|
9
14
|
def self.included mod
|
10
15
|
mod.send(:include, RestCore)
|
11
16
|
mod.send(:attr_reader, :app)
|
12
|
-
|
13
|
-
src =
|
17
|
+
mem = if mod.respond_to?(:members) then mod.members else [] end
|
18
|
+
src = mem.map{ |member| <<-RUBY }
|
14
19
|
def #{member} env
|
15
20
|
if env.key?('#{member}')
|
16
21
|
env['#{member}']
|
@@ -19,7 +24,7 @@ module RestCore::Middleware
|
|
19
24
|
end
|
20
25
|
end
|
21
26
|
RUBY
|
22
|
-
args = [:app] +
|
27
|
+
args = [:app] + mem
|
23
28
|
para_list = args.map{ |a| "#{a}=nil"}.join(', ')
|
24
29
|
args_list = args .join(', ')
|
25
30
|
ivar_list = args.map{ |a| "@#{a}" }.join(', ')
|
@@ -34,9 +39,10 @@ module RestCore::Middleware
|
|
34
39
|
mod.send(:include, accessor)
|
35
40
|
end
|
36
41
|
|
37
|
-
def call env
|
42
|
+
def call env, &k; app.call(env, &(k || id)) ; end
|
38
43
|
def fail env, obj; env.merge(FAIL => (env[FAIL] || []) + [obj]); end
|
39
44
|
def log env, obj; env.merge(LOG => (env[LOG] || []) + [obj]); end
|
45
|
+
def id ; Middleware.id ; end
|
40
46
|
def run app=app
|
41
47
|
if app.respond_to?(:app) && app.app
|
42
48
|
run(app.app)
|
@@ -53,9 +59,25 @@ module RestCore::Middleware
|
|
53
59
|
else
|
54
60
|
q = if env[REQUEST_PATH] =~ /\?/ then '&' else '?' end
|
55
61
|
"#{env[REQUEST_PATH]}#{q}" \
|
56
|
-
"#{query.map{ |(k, v)|
|
62
|
+
"#{query.sort.map{ |(k, v)|
|
57
63
|
"#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')}"
|
58
64
|
end
|
59
65
|
end
|
60
66
|
public :request_uri
|
67
|
+
|
68
|
+
def string_keys hash
|
69
|
+
hash.inject({}){ |r, (k, v)|
|
70
|
+
if v.kind_of?(Hash)
|
71
|
+
r[k.to_s] = case k.to_s
|
72
|
+
when REQUEST_QUERY, REQUEST_PAYLOAD, REQUEST_HEADERS
|
73
|
+
string_keys(v)
|
74
|
+
else; v
|
75
|
+
end
|
76
|
+
else
|
77
|
+
r[k.to_s] = v
|
78
|
+
end
|
79
|
+
r
|
80
|
+
}
|
81
|
+
end
|
82
|
+
public :string_keys
|
61
83
|
end
|
@@ -5,20 +5,20 @@ class RestCore::AuthBasic
|
|
5
5
|
def self.members; [:username, :password]; end
|
6
6
|
include RestCore::Middleware
|
7
7
|
|
8
|
-
def call env
|
8
|
+
def call env, &k
|
9
9
|
if username(env)
|
10
10
|
if password(env)
|
11
11
|
app.call(env.merge(REQUEST_HEADERS =>
|
12
|
-
auth_basic_header(env).merge(env[REQUEST_HEADERS] || {})))
|
12
|
+
auth_basic_header(env).merge(env[REQUEST_HEADERS] || {})), &k)
|
13
13
|
else
|
14
14
|
app.call(log(env, "AuthBasic: username provided: #{username(env)}," \
|
15
|
-
" but password is missing."))
|
15
|
+
" but password is missing."), &k)
|
16
16
|
end
|
17
17
|
elsif password(env)
|
18
18
|
app.call(log(env, "AuthBasic: password provided: #{password(env)}," \
|
19
|
-
" but username is missing."))
|
19
|
+
" but username is missing."), &k)
|
20
20
|
else
|
21
|
-
app.call(env)
|
21
|
+
app.call(env, &k)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|