rjr 0.18.2 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -0
  3. data/bin/rjr-client +16 -9
  4. data/bin/rjr-server +2 -1
  5. data/examples/client.rb +21 -19
  6. data/examples/server.rb +1 -1
  7. data/examples/structured_server.rb +1 -0
  8. data/examples/tcp.rb +1 -0
  9. data/lib/rjr/common.rb +1 -226
  10. data/lib/rjr/core_ext.rb +63 -0
  11. data/lib/rjr/dispatcher.rb +75 -219
  12. data/lib/rjr/messages.rb +8 -0
  13. data/lib/rjr/messages/compressed.rb +264 -0
  14. data/lib/rjr/messages/notification.rb +95 -0
  15. data/lib/rjr/messages/request.rb +99 -0
  16. data/lib/rjr/messages/response.rb +128 -0
  17. data/lib/rjr/node.rb +100 -97
  18. data/lib/rjr/node_callback.rb +43 -0
  19. data/lib/rjr/nodes/amqp.rb +12 -11
  20. data/lib/rjr/nodes/easy.rb +4 -4
  21. data/lib/rjr/nodes/local.rb +13 -12
  22. data/lib/rjr/nodes/multi.rb +1 -1
  23. data/lib/rjr/nodes/tcp.rb +15 -13
  24. data/lib/rjr/nodes/template.rb +4 -4
  25. data/lib/rjr/nodes/unix.rb +15 -13
  26. data/lib/rjr/nodes/web.rb +15 -14
  27. data/lib/rjr/nodes/ws.rb +12 -11
  28. data/lib/rjr/request.rb +128 -0
  29. data/lib/rjr/result.rb +75 -0
  30. data/lib/rjr/util/args.rb +145 -0
  31. data/lib/rjr/{em_adapter.rb → util/em_adapter.rb} +0 -0
  32. data/lib/rjr/util/handles_methods.rb +115 -0
  33. data/lib/rjr/util/has_messages.rb +50 -0
  34. data/lib/rjr/{inspect.rb → util/inspect.rb} +1 -1
  35. data/lib/rjr/util/json_parser.rb +101 -0
  36. data/lib/rjr/util/logger.rb +128 -0
  37. data/lib/rjr/{thread_pool.rb → util/thread_pool.rb} +2 -0
  38. data/lib/rjr/version.rb +1 -1
  39. data/site/jrw.js +1 -1
  40. data/specs/args_spec.rb +144 -0
  41. data/specs/dispatcher_spec.rb +399 -211
  42. data/specs/em_adapter_spec.rb +31 -18
  43. data/specs/handles_methods_spec.rb +154 -0
  44. data/specs/has_messages_spec.rb +54 -0
  45. data/specs/inspect_spec.rb +1 -1
  46. data/specs/json_parser_spec.rb +169 -0
  47. data/specs/messages/notification_spec.rb +59 -0
  48. data/specs/messages/request_spec.rb +66 -0
  49. data/specs/messages/response_spec.rb +94 -0
  50. data/specs/node_callbacks_spec.rb +47 -0
  51. data/specs/node_spec.rb +465 -56
  52. data/specs/request_spec.rb +147 -0
  53. data/specs/result_spec.rb +144 -0
  54. data/specs/thread_pool_spec.rb +1 -1
  55. metadata +41 -11
  56. data/lib/rjr/errors.rb +0 -23
  57. data/lib/rjr/message.rb +0 -351
  58. data/lib/rjr/semaphore.rb +0 -58
  59. data/specs/message_spec.rb +0 -229
@@ -30,7 +30,7 @@ require 'thread'
30
30
  require 'eventmachine'
31
31
 
32
32
  require 'rjr/node'
33
- require 'rjr/message'
33
+ require 'rjr/messages'
34
34
 
35
35
  module RJR
36
36
  module Nodes
@@ -57,7 +57,7 @@ class WebConnection < EventMachine::Connection
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 NotificationMessage.is_notification_message?(msg)
60
+ @rjr_node.send_msg "", self if Messages::Notification.is_notification_message?(msg)
61
61
  end
62
62
  end
63
63
 
@@ -87,13 +87,14 @@ end
87
87
  # puts client.invoke('http://localhost:7777', 'hello', 'mo')
88
88
  #
89
89
  # @example Invoking json-rpc requests over http using curl
90
- # $ curl -X POST http://localhost:7777 -d '{"jsonrpc":"2.0","method":"hello","params":["mo"],"id":"123"}'
91
- # > {"jsonrpc":"2.0","id":"123","result":"Hello mo!"}
90
+ # sh> curl -X POST http://localhost:7777 -d '{"jsonrpc":"2.0","method":"hello","params":["mo"],"id":"123"}'
91
+ # > {"jsonrpc":"2.0","id":"123","result":"Hello mo!"}
92
92
  #
93
93
  class Web < RJR::Node
94
94
 
95
95
  RJR_NODE_TYPE = :web
96
96
  PERSISTENT_NODE = false
97
+ INDIRECT_NODE = false
97
98
 
98
99
  public
99
100
 
@@ -113,7 +114,7 @@ class Web < RJR::Node
113
114
 
114
115
  # Send data using specified http connection
115
116
  #
116
- # Implementation of {RJR::Node#send_msg}
117
+ # Implementation of RJR::Node#send_msg
117
118
  def send_msg(data, connection)
118
119
  # we are assuming that since http connections
119
120
  # are not persistant, we should be sending a
@@ -131,7 +132,7 @@ class Web < RJR::Node
131
132
 
132
133
  # Instruct Node to start listening for and dispatching rpc requests
133
134
  #
134
- # Implementation of {RJR::Node#listen}
135
+ # Implementation of RJR::Node#listen
135
136
  def listen
136
137
  @@em.schedule do
137
138
  EventMachine::start_server(@host, @port, WebConnection, :rjr_node => self)
@@ -141,7 +142,7 @@ class Web < RJR::Node
141
142
 
142
143
  # Instructs node to send rpc request, and wait for / return response
143
144
  #
144
- # Implementation of {RJR::Node#invoke}
145
+ # Implementation of RJR::Node#invoke
145
146
  #
146
147
  # Do not invoke directly from em event loop or callback as will block the message
147
148
  # subscription used to receive responses
@@ -151,9 +152,9 @@ class Web < RJR::Node
151
152
  # @param [String] rpc_method json-rpc method to invoke on destination
152
153
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
153
154
  def invoke(uri, rpc_method, *args)
154
- message = RequestMessage.new :method => rpc_method,
155
- :args => args,
156
- :headers => @message_headers
155
+ message = Messages::Request.new :method => rpc_method,
156
+ :args => args,
157
+ :headers => @message_headers
157
158
  cb = lambda { |http|
158
159
  # TODO handle errors
159
160
  handle_message(http.response, http)
@@ -177,7 +178,7 @@ class Web < RJR::Node
177
178
 
178
179
  # Instructs node to send rpc notification (immadiately returns / no response is generated)
179
180
  #
180
- # Implementation of {RJR::Node#notify}
181
+ # Implementation of RJR::Node#notify
181
182
  #
182
183
  # @param [String] uri location of node to send request to, should be
183
184
  # in format of http://hostname:port
@@ -189,9 +190,9 @@ class Web < RJR::Node
189
190
  published_c = ConditionVariable.new
190
191
 
191
192
  invoked = false
192
- message = NotificationMessage.new :method => rpc_method,
193
- :args => args,
194
- :headers => @message_headers
193
+ message = Messages::Notification.new :method => rpc_method,
194
+ :args => args,
195
+ :headers => @message_headers
195
196
  cb = lambda { |arg| published_l.synchronize { invoked = true ; published_c.signal }}
196
197
  @@em.schedule do
197
198
  http = EventMachine::HttpRequest.new(uri).post :body => message.to_s,
@@ -23,7 +23,7 @@ else
23
23
  require 'thread'
24
24
 
25
25
  require 'rjr/node'
26
- require 'rjr/message'
26
+ require 'rjr/messages'
27
27
 
28
28
  module RJR
29
29
  module Nodes
@@ -55,6 +55,7 @@ module Nodes
55
55
  class WS < RJR::Node
56
56
  RJR_NODE_TYPE = :ws
57
57
  PERSISTENT_NODE = true
58
+ INDIRECT_NODE = false
58
59
 
59
60
  private
60
61
 
@@ -100,14 +101,14 @@ class WS < RJR::Node
100
101
 
101
102
  # Send data using specified websocket safely
102
103
  #
103
- # Implementation of {RJR::Node#send_msg}
104
+ # Implementation of RJR::Node#send_msg
104
105
  def send_msg(data, ws)
105
106
  @@em.schedule { ws.send(data) }
106
107
  end
107
108
 
108
109
  # Instruct Node to start listening for and dispatching rpc requests
109
110
  #
110
- # Implementation of {RJR::Node#listen}
111
+ # Implementation of RJR::Node#listen
111
112
  def listen
112
113
  @@em.schedule do
113
114
  EventMachine::WebSocket.run(:host => @host, :port => @port) do |ws|
@@ -122,7 +123,7 @@ class WS < RJR::Node
122
123
 
123
124
  # Instructs node to send rpc request, and wait for / return response
124
125
  #
125
- # Implementation of {RJR::Node#invoke}
126
+ # Implementation of RJR::Node#invoke
126
127
  #
127
128
  # Do not invoke directly from em event loop or callback as will block the message
128
129
  # subscription used to receive responses
@@ -132,9 +133,9 @@ class WS < RJR::Node
132
133
  # @param [String] rpc_method json-rpc method to invoke on destination
133
134
  # @param [Array] args array of arguments to convert to json and invoke remote method wtih
134
135
  def invoke(uri, rpc_method, *args)
135
- message = RequestMessage.new :method => rpc_method,
136
- :args => args,
137
- :headers => @message_headers
136
+ message = Messages::Request.new :method => rpc_method,
137
+ :args => args,
138
+ :headers => @message_headers
138
139
 
139
140
  @@em.schedule {
140
141
  init_client(uri) do |c|
@@ -155,7 +156,7 @@ class WS < RJR::Node
155
156
 
156
157
  # Instructs node to send rpc notification (immadiately returns / no response is generated)
157
158
  #
158
- # Implementation of {RJR::Node#notify}
159
+ # Implementation of RJR::Node#notify
159
160
  #
160
161
  # @param [String] uri location of node to send notification to, should be
161
162
  # in format of ws://hostname:port
@@ -167,9 +168,9 @@ class WS < RJR::Node
167
168
  published_c = ConditionVariable.new
168
169
 
169
170
  invoked = false
170
- message = NotificationMessage.new :method => rpc_method,
171
- :args => args,
172
- :headers => @message_headers
171
+ message = Messages::Notification.new :method => rpc_method,
172
+ :args => args,
173
+ :headers => @message_headers
173
174
  @@em.schedule {
174
175
  init_client(uri) do |c|
175
176
  c.stream { |msg| handle_message(msg.data, c) }
@@ -0,0 +1,128 @@
1
+ # RJR Request Representation
2
+ #
3
+ # Copyright (C) 2012-2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'json'
7
+ require 'rjr/result'
8
+ require 'rjr/core_ext'
9
+ require 'rjr/util/args'
10
+ require 'rjr/util/logger'
11
+
12
+ module RJR
13
+
14
+ # JSON-RPC request representation.
15
+ #
16
+ # Registered request handlers will be invoked in the context of
17
+ # instances of this class, meaning all member variables will be available
18
+ # for use in the handler.
19
+ class Request
20
+ # Result of the request operation, set by dispatcher
21
+ attr_accessor :result
22
+
23
+ # Method which request is for
24
+ attr_accessor :rjr_method
25
+
26
+ # Arguments be passed to method
27
+ attr_accessor :rjr_method_args
28
+
29
+ # Argument object encapsulating arguments
30
+ attr_accessor :rjr_args
31
+
32
+ # Headers which came w/ request
33
+ attr_accessor :rjr_headers
34
+
35
+ # Client IP which request came in on (only for direct nodes)
36
+ attr_accessor :rjr_client_ip
37
+
38
+ # Port which request came in on (only for direct nodes)
39
+ attr_accessor :rjr_client_port
40
+
41
+ # RJR callback which may be used to push data to client
42
+ attr_accessor :rjr_callback
43
+
44
+ # Node which the request came in on
45
+ attr_accessor :rjr_node
46
+
47
+ # Type of node which request came in on
48
+ attr_accessor :rjr_node_type
49
+
50
+ # ID of node which request came in on
51
+ attr_accessor :rjr_node_id
52
+
53
+ # Actual proc registered to handle request
54
+ attr_accessor :rjr_handler
55
+
56
+ # RJR Request initializer
57
+ #
58
+ # @param [Hash] args options to set on request,
59
+ # see Request accessors for valid keys
60
+ def initialize(args = {})
61
+ @rjr_method = args[:rjr_method] || args['rjr_method']
62
+ @rjr_method_args = args[:rjr_method_args] || args['rjr_method_args'] || []
63
+ @rjr_headers = args[:rjr_headers] || args['rjr_headers']
64
+
65
+ @rjr_client_ip = args[:rjr_client_ip]
66
+ @rjr_client_port = args[:rjr_client_port]
67
+
68
+ @rjr_callback = args[:rjr_callback]
69
+ @rjr_node = args[:rjr_node]
70
+ @rjr_node_id = args[:rjr_node_id] || args['rjr_node_id']
71
+ @rjr_node_type = args[:rjr_node_type] || args['rjr_node_type']
72
+
73
+ @rjr_handler = args[:rjr_handler]
74
+
75
+ @rjr_args = Arguments.new :args => @rjr_method_args
76
+ @result = nil
77
+ end
78
+
79
+ # Invoke the request by calling the registered handler with the registered
80
+ # method parameters in the local scope
81
+ def handle
82
+ node_sig = "#{@rjr_node_id}(#{@rjr_node_type})"
83
+ method_sig = "#{@rjr_method}(#{@rjr_method_args.join(',')})"
84
+
85
+ RJR::Logger.info "#{node_sig}->#{method_sig}"
86
+
87
+ # TODO option to compare arity of handler to number
88
+ # of method_args passed in ?
89
+ retval = instance_exec(*@rjr_method_args, &@rjr_handler)
90
+
91
+ RJR::Logger.info \
92
+ "#{node_sig}<-#{method_sig}<-#{retval.nil? ? "nil" : retval}"
93
+
94
+ return retval
95
+ end
96
+
97
+ def request_json
98
+ {:request => { :rjr_method => @rjr_method,
99
+ :rjr_method_args => @rjr_method_args,
100
+ :rjr_headers => @rjr_headers,
101
+ :rjr_node_type => @rjr_node_type,
102
+ :rjr_node_id => @rjr_node_id }}
103
+ end
104
+
105
+ def result_json
106
+ return {} unless !!@result
107
+ {:result => { :result => @result.result,
108
+ :error_code => @result.error_code,
109
+ :error_msg => @result.error_msg,
110
+ :error_class => @result.error_class }}
111
+ end
112
+
113
+ # Convert request to json representation and return it
114
+ def to_json(*a)
115
+ {'json_class' => self.class.name,
116
+ 'data' => request_json.merge(result_json)}.to_json(*a)
117
+ end
118
+
119
+ # Create new request from json representation
120
+ def self.json_create(o)
121
+ result = Result.new(o['data']['result'])
122
+ request = Request.new(o['data']['request'])
123
+ request.result = result
124
+ return request
125
+ end
126
+
127
+ end # class Request
128
+ end # module RJR
@@ -0,0 +1,75 @@
1
+ # RJR Result Representation
2
+ #
3
+ # Copyright (C) 2012-2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ module RJR
7
+
8
+ # JSON-RPC Result Representation
9
+ class Result
10
+ # Boolean indicating if request was successfully invoked
11
+ attr_accessor :success
12
+
13
+ # Boolean indicating if request failed in some manner
14
+ attr_accessor :failed
15
+
16
+ # Return value of the json-rpc call if successful
17
+ attr_accessor :result
18
+
19
+ # Code corresponding to json-rpc error if problem occured during request invocation
20
+ attr_accessor :error_code
21
+
22
+ # Message corresponding to json-rpc error if problem occured during request invocation
23
+ attr_accessor :error_msg
24
+
25
+ # Class of error raised (if any) during request invocation (this is extra metadata beyond standard json-rpc)
26
+ attr_accessor :error_class
27
+
28
+ # RJR result intializer
29
+ # @param [Hash] args options to set on result
30
+ # @option args [Object] :result result of json-rpc method handler if successfully returned
31
+ # @option args [Integer] :error_code code corresponding to json-rpc error if problem occured during request invocation
32
+ # @option args [String] :error_msg message corresponding to json-rpc error if problem occured during request invocation
33
+ # @option args [Class] :error_class class of error raised (if any) during request invocation (this is extra metadata beyond standard json-rpc)
34
+ def initialize(args = {})
35
+ @result = args[:result] || args['result']
36
+ @error_code = args[:error_code] || args['error_code']
37
+ @error_msg = args[:error_msg] || args['error_msg']
38
+ @error_class = args[:error_class] || args['error_class']
39
+
40
+ @success = @error_code.nil?
41
+ @failed = !@error_code.nil?
42
+ end
43
+
44
+ # Compare Result against other result, returning true if both correspond
45
+ # to equivalent json-rpc results else false
46
+ def ==(other)
47
+ @success == other.success &&
48
+ @failed == other.failed &&
49
+ @result == other.result &&
50
+ @error_code == other.error_code &&
51
+ @error_msg == other.error_msg &&
52
+ @error_class == other.error_class
53
+ end
54
+
55
+ # Convert Response to human consumable string
56
+ def to_s
57
+ "#{@success} #{@result} #{@error_code} #{@error_msg} #{@error_class}"
58
+ end
59
+
60
+ ######### Specific request types
61
+
62
+ # JSON-RPC -32600 / Invalid Request
63
+ def self.invalid_request
64
+ return Result.new(:error_code => -32600,
65
+ :error_msg => 'Invalid Request')
66
+ end
67
+
68
+ # JSON-RPC -32602 / Method not found
69
+ def self.method_not_found(name)
70
+ return Result.new(:error_code => -32602,
71
+ :error_msg => "Method '#{name}' not found")
72
+ end
73
+
74
+ end # class Result
75
+ end # module RJR
@@ -0,0 +1,145 @@
1
+ # RJR JSON-RPC Argument Representation
2
+ #
3
+ # Copyright (C) 2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'forwardable'
7
+
8
+ module RJR
9
+
10
+ # Encapsulates a list of JSON-RPC method arguments as sent
11
+ # from the client to the server, in addition to providing
12
+ # various helper / utility methods.
13
+ class Arguments
14
+ include Enumerable
15
+ extend Forwardable
16
+
17
+ attr_accessor :args
18
+
19
+ def_delegators :@args, :each, :<<, :length, :[], :index
20
+
21
+ def initialize(args={})
22
+ @args = args[:args] || []
23
+ end
24
+
25
+ # Validate arguments against acceptable values.
26
+ #
27
+ # Raises error if value is found which is not on
28
+ # list of acceptable values.
29
+ #
30
+ # If acceptable values are hash's, keys are compared
31
+ # and on matches the values are used as the # of following
32
+ # arguments to skip in the validator
33
+ #
34
+ # *Note* args / acceptable params are converted to strings
35
+ # before comparison
36
+ #
37
+ # @example
38
+ # args = Arguments.new :args => ['custom', 'another']
39
+ # args.validate! 'custom', 'another' #=> nil
40
+ # args.validate! 'something' #=> ArgumentError
41
+ #
42
+ # args = Arguments.new :args => ['with_id', 123, 'another']
43
+ # args.validate! :with_id => 1, :another => 0 #=> nil
44
+ # args.validate! :with_id => 1 #=> ArgumentError
45
+ def validate!(*acceptable)
46
+ i = 0
47
+ if acceptable.first.is_a?(Hash)
48
+ # clone acceptable hash, swap keys for string
49
+ acceptable = Hash[acceptable.first]
50
+ acceptable.keys.each { |k|
51
+ acceptable[k.to_s] = acceptable[k]
52
+ acceptable.delete(k) unless k.is_a?(String)
53
+ }
54
+
55
+ # compare acceptable against arguments, raising error if issue found
56
+ while(i < length) do
57
+ val = self[i]
58
+ passed = acceptable.has_key?(val)
59
+ raise ArgumentError, "#{val} not an acceptable arg" unless passed
60
+ skip = acceptable[val]
61
+ i += (skip + 1)
62
+ end
63
+
64
+ else
65
+ # clone acceptable array, swap values for string
66
+ acceptable = Array.new(acceptable).map { |a| a.to_s }
67
+
68
+ # compare acceptable against arguments, raising error if issue found
69
+ while(i < length) do
70
+ val = self[i].to_s
71
+ passed = acceptable.include?(val)
72
+ raise ArgumentError, "#{val} not an acceptable arg" unless passed
73
+ i += 1
74
+ end
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ # Extract groups of values from argument list
81
+ #
82
+ # Groups are generated by comparing arguments to keys in the
83
+ # specified map and on matches extracting the # of following
84
+ # arguments specified by the map values
85
+ #
86
+ # Note arguments / keys are converted to strings before comparison
87
+ #
88
+ # @example
89
+ # args = Arguments.new :args => ['with_id', 123, 'custom', 'another']
90
+ # args.extract :with_id => 1, :custom => 0
91
+ # # => [['with_id', 123], ['custom']]
92
+ #
93
+ def extract(map)
94
+ # clone map hash, swap keys for string
95
+ map = Hash[map]
96
+ map.keys.each { |k|
97
+ map[k.to_s] = map[k]
98
+ map.delete(k) unless k.is_a?(String)
99
+ }
100
+
101
+ groups = []
102
+ i = 0
103
+ while(i < length) do
104
+ val = self[i]
105
+ i += 1
106
+ next unless !!map.has_key?(val)
107
+
108
+ num = map[val]
109
+ group = [val]
110
+ 0.upto(num-1) do |j|
111
+ group << self[i]
112
+ i += 1
113
+ end
114
+ groups << group
115
+ end
116
+
117
+ groups
118
+ end
119
+
120
+ # Return boolean if tag appears in argument list.
121
+ # Simple wrapper around includes setting up the scope
122
+ # of argument 'specifiers'
123
+ #
124
+ # @example
125
+ # args = Arguments.new :args => ['foobar']
126
+ # args.specifies?('foobar') #=> true
127
+ # args.specifies?('barfoo') #=> false
128
+ def specifies?(tag)
129
+ include?(tag)
130
+ end
131
+
132
+ # Return specifier corresponding given tag. The specifier
133
+ # is defined as the value appearing in the argument list
134
+ # immediately after the tag
135
+ #
136
+ # @example
137
+ # args = Argument.new :args => ['with_id', 123]
138
+ # args.specifier_for('with_id') #=> 123
139
+ # args.specifier_for('other') #=> nil
140
+ def specifier_for(tag)
141
+ return nil unless specifies?(tag)
142
+ self[index(tag) + 1]
143
+ end
144
+ end
145
+ end