simrpc 0.1 → 0.2

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.
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