rjr 0.19.1 → 0.19.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 517f79e571aae798dcc24be8feacd2ef5cbc3606
4
- data.tar.gz: 340a496e1f47092a7b01a4b1363bac1db87b9e81
3
+ metadata.gz: 67d23eb4f09f94c96a7d601c338b4d5088605c24
4
+ data.tar.gz: 53bbd5973ce99fa089c5d4bf560e2e9fe6c9005c
5
5
  SHA512:
6
- metadata.gz: 6b5e056c47862523e346cf2a47923146e0b15a3fb856071f20a4fa226d55fdb6046e5ec463c8aea4e356d96d49e707279d5b477549df2537041943aaa04513f7
7
- data.tar.gz: 325453d870c227bd35826a3bab5933fc6a62e644f1efbb2d5de3f762e917b24e38908aa15cfad2d7d3aae2d6d2f8af8da57a7bf527f9558570a78412747c5b60
6
+ metadata.gz: 9b8a199e88e55fd824cdabaeebf750cd234624ade0b9fc2acd9640aec729d5a3ab4339f5ef0769f707916a830f987d7a95c680e7e0ea07b4f526bd97b1394d3c
7
+ data.tar.gz: 55e17f0513fedef6f3e5dccae5673317bf5943988ae5d2146c7c2d76fa7aaee0ce003495c774ad3b623421efdff5127338d88cbf78792f3ecde9434dd9629f31
data/README.md CHANGED
@@ -166,3 +166,6 @@ RJR as this is not standard JSON-RPC.
166
166
  ### Authors ###
167
167
  * Mo Morsi <mo@morsi.org>
168
168
  * Ladislav Smola <ladislav.smola@it-logica.cz>
169
+ * André Dieb Martins <andre.dieb@gmail.com>
170
+ * Eric Bakan <eric@ebakan.com>
171
+ * Jonas Collberg <jonas.collberg@gmail.com>
data/Rakefile CHANGED
@@ -3,8 +3,6 @@
3
3
  # Copyright (C) 2010-2012 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
- # TODO fix travis build
7
-
8
6
  require "rspec/core/rake_task"
9
7
 
10
8
  desc "Run all specs"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/ruby
2
+ # A RJR timeout example
3
+ #
4
+ # Copyright (C) 2015 Mohammed Morsi <mo@morsi.org>
5
+ # Licensed under the Apache License, Version 2.0
6
+
7
+ require 'rjr/nodes/tcp'
8
+
9
+ server = RJR::Nodes::TCP.new :host => 'localhost', :port => 9789, :node_id => "server"
10
+ server.dispatcher.handle('method') { |i|
11
+ sleep 2
12
+ }
13
+ server.listen
14
+
15
+ client = RJR::Nodes::TCP.new :node_id => "client",
16
+ :host => 'localhost',
17
+ :port => 9666,
18
+ :timeout => 1 # causes error, change to 3 to wait for server response
19
+
20
+ client.invoke "jsonrpc://localhost:9789", "method", "Hello World"
21
+ # => exception
22
+
23
+ #client.join
@@ -152,7 +152,7 @@ class Dispatcher
152
152
  request = Request.new args.merge(:rjr_handler => handler)
153
153
 
154
154
  # set request environment
155
- request.extend(environment) unless environment.nil?
155
+ request.set_env(environment) unless environment.nil?
156
156
 
157
157
  begin
158
158
  retval = request.handle
@@ -0,0 +1,50 @@
1
+ # RJR Intermediate Message
2
+ #
3
+ # Copyright (C) 2014-2015 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'json'
7
+ require 'rjr/util/json_parser'
8
+
9
+ module RJR
10
+ module Messages
11
+
12
+ # Intermediate representation of a JSON-RPC data containing
13
+ # extracted/parsed data which has not been analysed.
14
+ class Intermediate
15
+ # JSON from which data is extracted from
16
+ attr_accessor :json
17
+
18
+ # Data extracted from message
19
+ attr_accessor :data
20
+
21
+ def initialize(args = {})
22
+ @json = args[:json] || nil
23
+ @data = args[:data] || {}
24
+ end
25
+
26
+ def keys
27
+ data.keys
28
+ end
29
+
30
+ def [](key)
31
+ data[key.to_s]
32
+ end
33
+
34
+ def has?(key)
35
+ data.key?(key)
36
+ end
37
+
38
+ def self.parse(json)
39
+ parsed = nil
40
+
41
+ #allow parsing errs to propagate up
42
+ parsed = JSONParser.parse(json)
43
+
44
+ self.new :json => json,
45
+ :data => parsed
46
+ end
47
+
48
+ end # class Intermediate
49
+ end # module Messages
50
+ end # module RJR
@@ -13,7 +13,7 @@ module Messages
13
13
  # indicate the result should _not_ be returned
14
14
  class Notification
15
15
  # Message string received from the source
16
- attr_accessor :json_message
16
+ attr_accessor :message
17
17
 
18
18
  # Method source is invoking on the destination
19
19
  attr_accessor :jr_method
@@ -48,18 +48,17 @@ class Notification
48
48
  end
49
49
 
50
50
  def parse_message(message)
51
- @json_message = message
52
- notification = JSONParser.parse(@json_message)
53
- @jr_method = notification['method']
54
- @jr_args = notification['params']
51
+ @message = message
52
+ @jr_method = message['method']
53
+ @jr_args = message['params']
55
54
 
56
- parse_headers(notification)
55
+ parse_headers(message)
57
56
  end
58
57
 
59
- def parse_headers(notification)
60
- notification.keys.select { |k|
58
+ def parse_headers(message)
59
+ message.keys.select { |k|
61
60
  !['jsonrpc', 'method', 'params'].include?(k)
62
- }.each { |k| @headers[k] = notification[k] }
61
+ }.each { |k| @headers[k] = message[k] }
63
62
  end
64
63
 
65
64
  public
@@ -70,13 +69,7 @@ class Notification
70
69
  # @param [String] message string message to check
71
70
  # @return [true,false] indicating if message is a notification message
72
71
  def self.is_notification_message?(message)
73
- begin
74
- # FIXME log error
75
- parsed = JSONParser.parse(message)
76
- parsed.has_key?('method') && !parsed.has_key?('id')
77
- rescue Exception => e
78
- false
79
- end
72
+ message.has?('method') && !message.has?('id')
80
73
  end
81
74
 
82
75
  # Convert notification message to json
@@ -12,7 +12,7 @@ module Messages
12
12
  # Message sent from client to server to invoke a JSON-RPC method
13
13
  class Request
14
14
  # Message string received from the source
15
- attr_accessor :json_message
15
+ attr_accessor :message
16
16
 
17
17
  # Method source is invoking on the destination
18
18
  attr_accessor :jr_method
@@ -51,35 +51,29 @@ class Request
51
51
  end
52
52
 
53
53
  def parse_message(message)
54
- @json_message = message
55
- request = JSONParser.parse(@json_message)
56
- @jr_method = request['method']
57
- @jr_args = request['params']
58
- @msg_id = request['id']
54
+ @message = message
55
+ @jr_method = message['method']
56
+ @jr_args = message['params']
57
+ @msg_id = message['id']
59
58
 
60
- parse_headers(request)
59
+ parse_headers(message)
61
60
  end
62
61
 
63
- def parse_headers(request)
64
- request.keys.select { |k|
62
+ def parse_headers(message)
63
+ message.keys.select { |k|
65
64
  !['jsonrpc', 'id', 'method', 'params'].include?(k)
66
- }.each { |k| @headers[k] = request[k] }
65
+ }.each { |k| @headers[k] = message[k] }
67
66
  end
68
67
 
69
68
  public
70
69
 
71
- # Class helper to determine if the specified string is a valid json-rpc
72
- # method request
73
- # @param [String] message string message to check
70
+ # Class helper to determine if the specified message is a valid
71
+ # json-rpc method request message.
72
+ #
73
+ # @param [Message] message to check
74
74
  # @return [true,false] indicating if message is request message
75
75
  def self.is_request_message?(message)
76
- begin
77
- # FIXME log error
78
- parsed = JSONParser.parse(message)
79
- parsed.has_key?('method') && parsed.has_key?('id')
80
- rescue Exception => e
81
- false
82
- end
76
+ message.has?('method') && message.has?('id')
83
77
  end
84
78
 
85
79
  # Convert request message to json
@@ -13,7 +13,7 @@ module Messages
13
13
  # Message sent from server to client in response to a JSON-RPC request
14
14
  class Response
15
15
  # Message string received from the source
16
- attr_accessor :json_message
16
+ attr_accessor :message
17
17
 
18
18
  # ID of the message in accordance w/ json-rpc specification
19
19
  attr_accessor :msg_id
@@ -49,54 +49,47 @@ class Response
49
49
  end
50
50
 
51
51
  def parse_message(message)
52
- @json_message = message
53
- response = JSONParser.parse(@json_message)
54
- @msg_id = response['id']
52
+ @message = message
53
+ @msg_id = message['id']
55
54
 
56
- parse_result(response)
57
- parse_headers(response)
55
+ parse_result(message)
56
+ parse_headers(message)
58
57
  end
59
58
 
60
- def parse_result(response)
59
+ def parse_result(message)
61
60
  @result = Result.new
62
- @result.success = response.has_key?('result')
61
+ @result.success = message.has?('result')
63
62
  @result.failed = !@result.success
64
63
 
65
64
  if @result.success
66
- @result.result = response['result']
65
+ @result.result = message['result']
67
66
 
68
- elsif response.has_key?('error')
69
- @result.error_code = response['error']['code']
70
- @result.error_msg = response['error']['message']
67
+ elsif message.has?('error')
68
+ @result.error_code = message['error']['code']
69
+ @result.error_msg = message['error']['message']
71
70
 
72
71
  # TODO can we safely constantize this ?
73
- @result.error_class = response['error']['class']
72
+ @result.error_class = message['error']['class']
74
73
  end
75
74
 
76
75
  @result
77
76
  end
78
77
 
79
- def parse_headers(request)
80
- request.keys.select { |k|
78
+ def parse_headers(message)
79
+ message.keys.select { |k|
81
80
  !['jsonrpc', 'id', 'method', 'result', 'error'].include?(k)
82
- }.each { |k| @headers[k] = request[k] }
81
+ }.each { |k| @headers[k] = message[k] }
83
82
  end
84
83
 
85
84
  public
86
85
 
87
- # Class helper to determine if the specified string is a valid json-rpc
88
- # method response
86
+ # Class helper to determine if the specified string is a
87
+ # valid json-rpc method response
88
+ #
89
89
  # @param [String] message string message to check
90
90
  # @return [true,false] indicating if message is response message
91
91
  def self.is_response_message?(message)
92
- begin
93
- # FIXME log error
94
- json = JSONParser.parse(message)
95
- json.has_key?('result') || json.has_key?('error')
96
- rescue Exception => e
97
- puts e.to_s
98
- false
99
- end
92
+ message.has?('result') || message.has?('error')
100
93
  end
101
94
 
102
95
  def success_json
data/lib/rjr/messages.rb CHANGED
@@ -6,3 +6,4 @@
6
6
  require 'rjr/messages/request'
7
7
  require 'rjr/messages/response'
8
8
  require 'rjr/messages/notification'
9
+ require 'rjr/messages/intermediate'
data/lib/rjr/node.rb CHANGED
@@ -104,9 +104,12 @@ class Node
104
104
  clear_event_handlers
105
105
  @response_lock = Mutex.new
106
106
  @response_cv = ConditionVariable.new
107
+ @pending = {}
107
108
  @responses = []
108
109
 
109
110
  @node_id = args[:node_id]
111
+ @timeout = args[:timeout]
112
+ @wait_interval = args[:wait_interval] || 0.01
110
113
  @dispatcher = args[:dispatcher] || RJR::Dispatcher.new
111
114
  @message_headers = args.has_key?(:headers) ? {}.merge(args[:headers]) : {}
112
115
 
@@ -143,14 +146,20 @@ class Node
143
146
  ##################################################################
144
147
  # Reset connection event handlers
145
148
  def clear_event_handlers
146
- @connection_event_handlers = {:closed => [], :error => []}
149
+ @connection_event_handlers = {
150
+ :opened => [],
151
+ :closed => [],
152
+ :error => []
153
+ }
147
154
  end
148
155
 
149
156
  # Register connection event handler
150
- # @param [:error, :close] event the event to register the handler for
151
- # @param [Callable] handler block param to be added to array of handlers
152
- # that are called when event occurs
153
- # @yield [Node] self is passed to each registered handler when event occurs
157
+ # @param event [:opened, :closed, :error] the event to register the handler
158
+ # for
159
+ # @param handler [Callable] block param to be added to array of handlers
160
+ # that are called when event occurs
161
+ # @yield [Node, *args] self and event-specific *args are passed to each
162
+ # registered handler when event occurs
154
163
  def on(event, &handler)
155
164
  return unless @connection_event_handlers.keys.include?(event)
156
165
  @connection_event_handlers[event] << handler
@@ -158,12 +167,11 @@ class Node
158
167
 
159
168
  private
160
169
 
161
- # Internal helper, run connection event handlers for specified event
162
- def connection_event(event)
170
+ # Internal helper, run connection event handlers for specified event, passing
171
+ # self and args to handler
172
+ def connection_event(event, *args)
163
173
  return unless @connection_event_handlers.keys.include?(event)
164
- @connection_event_handlers[event].each { |h|
165
- h.call self
166
- }
174
+ @connection_event_handlers[event].each { |h| h.call(self, *args) }
167
175
  end
168
176
 
169
177
  ##################################################################
@@ -183,28 +191,36 @@ class Node
183
191
 
184
192
  # Internal helper, handle message received
185
193
  def handle_message(msg, connection = {})
186
- if Messages::Request.is_request_message?(msg)
187
- tp << ThreadPoolJob.new(msg) { |m| handle_request(m, false, connection) }
194
+ intermediate = Messages::Intermediate.parse(msg)
195
+
196
+ if Messages::Request.is_request_message?(intermediate)
197
+ tp << ThreadPoolJob.new(intermediate) { |i|
198
+ handle_request(i, false, connection)
199
+ }
188
200
 
189
- elsif Messages::Notification.is_notification_message?(msg)
190
- tp << ThreadPoolJob.new(msg) { |m| handle_request(m, true, connection) }
201
+ elsif Messages::Notification.is_notification_message?(intermediate)
202
+ tp << ThreadPoolJob.new(intermediate) { |i|
203
+ handle_request(i, true, connection)
204
+ }
191
205
 
192
- elsif Messages::Response.is_response_message?(msg)
193
- handle_response(msg)
206
+ elsif Messages::Response.is_response_message?(intermediate)
207
+ handle_response(intermediate)
194
208
 
195
209
  end
210
+
211
+ intermediate
196
212
  end
197
213
 
198
214
  # Internal helper, handle request message received
199
- def handle_request(data, notification=false, connection={})
215
+ def handle_request(message, notification=false, connection={})
200
216
  # get client for the specified connection
201
217
  # TODO should grap port/ip immediately on connection and use that
202
218
  client_port,client_ip = client_for(connection)
203
219
 
204
220
  msg = notification ?
205
- Messages::Notification.new(:message => data,
221
+ Messages::Notification.new(:message => message,
206
222
  :headers => @message_headers) :
207
- Messages::Request.new(:message => data,
223
+ Messages::Request.new(:message => message,
208
224
  :headers => @message_headers)
209
225
 
210
226
  callback = NodeCallback.new(:node => self,
@@ -233,8 +249,8 @@ class Node
233
249
  end
234
250
 
235
251
  # Internal helper, handle response message received
236
- def handle_response(data)
237
- msg = Messages::Response.new(:message => data,
252
+ def handle_response(message)
253
+ msg = Messages::Response.new(:message => message,
238
254
  :headers => self.message_headers)
239
255
  res = err = nil
240
256
  begin
@@ -254,25 +270,25 @@ class Node
254
270
  # Internal helper, block until response matching message id is received
255
271
  def wait_for_result(message)
256
272
  res = nil
273
+ message_id = message.msg_id
274
+ @pending[message_id] = Time.now
257
275
  while res.nil?
258
276
  @response_lock.synchronize{
259
- # FIXME throw err if more than 1 match found
277
+ # Prune messages that timed out
278
+ if @timeout
279
+ now = Time.now
280
+ @pending.delete_if { |_, start_time| (now - start_time) > @timeout }
281
+ end
282
+ pending_ids = @pending.keys
283
+ raise Exception, 'Timed out' unless pending_ids.include? message_id
284
+
285
+ # Prune invalid responses
286
+ @responses.keep_if { |response| @pending.has_key? response.first }
260
287
  res = @responses.find { |response| message.msg_id == response.first }
261
288
  if !res.nil?
262
289
  @responses.delete(res)
263
-
264
290
  else
265
- # FIXME if halt is invoked while this is sleeping, all other threads
266
- # may be deleted resulting in this sleeping indefinetly and a deadlock
267
-
268
- # TODO wait for a finite # of seconds, record time we started waiting
269
- # before while loop and on every iteration check to see if we've been
270
- # waiting longer than an optional timeout. If so throw an error (also
271
- # need mechanism to discard result if it comes in later).
272
- # finite # of seconds we wait and optional timeout should be
273
- # configurable on node class
274
- @response_cv.wait @response_lock
275
-
291
+ @response_cv.wait @response_lock, @wait_interval
276
292
  end
277
293
  }
278
294
  end
@@ -56,6 +56,17 @@ class AMQP < RJR::Node
56
56
 
57
57
  private
58
58
 
59
+ def amqp_options
60
+ opts = {}
61
+ opts[:host] = @host if @host
62
+ opts[:port] = @port if @port
63
+ opts[:vhost] = @vhost if @vhost
64
+ opts[:user] = @user if @user
65
+ opts[:pass] = @pass if @pass
66
+ opts[:ssl] = @ssl if @ssl
67
+ opts
68
+ end
69
+
59
70
  # Internal helper, initialize the amqp subsystem
60
71
  def init_node(&on_init)
61
72
  if !@conn.nil? && @conn.connected?
@@ -63,7 +74,7 @@ class AMQP < RJR::Node
63
74
  return
64
75
  end
65
76
 
66
- @conn = ::AMQP.connect(:host => @broker) do |conn|
77
+ @conn = ::AMQP.connect(amqp_options) do |conn|
67
78
  ::AMQP.connection = conn # XXX not sure why this is needed but the amqp
68
79
  # em interface won't shut down cleanly otherwise
69
80
 
@@ -117,12 +128,17 @@ class AMQP < RJR::Node
117
128
  # @option args [String] :broker the amqp message broker which to connect to
118
129
  def initialize(args = {})
119
130
  super(args)
120
- @broker = args[:broker]
131
+ @host = args[:host] || args[:broker]
132
+ @port = args[:port]
133
+ @vhost = args[:vhost]
134
+ @user = args[:user] || args[:username]
135
+ @pass = args[:pass] || args[:password]
136
+ @ssl = args[:ssl]
121
137
  @amqp_lock = Mutex.new
122
138
  end
123
139
 
124
140
  def to_s
125
- "RJR::Nodes::AMQP<#{@node_id},#{@broker},#{@queue_name}>"
141
+ "RJR::Nodes::AMQP<#{@node_id},#{@host},#{@port},#{@vhost},#{@queue_name}>"
126
142
  end
127
143
 
128
144
  # Publish a message using the amqp exchange
@@ -61,8 +61,9 @@ class Local < RJR::Node
61
61
  # Implementation of RJR::Node#send_msg
62
62
  def send_msg(msg, connection)
63
63
  # ignore response message
64
- unless Messages::Response.is_response_message?(msg)
65
- launch_request(msg, true) # .join?
64
+ inter = Messages::Intermediate.parse(msg)
65
+ unless Messages::Response.is_response_message?(inter)
66
+ launch_request(inter, true) # .join?
66
67
  end
67
68
  end
68
69
 
@@ -80,9 +81,15 @@ class Local < RJR::Node
80
81
  # (or close to it, globals will still be available, but locks will
81
82
  # not be locally held, etc)
82
83
  def launch_request(req, notification)
83
- Thread.new(req,notification) { |req,notification|
84
- res = handle_request(req, notification, nil)
85
- handle_response(res.to_s) unless res.nil?
84
+ Thread.new(req, notification) { |req, notification|
85
+ inter = req.is_a?(Messages::Intermediate) ? req :
86
+ Messages::Intermediate.parse(req)
87
+ res = handle_request(inter, notification, nil)
88
+
89
+ unless res.nil?
90
+ inter = Messages::Intermediate.parse(res.to_s)
91
+ handle_response(inter)
92
+ end
86
93
  }
87
94
  end
88
95
 
data/lib/rjr/nodes/tcp.rb CHANGED
@@ -33,6 +33,11 @@ class TCPConnection < EventMachine::Connection
33
33
 
34
34
  @send_lock = Mutex.new
35
35
  @data = ""
36
+ @rjr_node.add_connection(self)
37
+ end
38
+
39
+ def post_init
40
+ @rjr_node.send(:connection_event, :opened, self)
36
41
  end
37
42
 
38
43
  # EventMachine::Connection#receive_data callback, handle request / response messages
@@ -54,6 +59,10 @@ class TCPConnection < EventMachine::Connection
54
59
  }
55
60
  end
56
61
 
62
+ def unbind
63
+ @rjr_node.remove_connection(self)
64
+ @rjr_node.send(:connection_event, :closed, self)
65
+ end
57
66
  end
58
67
 
59
68
  # TCP node definition, listen for and invoke json-rpc requests via TCP sockets
@@ -89,18 +98,8 @@ class TCP < RJR::Node
89
98
  # Internal helper, initialize new client
90
99
  def init_client(args={}, &on_init)
91
100
  host,port = args[:host], args[:port]
92
- connection = nil
93
- @connections_lock.synchronize {
94
- connection = @connections.find { |c|
95
- port == c.port && host == c.host
96
- }
97
- if connection.nil?
98
- connection =
99
- EventMachine::connect host, port,
100
- TCPConnection, args
101
- @connections << connection
102
- end
103
- }
101
+ connection = @connections.find { |c| port == c.port && host == c.host }
102
+ connection ||= EventMachine::connect(host, port, TCPConnection, args)
104
103
  on_init.call(connection) # TODO move to tcpnode event ?
105
104
  end
106
105
 
@@ -140,6 +139,20 @@ class TCP < RJR::Node
140
139
  self
141
140
  end
142
141
 
142
+ # Called by TCPConnection::initialize
143
+ def add_connection(connection)
144
+ @connections_lock.synchronize do
145
+ connections << connection
146
+ end
147
+ end
148
+
149
+ # Called by TCPConnection::unbind
150
+ def remove_connection(connection)
151
+ @connections_lock.synchronize do
152
+ connections.delete(connection)
153
+ end
154
+ end
155
+
143
156
  # Instructs node to send rpc request, and wait for / return response.
144
157
  #
145
158
  # Implementation of RJR::Node#invoke
data/lib/rjr/nodes/web.rb CHANGED
@@ -52,12 +52,13 @@ class WebConnection < EventMachine::Connection
52
52
  def process_http_request
53
53
  # TODO support http protocols other than POST
54
54
  msg = @http_post_content.nil? ? '' : @http_post_content
55
- @rjr_node.send(:handle_message, msg, self) # XXX private method
55
+ inter = @rjr_node.send(:handle_message, msg, self) # XXX private method
56
56
 
57
57
  # XXX we still have to send a response back to client to satisfy
58
58
  # the http standard, even if this is a notification. handle_message
59
59
  # does not do this.
60
- @rjr_node.send_msg "", self if Messages::Notification.is_notification_message?(msg)
60
+ notification = Messages::Notification.is_notification_message?(inter)
61
+ @rjr_node.send_msg "", self if notification
61
62
  end
62
63
  end
63
64
 
data/lib/rjr/request.rb CHANGED
@@ -50,6 +50,9 @@ class Request
50
50
  # ID of node which request came in on
51
51
  attr_accessor :rjr_node_id
52
52
 
53
+ # Environment handler will be run in
54
+ attr_accessor :rjr_env
55
+
53
56
  # Actual proc registered to handle request
54
57
  attr_accessor :rjr_handler
55
58
 
@@ -73,7 +76,14 @@ class Request
73
76
  @rjr_handler = args[:rjr_handler]
74
77
 
75
78
  @rjr_args = Arguments.new :args => @rjr_method_args
76
- @result = nil
79
+ @rjr_env = nil
80
+ @result = nil
81
+ end
82
+
83
+ # Set the environment by extending Request instance with the specified module
84
+ def set_env(env)
85
+ @rjr_env = env
86
+ self.extend(env)
77
87
  end
78
88
 
79
89
  # Invoke the request by calling the registered handler with the registered