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
@@ -0,0 +1,115 @@
1
+ # RJR HandlesMethods Mixin
2
+ #
3
+ # Copyright (C) 2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ module RJR
7
+
8
+ # Mixin adding methods allowing developer to specify JSON-RPC
9
+ # methods which to dispatch to.
10
+ #
11
+ # @example Defining a structured JSON-RPC method handler
12
+ # class MyMethodHandler
13
+ # include RJR::HandlesMethods
14
+ #
15
+ # jr_method :do_something
16
+ #
17
+ # def handle(*params)
18
+ # 'return value'
19
+ # end
20
+ # end
21
+ #
22
+ # node = RJR::Nodes::TCP.new :host => '0.0.0.0', :port => 8888
23
+ # MyMethodHandler.dispatch_to(node.dispatcher)
24
+ # node.listen.join
25
+ #
26
+ # # clients can now invoke the 'do_something' json-rpc method by
27
+ # # issuing requests to the target host / port
28
+ #
29
+ module HandlesMethods
30
+ def self.included(base)
31
+ base.extend(ClassMethods)
32
+ end
33
+
34
+ # Override w/ custom handler logic
35
+ def handle
36
+ end
37
+
38
+ module ClassMethods
39
+ attr_accessor :jr_handlers
40
+
41
+ # Return the handler method matching the argument set
42
+ def extract_handler_method(args)
43
+ handler = nil
44
+
45
+ if method_defined?(args.last)
46
+ handler = args.last
47
+ args.delete_at(-1)
48
+
49
+ else
50
+ handler = :handle
51
+ end
52
+
53
+ [handler, args]
54
+ end
55
+
56
+ # Return bool indicating if handler exists for the specified method
57
+ def has_handler_for?(handler_method)
58
+ @jr_handlers ||= {}
59
+ @jr_handlers.has_key?(handler_method)
60
+ end
61
+
62
+ # Returns handler for specified method
63
+ def handler_for(handler_method)
64
+ @jr_handlers[handler_method]
65
+ end
66
+
67
+ # Create handler for specified method.
68
+ #
69
+ # Creates a proc that gets evaluated via instance_exec in request
70
+ def create_handler_for(handler_method)
71
+ @jr_handlers ||= {}
72
+ handler_class = self
73
+
74
+ @jr_handlers[handler_method] = proc { |*args|
75
+ # instantiate new handler instance
76
+ jr_instance = handler_class.new
77
+
78
+ # setup scope to include request variables
79
+ instance_variables.each { |iv|
80
+ jr_instance.instance_variable_set(iv, instance_variable_get(iv))
81
+ }
82
+
83
+ # invoke handler method
84
+ jr_instance.method(handler_method).call *args
85
+ }
86
+ end
87
+
88
+ # Register one or more json-rpc methods.
89
+ #
90
+ # Invoke w/ list of method signatures to match in dispatcher
91
+ # w/ optional id of local method to dispatch to. If no method
92
+ # specified, the :handle method will be used
93
+ def jr_method(*args)
94
+ @jr_method_args ||= []
95
+ @jr_method_args << args
96
+ end
97
+
98
+ # Register locally stored methods w/ the specified dispatcher
99
+ def dispatch_to(dispatcher)
100
+ @jr_method_args.each { |args|
101
+ # copy args so original is preserved
102
+ handler_method, jr_methods =
103
+ extract_handler_method(Array.new(args))
104
+ jr_methods.map! { |m| m.to_s }
105
+
106
+ handler = has_handler_for?(handler_method) ?
107
+ handler_for(handler_method) :
108
+ create_handler_for(handler_method)
109
+
110
+ dispatcher.handle jr_methods, handler
111
+ }
112
+ end
113
+ end # module ClassMethods
114
+ end # module HandlesMethods
115
+ end # module RJR
@@ -0,0 +1,50 @@
1
+ # RJR HasMessages Mixin
2
+ #
3
+ # Copyright (C) 2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ module RJR
7
+
8
+ # Mixin adding methods allowing developer to define performatted
9
+ # messages on a class. After they are defined they may be retrieved,
10
+ # manipulated, and sent to the server at any time
11
+ module HasMessages
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ # Wrapper around HasMessages#message
17
+ def define_message(name, &bl)
18
+ self.class.message(name, bl.call)
19
+ end
20
+
21
+ module ClassMethods
22
+ # Mechanism to register / retrieve preformatted message
23
+ #
24
+ # @param [Symbol] id id of message to get / set
25
+ # @param [String] msg optional preformatted message to store
26
+ # @return [String] json rpc message
27
+ def message(id, msg=nil)
28
+ @rjr_messages ||= {}
29
+ @rjr_messages[id] = msg unless msg.nil?
30
+ @rjr_messages[id]
31
+ end
32
+
33
+ # Clear preformatted messages
34
+ def clear_messages
35
+ @rjr_messages = {}
36
+ end
37
+
38
+ # Return random message from registry.
39
+ #
40
+ # Optionally specify the transport which the message must accept.
41
+ # TODO turn this into a generic selection callback
42
+ def rand_message(transport = nil)
43
+ @rjr_messages ||= {}
44
+ messages = @rjr_messages.select { |mid,m| m[:transports].nil? || transport.nil? ||
45
+ m[:transports].include?(transport) }
46
+ messages[messages.keys[rand(messages.keys.size)]]
47
+ end
48
+ end
49
+ end # module HasMessages
50
+ end # module RJR
@@ -51,7 +51,7 @@ def select_stats(dispatcher, *filter)
51
51
  end
52
52
 
53
53
  # Add inspection methods to specified dispatcher
54
- def dispatch_rjr_inspect(dispatcher)
54
+ def dispatch_rjr_util_inspect(dispatcher)
55
55
  # Retrieve all the dispatches this node served matching the specified criteri
56
56
  dispatcher.handle "rjr::dispatches" do |filter|
57
57
  select_stats(dispatcher, *filter)
@@ -0,0 +1,101 @@
1
+ # RJR JSON Parser
2
+ #
3
+ # Copyright (C) 2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'json'
7
+ require 'rjr/common'
8
+
9
+ module RJR
10
+
11
+ # Provides utilities / helpers to parse json in a sane/safe manner
12
+ class JSONParser
13
+
14
+ # Extract and return a single json message from a data string.
15
+ #
16
+ # Returns the message and remaining portion of the data string,
17
+ # if message is found, else nil
18
+ #
19
+ # TODO efficiency can probably be optimized in the case closing '}'
20
+ # hasn't arrived yet
21
+ #
22
+ # FIXME if uneven brackets appears in string data (such as in params)
23
+ # this will break, detect when in string and ignore in counts
24
+ def self.extract_json_from(data)
25
+ return nil if data.nil? || data.empty?
26
+ start = 0
27
+ start += 1 until start == data.length || data[start].chr == '{'
28
+ on = mi = 0
29
+ start.upto(data.length - 1).each { |i|
30
+ if data[i].chr == '{'
31
+ on += 1
32
+ elsif data[i].chr == '}'
33
+ on -= 1
34
+ end
35
+
36
+ if on == 0
37
+ mi = i
38
+ break
39
+ end
40
+ }
41
+
42
+ return nil if mi == 0
43
+ return data[start..mi], data[(mi+1)..-1]
44
+ end
45
+
46
+ # Return bool indicating if json class is invalid in context
47
+ # of rjr & cannot be parsed.
48
+ #
49
+ # An invalid class is one not on whitelist if enabled or one
50
+ # not in ruby class heirachy.
51
+ #
52
+ # Implements a safe mechanism which to validate json data
53
+ # to parse
54
+ def self.invalid_json_class?(jc)
55
+ Class.whitelist_json_classes ||= false
56
+ Class.whitelist_json_classes ?
57
+ !Class.permitted_json_classes.include?(jc) : jc.to_s.to_class.nil?
58
+ end
59
+
60
+ def self.validate_json_hash(jh)
61
+ jh.each { |k,v|
62
+ if k == ::JSON.create_id && invalid_json_class?(v)
63
+ raise ArgumentError, "can't create json class #{v}"
64
+ elsif v.is_a?(Array)
65
+ validate_json_array(v)
66
+ elsif v.is_a?(Hash)
67
+ validate_json_hash(v)
68
+ end
69
+ }
70
+ end
71
+
72
+ def self.validate_json_array(ja)
73
+ ja.each { |jai|
74
+ if jai.is_a?(Array)
75
+ validate_json_array(jai)
76
+ elsif jai.is_a?(Hash)
77
+ validate_json_hash(jai)
78
+ end
79
+ }
80
+ end
81
+
82
+ # Two stage json parser.
83
+ # For more details why this is required see json issue:
84
+ # https://github.com/flori/json/issues/179
85
+ #
86
+ # FIXME this will only work for json >= 1.7.6 where
87
+ # create_additions is defined
88
+ def self.parse(js)
89
+ jp = ::JSON.parse js, :create_additions => false
90
+ if jp.is_a?(Array)
91
+ validate_json_array(jp)
92
+ elsif jp.is_a?(Hash)
93
+ validate_json_hash(jp)
94
+ else
95
+ return jp
96
+ end
97
+ ::JSON.parse js, :create_additions => true
98
+ end
99
+
100
+ end # class JSONParser
101
+ end # module RJR
@@ -0,0 +1,128 @@
1
+ # RJR Logger Class
2
+ #
3
+ # Copyright (C) 2011-2014 Mohammed Morsi <mo@morsi.org>
4
+ # Licensed under the Apache License, Version 2.0
5
+
6
+ require 'logger'
7
+
8
+ module RJR
9
+
10
+ # Logger helper class.
11
+ #
12
+ # Encapsulates the standard ruby logger in a thread safe manner. Dispatches
13
+ # class methods to an internally tracked logger to provide global access.
14
+ #
15
+ # TODO handle logging errors (log size too big, logrotate, etc)
16
+ #
17
+ # @example
18
+ # RJR::Logger.info 'my message'
19
+ # RJR::Logger.warn 'my warning'
20
+ class Logger
21
+ private
22
+ def self._instantiate_logger
23
+ if @logger.nil?
24
+ #STDOUT.sync = true
25
+ output = @log_to || ENV['RJR_LOG'] || STDOUT
26
+ @logger = ::Logger.new(output)
27
+ @logger.level = @log_level || ::Logger::FATAL
28
+ @logger_mutex = Mutex.new
29
+ @filters = []
30
+ @highlights = []
31
+ end
32
+ end
33
+
34
+ public
35
+
36
+ # Add method which to call on every log message to determine
37
+ # if messages should be included/excluded
38
+ def self.add_filter(filter)
39
+ @logger_mutex.synchronize{
40
+ @filters << filter
41
+ }
42
+ end
43
+
44
+ # Add a method which to call on every log message to determine
45
+ # if message should be highlighted
46
+ def self.highlight(hlight)
47
+ @logger_mutex.synchronize{
48
+ @highlights << hlight
49
+ }
50
+ end
51
+
52
+ def self.method_missing(method_id, *args)
53
+ _instantiate_logger
54
+ @logger_mutex.synchronize {
55
+ args = args.first if args.first.is_a?(Array)
56
+ args.each { |a|
57
+ # run highlights / filters against output before
58
+ # sending formatted output to logger
59
+ # TODO allow user to customize highlight mechanism/text
60
+ na = @highlights.any? { |h| h.call a } ?
61
+ "\e[1m\e[31m#{a}\e[0m\e[0m" : a
62
+ @logger.send(method_id, na) if @filters.all? { |f| f.call a }
63
+ }
64
+ }
65
+ end
66
+
67
+ def self.safe_exec(*args, &bl)
68
+ _instantiate_logger
69
+ @logger_mutex.synchronize {
70
+ bl.call *args
71
+ }
72
+ end
73
+
74
+ def self.logger
75
+ _instantiate_logger
76
+ @logger
77
+ end
78
+
79
+ # Set log destination
80
+ # @param dst destination which to log to (file name, STDOUT, etc)
81
+ def self.log_to(dst)
82
+ @log_to = dst
83
+ @logger = nil
84
+ _instantiate_logger
85
+ end
86
+
87
+ # Set log level.
88
+ # @param level one of the standard rails log levels (default fatal)
89
+ def self.log_level=(level)
90
+ _instantiate_logger
91
+ if level.is_a?(String)
92
+ level = case level
93
+ when 'debug' then
94
+ ::Logger::DEBUG
95
+ when 'info' then
96
+ ::Logger::INFO
97
+ when 'warn' then
98
+ ::Logger::WARN
99
+ when 'error' then
100
+ ::Logger::ERROR
101
+ when 'fatal' then
102
+ ::Logger::FATAL
103
+ end
104
+ end
105
+ @log_level = level
106
+ @logger.level = level
107
+ end
108
+
109
+ # Return true if log level is set to debug, else false
110
+ def self.debug?
111
+ @log_level == ::Logger::DEBUG
112
+ end
113
+ end
114
+
115
+ end # module RJR
116
+
117
+ # Serialized puts, uses logger lock to serialize puts output.
118
+ # Definiting it in Kernel as 'puts' is defined there
119
+ #
120
+ # Though this could go in core_ext, since it's pretty specific
121
+ # to RJR logger, adding here
122
+ module Kernel
123
+ def sputs(*args)
124
+ ::RJR::Logger.safe_exec {
125
+ puts *args
126
+ }
127
+ end
128
+ end
@@ -3,6 +3,8 @@
3
3
  # Copyright (C) 2010-2013 Mohammed Morsi <mo@morsi.org>
4
4
  # Licensed under the Apache License, Version 2.0
5
5
 
6
+ require 'thread'
7
+
6
8
  module RJR
7
9
 
8
10
  # Work item to be executed in a thread launched by {ThreadPool}.
@@ -1,3 +1,3 @@
1
1
  module RJR
2
- VERSION = '0.18.2'
2
+ VERSION = '0.19.1'
3
3
  end
@@ -318,7 +318,7 @@ RJR.WsNode.prototype = {
318
318
 
319
319
  // Close socket connection
320
320
  close : function(){
321
- this.socket.close();
321
+ if(this.socket) this.socket.close();
322
322
  },
323
323
 
324
324
  // Invoke request on socket, may be invoked before or after socket is opened.
@@ -0,0 +1,144 @@
1
+ require 'rjr/util/args'
2
+
3
+ module RJR
4
+ describe Arguments do
5
+ describe "#intialize" do
6
+ it "initializes arguments" do
7
+ a = Arguments.new :args => [42]
8
+ a.args.should == [42]
9
+ end
10
+ end
11
+
12
+ describe "#validate!" do
13
+ context "acceptable is a hash" do
14
+ context "at least one argument not in acceptable hash keys" do
15
+ it "raises an ArgumentError" do
16
+ a = Arguments.new :args => ['invalid', 42]
17
+ expect {
18
+ a.validate! 'valid' => 1
19
+ }.to raise_error(ArgumentError)
20
+ end
21
+ end
22
+
23
+ context "all arguments are in acceptable hash keys" do
24
+ it "does not raise an error" do
25
+ a = Arguments.new :args => ['valid', 42]
26
+ expect {
27
+ a.validate! 'valid' => 1
28
+ }.not_to raise_error
29
+ end
30
+ end
31
+
32
+ context "no arguments" do
33
+ it "does not raise an error" do
34
+ a = Arguments.new
35
+ expect {
36
+ a.validate! 'valid' => 1
37
+ }.not_to raise_error
38
+ end
39
+ end
40
+
41
+ it "converts acceptable keys to strings before comparison" do
42
+ a = Arguments.new :args => ['valid', 42]
43
+ expect {
44
+ a.validate! :valid => 1
45
+ }.not_to raise_error
46
+ end
47
+
48
+ it "skips over # of arguments specified by acceptable hash values" do
49
+ a = Arguments.new :args => ['valid', 42]
50
+ expect {
51
+ a.validate! :valid => 1
52
+ }.not_to raise_error
53
+
54
+ expect {
55
+ a.validate! :valid => 0
56
+ }.to raise_error(ArgumentError)
57
+ end
58
+ end
59
+
60
+ context "acceptable is an array" do
61
+ context "at least one argument not on acceptable list" do
62
+ it "raises an ArgumentError" do
63
+ a = Arguments.new :args => ['invalid']
64
+ expect {
65
+ a.validate! 'valid'
66
+ }.to raise_error(ArgumentError)
67
+ end
68
+ end
69
+
70
+ context "all arguments on acceptable list" do
71
+ it "does not raise an error" do
72
+ a = Arguments.new :args => ['valid']
73
+ expect {
74
+ a.validate! 'valid'
75
+ }.not_to raise_error
76
+ end
77
+ end
78
+
79
+ context "no arguments" do
80
+ it "does not raise an error" do
81
+ a = Arguments.new
82
+ expect {
83
+ a.validate! 'valid'
84
+ }.not_to raise_error
85
+ end
86
+ end
87
+
88
+ it "converts acceptable list to strings before comparison" do
89
+ a = Arguments.new :args => ['valid']
90
+ expect {
91
+ a.validate! :valid
92
+ }.not_to raise_error
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "#extract" do
98
+ before(:each) do
99
+ @a = Arguments.new :args => ['match1', 'val1', 'val2',
100
+ 'match2', 'val2', 'match3']
101
+ end
102
+
103
+ it "returns extracted map key & following values from arguments" do
104
+ result = @a.extract 'match1' => 2, 'match3' => 0
105
+ result.should == [['match1', 'val1', 'val2'], ['match3']]
106
+ end
107
+
108
+ it "matches symbollic map keys" do
109
+ result = @a.extract :match2 => 1
110
+ result.should == [['match2', 'val2']]
111
+ end
112
+ end
113
+
114
+ describe "#specifies?" do
115
+ context "arguments includes tag" do
116
+ it "returns true" do
117
+ a = Arguments.new :args => ['with_id', 42]
118
+ a.specifies?('with_id').should be_true
119
+ end
120
+ end
121
+
122
+ context "arguments does not include tag" do
123
+ it "returns false" do
124
+ a = Arguments.new :args => ['with_id', 42]
125
+ a.specifies?('with_name').should be_false
126
+ end
127
+ end
128
+ end
129
+
130
+ describe "#specifier_for" do
131
+ it "returns argument at index of tag + 1" do
132
+ a = Arguments.new :args => ['with_id', 42]
133
+ a.specifier_for('with_id').should == 42
134
+ end
135
+
136
+ context "arguments do not specify tag" do
137
+ it "returns nil" do
138
+ a = Arguments.new :args => ['with_id', 42]
139
+ a.specifier_for('with_name').should be_nil
140
+ end
141
+ end
142
+ end
143
+ end # describe Arguments
144
+ end # module RJR