protobuf 1.0.0

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 (113) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +28 -0
  4. data/README.md +216 -0
  5. data/Rakefile +1 -0
  6. data/bin/rpc_server +117 -0
  7. data/bin/rprotoc +46 -0
  8. data/examples/addressbook.pb.rb +55 -0
  9. data/examples/addressbook.proto +24 -0
  10. data/examples/reading_a_message.rb +32 -0
  11. data/examples/writing_a_message.rb +46 -0
  12. data/lib/protobuf.rb +6 -0
  13. data/lib/protobuf/common/exceptions.rb +11 -0
  14. data/lib/protobuf/common/logger.rb +64 -0
  15. data/lib/protobuf/common/util.rb +59 -0
  16. data/lib/protobuf/common/wire_type.rb +10 -0
  17. data/lib/protobuf/compiler/compiler.rb +52 -0
  18. data/lib/protobuf/compiler/nodes.rb +323 -0
  19. data/lib/protobuf/compiler/proto.y +216 -0
  20. data/lib/protobuf/compiler/proto2.ebnf +79 -0
  21. data/lib/protobuf/compiler/proto_parser.rb +1425 -0
  22. data/lib/protobuf/compiler/template/rpc_bin.erb +4 -0
  23. data/lib/protobuf/compiler/template/rpc_client.erb +18 -0
  24. data/lib/protobuf/compiler/template/rpc_service.erb +25 -0
  25. data/lib/protobuf/compiler/template/rpc_service_implementation.erb +42 -0
  26. data/lib/protobuf/compiler/visitors.rb +302 -0
  27. data/lib/protobuf/descriptor/descriptor.proto +286 -0
  28. data/lib/protobuf/descriptor/descriptor.rb +55 -0
  29. data/lib/protobuf/descriptor/descriptor_builder.rb +143 -0
  30. data/lib/protobuf/descriptor/descriptor_proto.rb +138 -0
  31. data/lib/protobuf/descriptor/enum_descriptor.rb +33 -0
  32. data/lib/protobuf/descriptor/field_descriptor.rb +49 -0
  33. data/lib/protobuf/descriptor/file_descriptor.rb +37 -0
  34. data/lib/protobuf/message/decoder.rb +83 -0
  35. data/lib/protobuf/message/encoder.rb +46 -0
  36. data/lib/protobuf/message/enum.rb +62 -0
  37. data/lib/protobuf/message/extend.rb +8 -0
  38. data/lib/protobuf/message/field.rb +701 -0
  39. data/lib/protobuf/message/message.rb +402 -0
  40. data/lib/protobuf/message/protoable.rb +38 -0
  41. data/lib/protobuf/rpc/buffer.rb +74 -0
  42. data/lib/protobuf/rpc/client.rb +268 -0
  43. data/lib/protobuf/rpc/client_connection.rb +225 -0
  44. data/lib/protobuf/rpc/error.rb +34 -0
  45. data/lib/protobuf/rpc/error/client_error.rb +31 -0
  46. data/lib/protobuf/rpc/error/server_error.rb +43 -0
  47. data/lib/protobuf/rpc/rpc.pb.rb +107 -0
  48. data/lib/protobuf/rpc/server.rb +183 -0
  49. data/lib/protobuf/rpc/service.rb +244 -0
  50. data/lib/protobuf/rpc/stat.rb +70 -0
  51. data/lib/protobuf/version.rb +3 -0
  52. data/proto/rpc.proto +73 -0
  53. data/protobuf.gemspec +25 -0
  54. data/script/mk_parser +2 -0
  55. data/spec/functional/embedded_service_spec.rb +7 -0
  56. data/spec/proto/test.pb.rb +31 -0
  57. data/spec/proto/test.proto +31 -0
  58. data/spec/proto/test_service.rb +30 -0
  59. data/spec/proto/test_service_impl.rb +17 -0
  60. data/spec/spec_helper.rb +26 -0
  61. data/spec/unit/client_spec.rb +128 -0
  62. data/spec/unit/common/logger_spec.rb +121 -0
  63. data/spec/unit/enum_spec.rb +13 -0
  64. data/spec/unit/message_spec.rb +67 -0
  65. data/spec/unit/server_spec.rb +27 -0
  66. data/spec/unit/service_spec.rb +75 -0
  67. data/test/check_unbuild.rb +30 -0
  68. data/test/data/data.bin +3 -0
  69. data/test/data/data_source.py +14 -0
  70. data/test/data/types.bin +0 -0
  71. data/test/data/types_source.py +22 -0
  72. data/test/data/unk.png +0 -0
  73. data/test/proto/addressbook.pb.rb +66 -0
  74. data/test/proto/addressbook.proto +33 -0
  75. data/test/proto/addressbook_base.pb.rb +58 -0
  76. data/test/proto/addressbook_base.proto +26 -0
  77. data/test/proto/addressbook_ext.pb.rb +20 -0
  78. data/test/proto/addressbook_ext.proto +6 -0
  79. data/test/proto/collision.pb.rb +17 -0
  80. data/test/proto/collision.proto +5 -0
  81. data/test/proto/ext_collision.pb.rb +24 -0
  82. data/test/proto/ext_collision.proto +8 -0
  83. data/test/proto/ext_range.pb.rb +22 -0
  84. data/test/proto/ext_range.proto +7 -0
  85. data/test/proto/float_default.proto +10 -0
  86. data/test/proto/lowercase.pb.rb +30 -0
  87. data/test/proto/lowercase.proto +9 -0
  88. data/test/proto/merge.pb.rb +39 -0
  89. data/test/proto/merge.proto +15 -0
  90. data/test/proto/nested.pb.rb +30 -0
  91. data/test/proto/nested.proto +9 -0
  92. data/test/proto/optional_field.pb.rb +35 -0
  93. data/test/proto/optional_field.proto +12 -0
  94. data/test/proto/packed.pb.rb +22 -0
  95. data/test/proto/packed.proto +6 -0
  96. data/test/proto/rpc.proto +6 -0
  97. data/test/proto/types.pb.rb +84 -0
  98. data/test/proto/types.proto +37 -0
  99. data/test/test_addressbook.rb +56 -0
  100. data/test/test_compiler.rb +325 -0
  101. data/test/test_descriptor.rb +122 -0
  102. data/test/test_enum_value.rb +41 -0
  103. data/test/test_extension.rb +36 -0
  104. data/test/test_lowercase.rb +11 -0
  105. data/test/test_message.rb +128 -0
  106. data/test/test_optional_field.rb +103 -0
  107. data/test/test_packed_field.rb +40 -0
  108. data/test/test_parse.rb +15 -0
  109. data/test/test_repeated_types.rb +132 -0
  110. data/test/test_serialize.rb +61 -0
  111. data/test/test_standard_message.rb +96 -0
  112. data/test/test_types.rb +226 -0
  113. metadata +261 -0
@@ -0,0 +1,34 @@
1
+ require 'protobuf/rpc/rpc.pb'
2
+
3
+ module Protobuf
4
+ module Rpc
5
+
6
+ autoload :BadRequestData, 'protobuf/rpc/error/server_error'
7
+ autoload :BadRequestProto, 'protobuf/rpc/error/server_error'
8
+ autoload :ServiceNotFound, 'protobuf/rpc/error/server_error'
9
+ autoload :MethodNotFound, 'protobuf/rpc/error/server_error'
10
+ autoload :RpcError, 'protobuf/rpc/error/server_error'
11
+ autoload :RpcFailed, 'protobuf/rpc/error/server_error'
12
+
13
+ autoload :InvalidRequestProto, 'protobuf/rpc/error/client_error'
14
+ autoload :BadResponseProto, 'protobuf/rpc/error/client_error'
15
+ autoload :UnknownHost, 'protobuf/rpc/error/client_error'
16
+ autoload :IOError, 'protobuf/rpc/error/client_error'
17
+
18
+ # Base RpcError class for client and server errors
19
+ class PbError < StandardError
20
+ attr_reader :error_type
21
+
22
+ def initialize message='An unknown RpcError occurred', error_type='RPC_ERROR'
23
+ @error_type = error_type.is_a?(String) ? Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type
24
+ super message
25
+ end
26
+
27
+ def to_response response
28
+ response.error = message
29
+ response.error_reason = @error_type
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ require 'protobuf/rpc/error'
2
+
3
+ module Protobuf
4
+ module Rpc
5
+
6
+ class InvalidRequestProto < PbError
7
+ def initialize message='Invalid request type given'
8
+ super message, 'INVALID_REQUEST_PROTO'
9
+ end
10
+ end
11
+
12
+ class BadResponseProto < PbError
13
+ def initialize message='Bad response type from server'
14
+ super message, 'BAD_RESPONSE_PROTO'
15
+ end
16
+ end
17
+
18
+ class UnkownHost < PbError
19
+ def initialize message='Unknown host or port'
20
+ super message, 'UNKNOWN_HOST'
21
+ end
22
+ end
23
+
24
+ class IOError < PbError
25
+ def initialize message='IO Error occurred'
26
+ super message, 'IO_ERROR'
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ require 'protobuf/rpc/rpc.pb'
2
+
3
+ module Protobuf
4
+ module Rpc
5
+
6
+ class BadRequestData < PbError
7
+ def initialize message='Unable to parse request'
8
+ super message, 'BAD_REQUEST_DATA'
9
+ end
10
+ end
11
+
12
+ class BadRequestProto < PbError
13
+ def initialize message='Request is of wrong type'
14
+ super message, 'BAD_REQUEST_PROTO'
15
+ end
16
+ end
17
+
18
+ class ServiceNotFound < PbError
19
+ def initialize message='Service class not found'
20
+ super message, 'SERVICE_NOT_FOUND'
21
+ end
22
+ end
23
+
24
+ class MethodNotFound < PbError
25
+ def initialize message='Service method not found'
26
+ super message, 'METHOD_NOT_FOUND'
27
+ end
28
+ end
29
+
30
+ class RpcError < PbError
31
+ def initialize message='RPC exception occurred'
32
+ super message, 'RPC_ERROR'
33
+ end
34
+ end
35
+
36
+ class RpcFailed < PbError
37
+ def initialize message='RPC failed'
38
+ super message, 'RPC_FAILED'
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,107 @@
1
+ ### Generated by rprotoc. DO NOT EDIT!
2
+ ### <proto file: rpc.proto>
3
+ # // Copyright (c) 2009 Shardul Deo
4
+ # //
5
+ # // Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # // of this software and associated documentation files (the "Software"), to deal
7
+ # // in the Software without restriction, including without limitation the rights
8
+ # // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # // copies of the Software, and to permit persons to whom the Software is
10
+ # // furnished to do so, subject to the following conditions:
11
+ # //
12
+ # // The above copyright notice and this permission notice shall be included in
13
+ # // all copies or substantial portions of the Software.
14
+ # //
15
+ # // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # // THE SOFTWARE.
22
+ #
23
+ # // Author: Shardul Deo
24
+ # //
25
+ # // Protobufs needed for socket rpcs.
26
+ #
27
+ # package protobuf.socketrpc;
28
+ #
29
+ # message Request {
30
+ #
31
+ # // RPC service full name
32
+ # required string service_name = 1;
33
+ #
34
+ # // RPC method name
35
+ # required string method_name = 2;
36
+ #
37
+ # // RPC request proto
38
+ # required bytes request_proto = 3;
39
+ # }
40
+ #
41
+ # message Response {
42
+ #
43
+ # // RPC response proto
44
+ # optional bytes response_proto = 1;
45
+ #
46
+ # // Error, if any
47
+ # optional string error = 2;
48
+ #
49
+ # // Was callback invoked
50
+ # optional bool callback = 3 [default = false];
51
+ #
52
+ # // Error Reason
53
+ # optional ErrorReason error_reason = 4;
54
+ # }
55
+ #
56
+ # // Possible error reasons
57
+ # // The server-side errors are returned in the response from the server.
58
+ # // The client-side errors are returned by the client-side code when it doesn't
59
+ # // have a response from the server.
60
+ # enum ErrorReason {
61
+ #
62
+ # // Server-side errors
63
+ # BAD_REQUEST_DATA = 0; // Server received bad request data
64
+ # BAD_REQUEST_PROTO = 1; // Server received bad request proto
65
+ # SERVICE_NOT_FOUND = 2; // Service not found on server
66
+ # METHOD_NOT_FOUND = 3; // Method not found on server
67
+ # RPC_ERROR = 4; // Rpc threw exception on server
68
+ # RPC_FAILED = 5; // Rpc failed on server
69
+ #
70
+ # // Client-side errors (these are returned by the client-side code)
71
+ # INVALID_REQUEST_PROTO = 6; // Rpc was called with invalid request proto
72
+ # BAD_RESPONSE_PROTO = 7; // Server returned a bad response proto
73
+ # UNKNOWN_HOST = 8; // Could not find supplied host
74
+ # IO_ERROR = 9; // I/O error while communicating with server
75
+ # }
76
+
77
+ require 'protobuf/message/message'
78
+ require 'protobuf/message/enum'
79
+ require 'protobuf/message/extend'
80
+
81
+ module Protobuf
82
+ module Socketrpc
83
+ class Request < ::Protobuf::Message
84
+ required :string, :service_name, 1
85
+ required :string, :method_name, 2
86
+ required :bytes, :request_proto, 3
87
+ end
88
+ class Response < ::Protobuf::Message
89
+ optional :bytes, :response_proto, 1
90
+ optional :string, :error, 2
91
+ optional :bool, :callback, 3, :default => false
92
+ optional :ErrorReason, :error_reason, 4
93
+ end
94
+ class ErrorReason < ::Protobuf::Enum
95
+ define :BAD_REQUEST_DATA, 0
96
+ define :BAD_REQUEST_PROTO, 1
97
+ define :SERVICE_NOT_FOUND, 2
98
+ define :METHOD_NOT_FOUND, 3
99
+ define :RPC_ERROR, 4
100
+ define :RPC_FAILED, 5
101
+ define :INVALID_REQUEST_PROTO, 6
102
+ define :BAD_RESPONSE_PROTO, 7
103
+ define :UNKNOWN_HOST, 8
104
+ define :IO_ERROR, 9
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,183 @@
1
+ require 'eventmachine'
2
+ require 'socket'
3
+ require 'protobuf/common/logger'
4
+ require 'protobuf/rpc/rpc.pb'
5
+ require 'protobuf/rpc/buffer'
6
+ require 'protobuf/rpc/error'
7
+ require 'protobuf/rpc/stat'
8
+
9
+ module Protobuf
10
+ module Rpc
11
+ class Server < EventMachine::Connection
12
+ include Protobuf::Logger::LogMethods
13
+
14
+ # Initialize a new read buffer for storing client request info
15
+ def post_init
16
+ log_debug '[server] Post init, new read buffer created'
17
+
18
+ @stats = Protobuf::Rpc::Stat.new(:SERVER, true)
19
+ @stats.client = Socket.unpack_sockaddr_in(get_peername)
20
+
21
+ @buffer = Protobuf::Rpc::Buffer.new :read
22
+ @did_respond = false
23
+ end
24
+
25
+ # Receive a chunk of data, potentially flushed to handle_client
26
+ def receive_data data
27
+ log_debug '[server] receive_data: %s' % data
28
+ @buffer << data
29
+ handle_client if @buffer.flushed?
30
+ end
31
+
32
+ # Invoke the service method dictated by the proto wrapper request object
33
+ def handle_client
34
+ @stats.request_size = @buffer.size
35
+
36
+ # Setup the initial request and response
37
+ @request = Protobuf::Socketrpc::Request.new
38
+ @response = Protobuf::Socketrpc::Response.new
39
+
40
+ # Parse the protobuf request from the socket
41
+ log_debug '[server] Parsing request from client'
42
+ parse_request_from_buffer
43
+
44
+ # Determine the service class and method name from the request
45
+ log_debug '[server] Extracting procedure call info from request'
46
+ parse_service_info
47
+
48
+ # Call the service method
49
+ log_debug '[server] Dispatching client request to service'
50
+ invoke_rpc_method
51
+
52
+ rescue => error
53
+ # Ensure we're handling any errors that try to slip out the back door
54
+ log_error error.message
55
+ log_error error.backtrace.join("\n")
56
+ handle_error(error)
57
+ send_response
58
+ end
59
+
60
+ private
61
+
62
+ # Assuming all things check out, we can call the service method
63
+ def invoke_rpc_method
64
+ # Get a new instance of the service
65
+ @service = @klass.new
66
+
67
+ # Define our response callback to perform the "successful" response to our client
68
+ # This decouples the service's rpc method from our response to the client,
69
+ # allowing the service to be the dictator for when the response should be sent back.
70
+ #
71
+ # In other words, we don't send the response once the service method finishes executing
72
+ # since the service may perform it's own operations asynchronously.
73
+ @service.on_send_response do |response|
74
+ unless @did_respond
75
+ parse_response_from_service(response)
76
+ send_response
77
+ end
78
+ end
79
+
80
+ @service.on_rpc_failed do |error|
81
+ unless @did_respond
82
+ handle_error(error)
83
+ send_response
84
+ end
85
+ end
86
+
87
+ # Call the service method
88
+ log_debug '[server] Invoking %s#%s with request %s' % [@klass.name, @method, @request.inspect]
89
+ @service.__send__ @method, @request
90
+ end
91
+
92
+ # Parse the incoming request object into our expected request object
93
+ def parse_request_from_buffer
94
+ begin
95
+ log_debug '[server] parsing request from buffer: %s' % @buffer.data.inspect
96
+ @request.parse_from_string @buffer.data
97
+ rescue => error
98
+ exc = BadRequestData.new 'Unable to parse request: %s' % error.message
99
+ log_error exc.message
100
+ raise exc
101
+ end
102
+ end
103
+
104
+ # Read out the response from the service method,
105
+ # setting it on the pb request, and serializing the
106
+ # response to the protobuf response wrapper
107
+ def parse_response_from_service response
108
+ begin
109
+ expected = @klass.rpcs[@klass][@method].response_type
110
+
111
+ # Cannibalize the response if it's a Hash
112
+ response = expected.new(response) if response.is_a?(Hash)
113
+ actual = response.class
114
+
115
+ log_debug '[server] response (should/actual): %s/%s' % [expected.name, actual.name]
116
+
117
+ # Determine if the service tried to change response types on us
118
+ if expected == actual
119
+ begin
120
+ # Response types match, so go ahead and serialize
121
+ log_debug '[server] serializing response: %s' % response.inspect
122
+ @response.response_proto = response.serialize_to_string
123
+ rescue
124
+ raise BadResponseProto, $!.message
125
+ end
126
+ else
127
+ # response types do not match, throw the appropriate error
128
+ raise BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name]
129
+ end
130
+ rescue => error
131
+ log_error error.message
132
+ log_error error.backtrace.join("\n")
133
+ handle_error(error)
134
+ end
135
+ end
136
+
137
+ # Write the response wrapper to the client
138
+ def send_response
139
+ raise 'Response already sent to client' if @did_respond
140
+ log_debug '[server] Sending response to client: %s' % @response.inspect
141
+ response_buffer = Protobuf::Rpc::Buffer.new(:write, @response)
142
+ send_data(response_buffer.write)
143
+ @stats.response_size = response_buffer.size
144
+ @stats.end
145
+ @stats.log_stats
146
+ @did_respond = true
147
+ end
148
+
149
+ # Client error handler. Receives an exception object and writes it into the @response
150
+ def handle_error error
151
+ log_debug '[server] handle_error: %s' % error.inspect
152
+ if error.is_a? PbError
153
+ error.to_response @response
154
+ elsif error.is_a? ClientError
155
+ PbError.new(error.message, error.code.to_s).to_response @response
156
+ else
157
+ message = error.is_a?(String) ? error : error.message
158
+ PbError.new(message, 'RPC_ERROR').to_response @response
159
+ end
160
+ end
161
+
162
+ # Parses and returns the service and method name from the request wrapper proto
163
+ def parse_service_info
164
+ @klass, @method = nil, nil
165
+
166
+ begin
167
+ @klass = Util.constantize(@request.service_name)
168
+ rescue
169
+ raise ServiceNotFound, "Service class #{@request.service_name} is not found"
170
+ end
171
+
172
+ @method = Util.underscore(@request.method_name).to_sym
173
+ unless @klass.instance_methods.include?(@method)
174
+ raise MethodNotFound, "Service method #{@request.method_name} is not defined by the service"
175
+ end
176
+
177
+ @stats.service = @klass.name
178
+ @stats.method = @method
179
+ end
180
+
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,244 @@
1
+ require 'protobuf/common/logger'
2
+ require 'protobuf/rpc/client'
3
+ require 'protobuf/rpc/error'
4
+
5
+ module Protobuf
6
+ module Rpc
7
+
8
+ # Object to encapsulate the request/response types for a given service method
9
+ #
10
+ RpcMethod = Struct.new "RpcMethod", :service, :method, :request_type, :response_type
11
+
12
+ class Service
13
+ include Protobuf::Logger::LogMethods
14
+
15
+ attr_reader :request
16
+ attr_accessor :response, :async_responder
17
+ private :request, :response, :response=
18
+
19
+ DEFAULT_LOCATION = {
20
+ :host => 'localhost',
21
+ :port => 9939
22
+ }
23
+
24
+ # Class methods are intended for use on the client-side.
25
+ #
26
+ class << self
27
+
28
+ # You MUST add the method name to this list if you are adding
29
+ # instance methods below, otherwise stuff will definitely break
30
+ NON_RPC_METHODS = %w( rpcs call_rpc on_rpc_failed rpc_failed request response method_missing async_responder on_send_response send_response )
31
+
32
+ # Override methods being added to the class
33
+ # If the method isn't already a private instance method, or it doesn't start with rpc_,
34
+ # or it isn't in the reserved method list (NON_RPC_METHODS),
35
+ # We want to remap the method such that we can wrap it in before and after behavior,
36
+ # most notably calling call_rpc against the method. See call_rpc for more info.
37
+ def method_added old
38
+ new_method = :"rpc_#{old}"
39
+ return if private_instance_methods.include?(new_method) or old =~ /^rpc_/ or NON_RPC_METHODS.include?(old.to_s)
40
+
41
+ alias_method new_method, old
42
+ private new_method
43
+
44
+ begin
45
+ define_method(old) do |pb_request|
46
+ call_rpc old.to_sym, pb_request
47
+ end
48
+ rescue ArgumentError => e
49
+ # Wrap a known issue where an instance method was defined in the class without
50
+ # it being ignored with NON_RPC_METHODS.
51
+ raise ArgumentError, "#{e.message} (Note: This could mean that you need to add the method #{old} to the NON_RPC_METHODS list)"
52
+ end
53
+ end
54
+
55
+ # Generated service classes should call this method on themselves to add rpc methods
56
+ # to the stack with a given request and response type
57
+ def rpc method, request_type, response_type
58
+ rpcs[self] ||= {}
59
+ rpcs[self][method] = RpcMethod.new self, method, request_type, response_type
60
+ end
61
+
62
+ # Shorthand for @rpcs class instance var
63
+ def rpcs
64
+ @rpcs ||= {}
65
+ end
66
+
67
+ # Create a new client for the given service.
68
+ # See Client#initialize and ClientConnection::DEFAULT_OPTIONS
69
+ # for all available options.
70
+ #
71
+ def client options={}
72
+ configure
73
+ Client.new({
74
+ :service => self,
75
+ :async => false,
76
+ :host => self.host,
77
+ :port => self.port
78
+ }.merge(options))
79
+ end
80
+
81
+ # Allows service-level configuration of location.
82
+ # Useful for system-startup configuration of a service
83
+ # so that any Clients using the Service.client sugar
84
+ # will not have to configure the location each time.
85
+ #
86
+ def configure config={}
87
+ locations[self] ||= {}
88
+ locations[self][:host] = config[:host] if config.key? :host
89
+ locations[self][:port] = config[:port] if config.key? :port
90
+ end
91
+
92
+ # Shorthand call to configure, passing a string formatted as hostname:port
93
+ # e.g. 127.0.0.1:9933
94
+ # e.g. localhost:0
95
+ #
96
+ def located_at location
97
+ return if location.nil? or location.downcase.strip !~ /[a-z0-9.]+:\d+/
98
+ host, port = location.downcase.strip.split ':'
99
+ configure :host => host, :port => port.to_i
100
+ end
101
+
102
+ # The host location of the service
103
+ def host
104
+ configure
105
+ locations[self][:host] || DEFAULT_LOCATION[:host]
106
+ end
107
+
108
+ # The port of the service on the destination server
109
+ def port
110
+ configure
111
+ locations[self][:port] || DEFAULT_LOCATION[:port]
112
+ end
113
+
114
+ # Shorthand for @locations class instance var
115
+ def locations
116
+ @locations ||= {}
117
+ end
118
+
119
+ end
120
+
121
+ # If a method comes through that hasn't been found, and it
122
+ # is defined in the rpcs method list, we know that the rpc
123
+ # stub has been created, but no implementing method provides the
124
+ # functionality, so throw an appropriate error, otherwise go to super
125
+ #
126
+ def method_missing m, *params
127
+ if rpcs.key?(m)
128
+ exc = MethodNotFound.new "#{self}##{m} was defined as a valid rpc method, but was not implemented."
129
+ log_error exc.message
130
+ raise exc
131
+ else
132
+ log_error '-------------- [service] %s#%s not rpc method, passing to super' % [self.class.name, m.to_s]
133
+ super m, params
134
+ end
135
+ end
136
+
137
+ # Convenience wrapper around the rpc method list for a given class
138
+ def rpcs
139
+ self.class.rpcs[self.class]
140
+ end
141
+
142
+ # Callback register for the server when a service
143
+ # method calls rpc_failed. Called by Service#rpc_failed.
144
+ def on_rpc_failed &rpc_failure_callback
145
+ @rpc_failure_callback = rpc_failure_callback
146
+ end
147
+
148
+ # Automatically fail a service method.
149
+ # NOTE: This shortcuts the @async_responder paradigm. There is
150
+ # not any way to get around this currently (and I'm not sure you should want to).
151
+ #
152
+ def rpc_failed message="RPC Failed while executing service method #{@current_method}"
153
+ if @rpc_failure_callback.nil?
154
+ exc = RuntimeError.new 'Unable to invoke rpc_failed, no failure callback is setup.'
155
+ log_error exc.message
156
+ raise exc
157
+ end
158
+ error = message.is_a?(String) ? RpcFailed.new(message) : message
159
+ log_warn '[service] RPC Failed: %s' % error.message
160
+ @rpc_failure_callback.call(error)
161
+ end
162
+
163
+ # Callback register for the server to be notified
164
+ # when it is appropriate to generate a response to the client.
165
+ # Used in conjunciton with Service#send_response.
166
+ #
167
+ def on_send_response &responder
168
+ @responder = responder
169
+ end
170
+
171
+ # Tell the server to generate response and send it to the client.
172
+ #
173
+ # NOTE: If @async_responder is set to true, this MUST be called by
174
+ # the implementing service method, otherwise the connection
175
+ # will timeout since no data will be sent.
176
+ #
177
+ def send_response
178
+ if @responder.nil?
179
+ exc = RuntimeError.new "Unable to send response, responder is nil. It appears you aren't inside of an RPC request/response cycle."
180
+ log_error exc.message
181
+ raise exc
182
+ end
183
+ @responder.call @response
184
+ end
185
+
186
+ private
187
+
188
+ # Call the rpc method that was previously privatized.
189
+ # call_rpc allows us to wrap the normal method call with
190
+ # before and after behavior, most notably setting up the request
191
+ # and response instances.
192
+ #
193
+ # Implementing rpc methods should be aware
194
+ # that request and response are implicitly available, and
195
+ # that response should be manipulated during the rpc method,
196
+ # as there is no way to reliably determine the response like
197
+ # a normal (http-based) controller method would be able to.
198
+ #
199
+ # Async behavior of responding can be achieved in the rpc method
200
+ # by explicitly setting self.async_responder = true. It is then
201
+ # the responsibility of the service method to send the response,
202
+ # by calling self.send_response without any arguments. The rpc
203
+ # server is setup to handle synchronous and asynchronous responses.
204
+ #
205
+ def call_rpc method, pb_request
206
+ @current_method = method
207
+
208
+ # Allows the service to set whether or not
209
+ # it would like to asynchronously respond to the connected client(s)
210
+ @async_responder = false
211
+
212
+ begin
213
+ # Setup the request
214
+ @request = rpcs[method].request_type.new
215
+ @request.parse_from_string pb_request.request_proto
216
+ rescue
217
+ exc = BadRequestProto.new 'Unable to parse request: %s' % $!.message
218
+ log_error exc.message
219
+ log_error $!.backtrace.join("\n")
220
+ raise exc
221
+ end
222
+
223
+ # Setup the response
224
+ @response = rpcs[method].response_type.new
225
+
226
+ log_debug '[service] calling service method %s#%s' % [self.class.name, method]
227
+ # Call the aliased rpc method (e.g. :rpc_find for :find)
228
+ __send__("rpc_#{method}".to_sym)
229
+ log_debug '[service] completed service method %s#%s' % [self.class.name, method]
230
+
231
+ # Pass the populated response back to the server
232
+ # Note this will only get called if the rpc method didn't explode (by design)
233
+ if @async_responder
234
+ log_debug '[service] async request, not sending response (yet)'
235
+ else
236
+ log_debug '[service] trigger server send_response'
237
+ send_response
238
+ end
239
+ end
240
+
241
+ end
242
+
243
+ end
244
+ end