simrpc 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Mohammed Morsi <movitto@yahoo.com>
1
+ Copyright (c) 2010 Mohammed Morsi <movitto@yahoo.com>
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
data/README.rdoc ADDED
@@ -0,0 +1,79 @@
1
+ == Simrpc - A Simple RPC library using AMQP as the transport mechanism
2
+
3
+ Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+
5
+ Simrpc is made available under the MIT License
6
+
7
+ === Intro
8
+ Simrpc is a simple Ruby module for rpc communication
9
+ that uses Apache QPID as the transport mechanism.
10
+
11
+ Developers define class / function schemas in xml and register handlers
12
+ to be invoked when schema methods are called. Schema classes and members
13
+ are mapped to Ruby classes via Ruby's builtin introspection mechanisms.
14
+
15
+ To install simrpc simply run:
16
+ gem install simrpc
17
+
18
+ Source code is available via:
19
+ git clone http://github.com/movitto/simrpc
20
+
21
+ === Using
22
+ Generate documentation via
23
+ rake rdoc
24
+
25
+ Also see specs for detailed usage.
26
+
27
+ The primary interface to simrpc is provided by the 'node' module which
28
+ defines Simrpc::Node. This class should be instantiated using a custom identifier
29
+ for every simrpc endpoint you want to establish (nodes that share
30
+ an identifier will share a message queue) after which it can be used to
31
+ register handlers to schema methods and invoke remote methods.
32
+
33
+
34
+ == Example
35
+
36
+ TEST_SCHEMA =
37
+ "<schema>"+
38
+ " <method name='foo_method'>" +
39
+ " <param type='int' name='some_int'/>"+
40
+ " <param type='float' name='floating_point_number'/>"+
41
+ " <return_value type='str' name='a_string' />" +
42
+ " <return_value type='obj' name='my_class_instance' associated='MyClass' />" +
43
+ " </method>"+
44
+ " <method name='bar_method'>" +
45
+ " <param type='array' name='byte_array' associated='int'/>"+
46
+ " <return_value type='int' name='bool_success' />"+
47
+ " </method>"+
48
+ " <class name='MyClass'>"+
49
+ " <member type='str' name='str_member' />" +
50
+ " <member type='float' name='float_member' />" +
51
+ " <member type='obj' name='associated_obj' ignore_null='true' />" +
52
+ " </class>"+
53
+ "</schema>"
54
+
55
+ class MyClass
56
+ attr_accessor :str_member
57
+ attr_accessor :float_member
58
+ attr_accessor :associated_obj
59
+ end
60
+
61
+ server = Node.new(:id => "server3", :schema => TEST_SCHEMA)
62
+ client = Node.new(:id => "client3", :schema => TEST_SCHEMA, :destination => "server3")
63
+
64
+ server.handle_method("foo_method") { |some_int, floating_point_number|
65
+ some_int # => 10
66
+ floating_point_number # => 15.4
67
+
68
+ ["stuff", MyClass.new("foobar", 4.2)]
69
+ }
70
+
71
+ a_str, my_class_instance = client.foo_method(10, 15.4)
72
+
73
+ a_str # => "stuff"
74
+ my_class_instance.str_member # => "foobar"
75
+ my_class_instance.float_member # => 4.2
76
+
77
+
78
+ === Authors
79
+ Mohammed Morsi <movitto@yahoo.com>
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ # simrpc project Rakefile
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ # See LICENSE for the License of this software
5
+
6
+ require 'rake/rdoctask'
7
+ require 'spec/rake/spectask'
8
+ require 'rake/gempackagetask'
9
+
10
+ GEM_NAME="simrpc"
11
+ PKG_VERSION=0.2
12
+
13
+ desc "Run all specs"
14
+ Spec::Rake::SpecTask.new('spec') do |t|
15
+ t.spec_files = FileList['spec/*_spec.rb']
16
+ end
17
+
18
+ Rake::RDocTask.new do |rd|
19
+ rd.main = "README.rdoc"
20
+ rd.rdoc_dir = "doc/site/api"
21
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
22
+ end
23
+
24
+ PKG_FILES = FileList['lib/**/*.rb', 'LICENSE', 'Rakefile', 'README.rdoc', 'spec/**/*.rb' ]
25
+
26
+ SPEC = Gem::Specification.new do |s|
27
+ s.name = GEM_NAME
28
+ s.version = PKG_VERSION
29
+ s.files = PKG_FILES
30
+
31
+ s.required_ruby_version = '>= 1.8.1'
32
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.3.3")
33
+ # FIXME require qpid
34
+
35
+ s.author = "Mohammed Morsi"
36
+ s.email = "movitto@yahoo.com"
37
+ s.date = %q{2010-03-11}
38
+ s.description = %q{simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.}
39
+ s.summary = %q{simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.}
40
+ s.homepage = %q{http://projects.morsi.org/Simrpc}
41
+ end
42
+
43
+ Rake::GemPackageTask.new(SPEC) do |pkg|
44
+ pkg.need_tar = true
45
+ pkg.need_zip = true
46
+ end
@@ -0,0 +1,46 @@
1
+ # simrpc common module, methods that don't fit elsewhere
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ require 'logger'
27
+
28
+ module Simrpc
29
+
30
+ # Logger helper class
31
+ class Logger
32
+ private
33
+ def self._instantiate_logger
34
+ unless defined? @@logger
35
+ @@logger = ::Logger.new(STDOUT)
36
+ @@logger.level = ::Logger::FATAL # FATAL ERROR WARN INFO DEBUG
37
+ end
38
+ end
39
+ public
40
+ def self.method_missing(method_id, *args)
41
+ _instantiate_logger
42
+ @@logger.send(method_id, args)
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,30 @@
1
+ # simrpc exceptions, may be thrown in simrpc operations
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ class InvalidSchemaClass < RuntimeError
27
+ def self(msg = '')
28
+ super(msg)
29
+ end
30
+ end
@@ -0,0 +1,156 @@
1
+ # simrpc message module
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module Simrpc
27
+
28
+ # the message module provides the Message definition,
29
+ # including a header with routing info and a body
30
+ # with any number of data fields
31
+ module Message
32
+
33
+ # Simrpc::Message formatter helper module
34
+ class Formatter
35
+
36
+ # helper method to format a data field,
37
+ # prepending a fixed size to it
38
+ def self.format_with_size(data)
39
+ # currently size is set to a 8 digit int
40
+ len = "%08d" % data.to_s.size
41
+ len + data.to_s
42
+ end
43
+
44
+ # helper method to parse a data field
45
+ # off the front of a data sequence, using the
46
+ # formatted size. Returns parsed data field
47
+ # and remaining data sequence. If optional
48
+ # class is given, the from_s method will
49
+ # be invoked w/ the parsed data field and
50
+ # returned with the remaining data sequence
51
+ # instead
52
+ def self.parse_from_formatted(data, data_class = nil)
53
+ len = data[0...8].to_i
54
+ parsed = data[8...8+len]
55
+ remaining = data[8+len...data.size]
56
+ return parsed, remaining if data_class.nil?
57
+ return data_class.from_s(parsed), remaining
58
+ end
59
+ end
60
+
61
+ # a single field trasnmitted via a message,
62
+ # containing a key / value pair
63
+ class Field
64
+ attr_accessor :name, :value
65
+
66
+ def initialize(args = {})
67
+ @name = args[:name].nil? ? "" : args[:name]
68
+ @value = args[:value].nil? ? "" : args[:value]
69
+ end
70
+
71
+ def to_s
72
+ Formatter::format_with_size(@name) + Formatter::format_with_size(@value)
73
+ end
74
+
75
+ def self.from_s(data)
76
+ field = Field.new
77
+ field.name, data = Formatter::parse_from_formatted(data)
78
+ field.value, data = Formatter::parse_from_formatted(data)
79
+ return field
80
+ end
81
+ end
82
+
83
+ # header contains various descriptive properies
84
+ # about a message
85
+ class Header
86
+ attr_accessor :type, :target
87
+
88
+ def initialize(args = {})
89
+ @type = args[:type].nil? ? "" : args[:type]
90
+ @target = args[:target].nil? ? "" : args[:target]
91
+ end
92
+
93
+ def to_s
94
+ Formatter::format_with_size(@type) + Formatter::format_with_size(@target)
95
+ end
96
+
97
+ def self.from_s(data)
98
+ header = Header.new
99
+ header.type, data = Formatter::parse_from_formatted(data)
100
+ header.target, data = Formatter::parse_from_formatted(data)
101
+ return header
102
+ end
103
+ end
104
+
105
+ # body consists of a list of data fields
106
+ class Body
107
+ attr_accessor :fields
108
+
109
+ def initialize
110
+ @fields = []
111
+ end
112
+
113
+ def to_s
114
+ s = ''
115
+ @fields.each { |field|
116
+ fs = field.to_s
117
+ s += Formatter::format_with_size(fs)
118
+ }
119
+ return s
120
+ end
121
+
122
+ def self.from_s(data)
123
+ body = Body.new
124
+ while(data != "")
125
+ field, data = Formatter::parse_from_formatted(data)
126
+ field = Field.from_s field
127
+ body.fields.push field
128
+ end
129
+ return body
130
+ end
131
+ end
132
+
133
+ # message contains a header / body
134
+ class Message
135
+ attr_accessor :header, :body
136
+
137
+ def initialize
138
+ @header = Header.new
139
+ @body = Body.new
140
+ end
141
+
142
+ def to_s
143
+ Formatter::format_with_size(@header) + Formatter::format_with_size(@body)
144
+ end
145
+
146
+ def self.from_s(data)
147
+ message = Message.new
148
+ message.header, data = Formatter::parse_from_formatted(data, Header)
149
+ message.body, data = Formatter::parse_from_formatted(data, Body)
150
+ return message
151
+ end
152
+ end
153
+
154
+ end # module Message
155
+
156
+ end # module Simrpc
@@ -0,0 +1,206 @@
1
+ # simrpc node module
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module Simrpc
27
+
28
+ # Simrpc Method Message Controller, generates and handles method messages
29
+ class MethodMessageController
30
+ public
31
+ # initialize with a specified schema definition
32
+ def initialize(schema_def)
33
+ @schema_def = schema_def
34
+ end
35
+
36
+ # generate new new method message, setting the message
37
+ # target to the specified method name, and setting the fields
38
+ # on the message to the method arguments
39
+ def generate(method_name, args)
40
+ @schema_def.methods.each { |method|
41
+ if method.name == method_name
42
+ msg = Message::Message.new
43
+ msg.header.type = 'request'
44
+ msg.header.target = method.name
45
+
46
+ # loop through each param, convering corresponding
47
+ # argument to message field and adding it to msg
48
+ i = 0
49
+ method.parameters.each { |param|
50
+ field = Message::Field.new
51
+ field.name = param.name
52
+ field.value = param.to_s(args[i], @schema_def)
53
+ msg.body.fields.push field
54
+ i += 1
55
+ }
56
+
57
+ return msg
58
+ end
59
+ }
60
+ return nil
61
+ end
62
+
63
+ # should be invoked when a message is received,
64
+ # takes a message, converts it into a method call, and calls the corresponding
65
+ # handler in the provided schema. Takes return arguments and sends back to caller
66
+ def message_received(node, message, reply_to)
67
+ message = Message::Message::from_s(message)
68
+ @schema_def.methods.each { |method|
69
+
70
+ if method.name == message.header.target
71
+ Logger.info "received method #{method.name} message "
72
+
73
+ # for request messages, dispatch to method handler
74
+ if message.header.type != 'response' && method.handler != nil
75
+ # order the params
76
+ params = []
77
+ method.parameters.each { |data_field|
78
+ value_field = message.body.fields.find { |f| f.name == data_field.name }
79
+ params.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
80
+ }
81
+
82
+ Logger.info "invoking #{method.name} handler "
83
+
84
+ # invoke method handler
85
+ return_values = method.handler.call(*params) # FIXME handlers can't use 'return' as this will fall through here
86
+ # FIXME throw a catch block around this call to catch all handler exceptions
87
+ return_values = [return_values] unless return_values.is_a? Array
88
+
89
+ # if method returns no values, do not return response
90
+ unless method.return_values.size == 0
91
+
92
+ # consruct and send response message using return values
93
+ response = Message::Message.new
94
+ response.header.type = 'response'
95
+ response.header.target = method.name
96
+ (0...method.return_values.size).each { |rvi|
97
+ field = Message::Field.new
98
+ field.name = method.return_values[rvi].name
99
+ field_def = method.return_values.find { |rv| rv.name == field.name }
100
+ field.value = field_def.to_s(return_values[rvi], @schema_def) unless field_def.nil? # TODO what if field_def is nil
101
+ response.body.fields.push field
102
+ }
103
+ Logger.info "responding to #{reply_to}"
104
+ node.send_message(reply_to, response)
105
+
106
+ end
107
+
108
+ # for response values just return converted return values
109
+ else
110
+ results = []
111
+ method.return_values.each { |data_field|
112
+ value_field = message.body.fields.find { |f| f.name == data_field.name }
113
+ results.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
114
+ }
115
+ return results
116
+ end
117
+ end
118
+ }
119
+ end
120
+ end
121
+
122
+ # Simrpc Node represents ths main api which to communicate and send/listen for data.
123
+ class Node
124
+
125
+ # Instantiate it w/ a specified id
126
+ # or one will be autogenerated. Specify schema (or location) containing
127
+ # data and methods which to invoke and/or handle. Optionally specify
128
+ # a remote destination which to send new messages to. Automatically listens
129
+ # for incoming messages.
130
+ def initialize(args = {})
131
+ @id = args[:id] if args.has_key? :id
132
+ @schema = args[:schema]
133
+ @schema_file = args[:schema_file]
134
+ @destination = args[:destination]
135
+
136
+ if !@schema.nil?
137
+ @schema_def = Schema::Parser.parse(:schema => @schema)
138
+ elsif !@schema_file.nil?
139
+ @schema_def = Schema::Parser.parse(:file => @schema_file)
140
+ end
141
+ raise ArgumentError, "schema_def cannot be nil" if @schema_def.nil?
142
+ @mmc = MethodMessageController.new(@schema_def)
143
+ @message_lock = Semaphore.new(1)
144
+ @message_lock.wait
145
+
146
+ # FIXME currently not allowing for any other params to be passed into
147
+ # QpidAdapter::Node such as broker ip or port, NEED TO FIX THIS
148
+ @qpid_node = QpidAdapter::Node.new(:id => @id)
149
+ @qpid_node.async_accept { |node, msg, reply_to|
150
+ results = @mmc.message_received(node, msg, reply_to)
151
+ message_received(results)
152
+ }
153
+ end
154
+
155
+ def id
156
+ return @id unless @id.nil?
157
+ return @qpid_node.node_id
158
+ end
159
+
160
+ # implements, message_received callback to be notified when qpid receives a message
161
+ def message_received(results)
162
+ @message_results = results
163
+ @message_lock.signal
164
+ end
165
+
166
+ # wait until the node is no longer accepting messages
167
+ def join
168
+ @qpid_node.join
169
+ end
170
+
171
+ # add a handler which to invoke when an schema method is invoked
172
+ def handle_method(method, &handler)
173
+ @schema_def.methods.each { |smethod|
174
+ if smethod.name == method.to_s
175
+ smethod.handler = handler
176
+ break
177
+ end
178
+ }
179
+ end
180
+
181
+ # send method request to remote destination w/ the specified args
182
+ def send_method(method_name, destination, *args)
183
+ # generate and send new method message
184
+ msg = @mmc.generate(method_name, args)
185
+ @qpid_node.send_message(destination + "-queue", msg)
186
+
187
+ # FIXME race condition if response is received b4 wait is invoked
188
+
189
+ # block if we are expecting return values
190
+ if @schema_def.methods.find{|m| m.name == method_name}.return_values.size != 0
191
+ @message_lock.wait # block until response received
192
+
193
+ # return return values
194
+ #@message_received.body.fields.collect { |f| f.value }
195
+ return *@message_results
196
+ end
197
+ end
198
+
199
+ # can invoke schema methods directly on Node instances, this will catch
200
+ # them and send them onto the destination
201
+ def method_missing(method_id, *args)
202
+ send_method(method_id.to_s, @destination, *args)
203
+ end
204
+ end
205
+
206
+ end # module Simrpc