rjr 0.18.2 → 0.19.1

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