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 +1 -1
- data/README.rdoc +79 -0
- data/Rakefile +46 -0
- data/lib/simrpc/common.rb +46 -0
- data/lib/simrpc/exceptions.rb +30 -0
- data/lib/simrpc/message.rb +156 -0
- data/lib/simrpc/node.rb +206 -0
- data/lib/simrpc/qpid_adapter.rb +171 -0
- data/lib/simrpc/schema.rb +400 -0
- data/lib/simrpc.rb +9 -802
- data/spec/common_spec.rb +26 -0
- data/spec/message_spec.rb +93 -0
- data/spec/node_spec.rb +99 -0
- data/spec/qpid_spec.rb +92 -0
- data/spec/schema_spec.rb +348 -0
- data/spec/spec_helper.rb +56 -0
- metadata +25 -23
- data/README +0 -25
- /data/lib/{semaphore.rb → simrpc/semaphore.rb} +0 -0
@@ -0,0 +1,171 @@
|
|
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
|
+
require 'qpid'
|
27
|
+
require 'socket'
|
28
|
+
require 'semaphore'
|
29
|
+
|
30
|
+
module Simrpc
|
31
|
+
|
32
|
+
# The QpidAdapter module implements the simrpc qpid subsystem, providing
|
33
|
+
# a convenient way to access qpid constructs
|
34
|
+
module QpidAdapter
|
35
|
+
|
36
|
+
# Simrpc::Qpid::Node class, represents an enpoint on a qpid
|
37
|
+
# network which has its own exchange and queue which it listens on
|
38
|
+
class Node
|
39
|
+
private
|
40
|
+
# helper method to generate a random id
|
41
|
+
def gen_uuid
|
42
|
+
["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
|
43
|
+
Array.new(16) {|x| rand(0xff) }
|
44
|
+
end
|
45
|
+
|
46
|
+
public
|
47
|
+
# a node can have children nodes mapped to by keys
|
48
|
+
attr_accessor :children
|
49
|
+
|
50
|
+
# node always has a node id
|
51
|
+
attr_reader :node_id
|
52
|
+
|
53
|
+
# create the qpid base connection with the specified broker / port
|
54
|
+
# or config file. Then establish exchange and queue and start listening
|
55
|
+
# for requests.
|
56
|
+
#
|
57
|
+
# specify :broker and :port arguments to directly connect to those
|
58
|
+
# specify :config argument to use that yml file
|
59
|
+
# specify MOTEL_AMQP_CONF environment variable to use that yml file
|
60
|
+
# specify :id parameter to set id, else it will be set to a uuid just created
|
61
|
+
def initialize(args = {})
|
62
|
+
# if no id specified generate a new uuid
|
63
|
+
@node_id = args[:id].nil? ? gen_uuid : args[:id]
|
64
|
+
|
65
|
+
# we generate a random session id
|
66
|
+
@session_id = gen_uuid
|
67
|
+
|
68
|
+
# get the broker/port
|
69
|
+
broker = args[:broker].nil? ? "localhost" : args[:broker]
|
70
|
+
port = args[:port].nil? ? 5672 : args[:port]
|
71
|
+
|
72
|
+
if (broker.nil? || port.nil?) && args.has_key?(:config)
|
73
|
+
config =
|
74
|
+
amqpconfig = YAML::load(File.open(args[:config]))
|
75
|
+
broker = amqpconfig["broker"] if broker.nil?
|
76
|
+
port = amqpconfig["port"] if port.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
### create underlying tcp connection
|
80
|
+
@conn = Qpid::Connection.new(TCPSocket.new(broker,port))
|
81
|
+
@conn.start
|
82
|
+
|
83
|
+
### connect to qpid broker
|
84
|
+
@ssn = @conn.session(@session_id)
|
85
|
+
|
86
|
+
@children = {}
|
87
|
+
|
88
|
+
@accept_lock = Semaphore.new(1)
|
89
|
+
|
90
|
+
# qpid constructs that will be created for node
|
91
|
+
@exchange = args[:exchange].nil? ? @node_id.to_s + "-exchange" : args[:exchange]
|
92
|
+
@queue = args[:queue].nil? ? @node_id.to_s + "-queue" : args[:queue]
|
93
|
+
@local_queue = args[:local_queue].nil? ? @node_id.to_s + "-local-queue" : args[:local_queue]
|
94
|
+
@routing_key = @queue
|
95
|
+
|
96
|
+
Logger.warn "creating qpid exchange #{@exchange} queue #{@queue} binding_key #{@routing_key}"
|
97
|
+
|
98
|
+
if @ssn.exchange_query(@exchange).not_found
|
99
|
+
@ssn.exchange_declare(@exchange, :type => "direct")
|
100
|
+
end
|
101
|
+
|
102
|
+
if @ssn.queue_query(@queue).queue.nil?
|
103
|
+
@ssn.queue_declare(@queue)
|
104
|
+
end
|
105
|
+
|
106
|
+
@ssn.exchange_bind(:exchange => @exchange,
|
107
|
+
:queue => @queue,
|
108
|
+
:binding_key => @routing_key)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Instruct Node to start accepting requests asynchronously and immediately return.
|
112
|
+
# handler must be callable and take node, msg, respond_to arguments, corresponding to
|
113
|
+
# 'self', the message received', and the routing_key which to send any response.
|
114
|
+
def async_accept(&handler)
|
115
|
+
# TODO permit a QpidNode to accept messages from multiple exchanges/queues
|
116
|
+
@accept_lock.wait
|
117
|
+
|
118
|
+
# subscribe to the queue
|
119
|
+
@ssn.message_subscribe(:destination => @local_queue,
|
120
|
+
:queue => @queue,
|
121
|
+
:accept_mode => @ssn.message_accept_mode.none)
|
122
|
+
@incoming = @ssn.incoming(@local_queue)
|
123
|
+
@incoming.start
|
124
|
+
|
125
|
+
Logger.warn "listening for messages on #{@queue}"
|
126
|
+
|
127
|
+
# start receiving messages
|
128
|
+
@incoming.listen{ |msg|
|
129
|
+
Logger.info "queue #{@queue} received message #{msg.body.to_s.size} #{msg.body}"
|
130
|
+
reply_to = msg.get(:message_properties).reply_to.routing_key
|
131
|
+
handler.call(self, msg.body, reply_to)
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
# block until accept operation is complete
|
136
|
+
def join
|
137
|
+
@accept_lock.wait
|
138
|
+
end
|
139
|
+
|
140
|
+
# instructs QpidServer to stop accepting, blocking
|
141
|
+
# untill all accepting operations have terminated
|
142
|
+
def terminate
|
143
|
+
Logger.warn "terminating qpid session"
|
144
|
+
unless @incoming.nil?
|
145
|
+
@incoming.stop
|
146
|
+
@incoming.close
|
147
|
+
@accept_lock.signal
|
148
|
+
end
|
149
|
+
@ssn.close
|
150
|
+
# TODO undefine the @queue/@exchange
|
151
|
+
end
|
152
|
+
|
153
|
+
# send a message to the specified routing_key
|
154
|
+
def send_message(routing_key, message)
|
155
|
+
dp = @ssn.delivery_properties(:routing_key => routing_key)
|
156
|
+
mp = @ssn.message_properties( :content_type => "text/plain")
|
157
|
+
rp = @ssn.message_properties( :reply_to =>
|
158
|
+
@ssn.reply_to(@exchange, @routing_key))
|
159
|
+
msg = Qpid::Message.new(dp, mp, rp, message.to_s)
|
160
|
+
|
161
|
+
Logger.warn "sending qpid message #{msg.body} to #{routing_key}"
|
162
|
+
|
163
|
+
# send it
|
164
|
+
@ssn.message_transfer(:message => msg)
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end # module QpidAdapter
|
170
|
+
|
171
|
+
end # module Simrpc
|
@@ -0,0 +1,400 @@
|
|
1
|
+
# simrpc schema 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
|
+
require 'rexml/document'
|
27
|
+
|
28
|
+
# FIXME store lengths in binary instead of ascii string
|
29
|
+
|
30
|
+
module Simrpc
|
31
|
+
|
32
|
+
# schema module defines classes / methods to define
|
33
|
+
# a simrpc schema / load it from an xml file
|
34
|
+
module Schema
|
35
|
+
|
36
|
+
Types = [ :str, :int, :float, :bool, :obj, :array ]
|
37
|
+
|
38
|
+
# return true if type is a primitive, else false
|
39
|
+
def is_primitive?(type)
|
40
|
+
return [:str, :int, :float, :bool].include?(type)
|
41
|
+
end
|
42
|
+
module_function :is_primitive?
|
43
|
+
|
44
|
+
# convert primitive type from string
|
45
|
+
def primitive_from_str(type, str)
|
46
|
+
if type == :str
|
47
|
+
return str
|
48
|
+
|
49
|
+
elsif type == :int
|
50
|
+
return str.to_i
|
51
|
+
|
52
|
+
elsif type == :float
|
53
|
+
return str.to_f
|
54
|
+
|
55
|
+
elsif type == :bool
|
56
|
+
return str == "true"
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
module_function :primitive_from_str
|
61
|
+
|
62
|
+
# Data field defintion containing a type, name, and value.
|
63
|
+
# Optinally an associated class or type may be given.
|
64
|
+
# Associated only valid for :obj and :array types, and
|
65
|
+
# must be set to associated ClassDef or Type (:array only)
|
66
|
+
class DataFieldDef
|
67
|
+
attr_accessor :type, :name, :associated
|
68
|
+
|
69
|
+
# indicates this data field should be ignored if it has a null value
|
70
|
+
attr_accessor :ignore_null
|
71
|
+
|
72
|
+
def initialize(args = {})
|
73
|
+
@type = args[:type] unless args[:type].nil?
|
74
|
+
@name = args[:name] unless args[:name].nil?
|
75
|
+
@associated = args[:associated] unless args[:associated].nil?
|
76
|
+
@ignore_null = !args[:ignore_null].nil? && args[:ignore_null]
|
77
|
+
end
|
78
|
+
|
79
|
+
# helper method to lookup and return the specified class name in the
|
80
|
+
# specified schema
|
81
|
+
def find_class_def(cl_name, schema_def)
|
82
|
+
return schema_def.classes.find { |cl| cl.name == cl_name.to_s } unless cl_name.nil? || schema_def.nil?
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# helper method to lookup and return the class definition corresponding to the associated
|
87
|
+
# attribte in the specified schema def
|
88
|
+
def associated_class_def(schema_def)
|
89
|
+
find_class_def(@associated, schema_def) unless @associated.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
# convert given value of this data field into a string. Provide schema_def
|
93
|
+
# for :obj or :array data fields associated w/ a non-primitive type. converted_classes
|
94
|
+
# is a recursive helper array used/maintained internally
|
95
|
+
def to_s(value, schema_def = nil, converted_classes = [])
|
96
|
+
if value.nil?
|
97
|
+
return ""
|
98
|
+
elsif Schema::is_primitive?(@type)
|
99
|
+
return value.to_s
|
100
|
+
|
101
|
+
elsif type == :array
|
102
|
+
str = "%04d" % value.size
|
103
|
+
value.each { |val|
|
104
|
+
if Schema::is_primitive?(@associated)
|
105
|
+
str += "%04d" % val.to_s.size
|
106
|
+
str += val.to_s
|
107
|
+
else
|
108
|
+
cl_def = associated_class_def(schema_def)
|
109
|
+
unless cl_def.nil?
|
110
|
+
cl_s = cl_def.to_s(val, schema_def, converted_classes)
|
111
|
+
str += "%04d" % cl_s.size
|
112
|
+
str += cl_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
}
|
116
|
+
return str
|
117
|
+
|
118
|
+
# elsif @type == :map # TODO
|
119
|
+
|
120
|
+
elsif type == :obj
|
121
|
+
cl_name = ""
|
122
|
+
cl_def = associated_class_def(schema_def)
|
123
|
+
|
124
|
+
# if associated class isn't specified, store the class name,
|
125
|
+
# providing for generic object support
|
126
|
+
if cl_def.nil?
|
127
|
+
cl_name = value.class.to_s.demodulize
|
128
|
+
cl_def = find_class_def(cl_name, schema_def)
|
129
|
+
raise InvalidSchemaClass.new("cannot find #{cl_name} in schema") if cl_def.nil?
|
130
|
+
cl_name = "%04d" % cl_name.size + cl_name
|
131
|
+
end
|
132
|
+
|
133
|
+
return cl_name + cl_def.to_s(value, schema_def, converted_classes)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# convert given string representation of this data field into its original value.
|
138
|
+
# Provide schema_def for :obj or :array data fields associated w/ non-primitive types
|
139
|
+
# # coverted_classes is a recursive helper array used/maintained internally
|
140
|
+
def from_s(str, schema_def = nil, converted_classes = [])
|
141
|
+
if str == ""
|
142
|
+
return nil
|
143
|
+
|
144
|
+
elsif Schema::is_primitive?(@type)
|
145
|
+
return Schema::primitive_from_str(@type, str)
|
146
|
+
|
147
|
+
elsif @type == :array
|
148
|
+
res = []
|
149
|
+
cl_def = associated_class_def(schema_def) unless Schema::is_primitive?(@associated)
|
150
|
+
alen = str[0...4].to_i
|
151
|
+
apos = 4
|
152
|
+
(0...alen).each { |i|
|
153
|
+
elen = str[apos...apos+4].to_i
|
154
|
+
parsed = str[apos+4...apos+4+elen]
|
155
|
+
if Schema::is_primitive?(@associated)
|
156
|
+
p = Schema::primitive_from_str(@associated, parsed)
|
157
|
+
res.push p
|
158
|
+
else
|
159
|
+
res.push cl_def.from_s(parsed, schema_def, converted_classes)
|
160
|
+
end
|
161
|
+
apos = apos+4+elen
|
162
|
+
}
|
163
|
+
return res
|
164
|
+
|
165
|
+
# elsif @type == :map # TODO
|
166
|
+
|
167
|
+
elsif @type == :obj
|
168
|
+
cl_def = associated_class_def(schema_def)
|
169
|
+
|
170
|
+
# if associated class isn't specified, parse the class name,
|
171
|
+
# providing for generic object support
|
172
|
+
if cl_def.nil?
|
173
|
+
cnlen = str[0...4].to_i
|
174
|
+
cname = str[4...cnlen+4]
|
175
|
+
str = str[cnlen+4...str.size]
|
176
|
+
cl_def = find_class_def(cname, schema_def)
|
177
|
+
raise InvalidSchemaClass.new("cannot find #{cname} in schema") if cl_def.nil?
|
178
|
+
end
|
179
|
+
|
180
|
+
return cl_def.from_s(str, schema_def, converted_classes)
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
# A class definition, containing data members.
|
188
|
+
# Right now we build into this the assumption that
|
189
|
+
# the 'name' attribute will share the same name as
|
190
|
+
# the actual class name which data will be mapped to / from
|
191
|
+
# and accessors exist on it corresponding to the names of
|
192
|
+
# each of the members
|
193
|
+
class ClassDef
|
194
|
+
# class name
|
195
|
+
# array of DataFieldDef
|
196
|
+
# base class name
|
197
|
+
attr_accessor :name, :members, :inherits
|
198
|
+
|
199
|
+
def initialize
|
200
|
+
@members = []
|
201
|
+
end
|
202
|
+
|
203
|
+
def base_class_def(schema_def)
|
204
|
+
return schema_def.classes.find { |cl| cl.name == inherits.to_s } unless inherits.nil? || schema_def.nil?
|
205
|
+
end
|
206
|
+
|
207
|
+
# convert value instance of class represented by this ClassDef
|
208
|
+
# into a string. schema_def must be provided if this ClassDef
|
209
|
+
# contains any associated class members. converted_classes is
|
210
|
+
# a recursive helper array used internally
|
211
|
+
def to_s(value, schema_def = nil, converted_classes = [])
|
212
|
+
return "O" + ("%04d" % converted_classes.index(value)) if converted_classes.include? value # if we already converted the class, store 'O' + its index
|
213
|
+
converted_classes.push value
|
214
|
+
|
215
|
+
# just encode each member w/ length
|
216
|
+
str = "I" # NEED to have something here incase the length of the first member is the same as the ascii character for 'O'
|
217
|
+
unless value.nil?
|
218
|
+
@members.each { |member|
|
219
|
+
mval = value.send(member.name.intern) if value.respond_to? member.name.intern
|
220
|
+
#mval = value.method(member.name.intern).call # invoke member getter
|
221
|
+
mstr = member.to_s(mval, schema_def, converted_classes)
|
222
|
+
mlen = "%04d" % mstr.size
|
223
|
+
#unless mstr == "" && member.ignore_null
|
224
|
+
str += mlen + mstr
|
225
|
+
#end
|
226
|
+
}
|
227
|
+
|
228
|
+
# encode and append base class
|
229
|
+
base_class = base_class_def(schema_def)
|
230
|
+
until base_class.nil?
|
231
|
+
base_class.members.each { |member|
|
232
|
+
mval = value.send(member.name.intern) if value.respond_to? member.name.intern
|
233
|
+
mstr = member.to_s(mval, schema_def, converted_classes)
|
234
|
+
mlen = "%04d" % mstr.size
|
235
|
+
str += mlen + mstr
|
236
|
+
}
|
237
|
+
base_class = base_class.base_class_def(schema_def)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
return str
|
242
|
+
end
|
243
|
+
|
244
|
+
# convert string instance of class represented by this ClassDef
|
245
|
+
# into actual class instance. schema_def must be provided if this
|
246
|
+
# ClassDef contains any associated class members.
|
247
|
+
# The converted_classes recursive helper array is used internally.
|
248
|
+
def from_s(str, schema_def = nil, converted_classes = [])
|
249
|
+
return nil if str == "I"
|
250
|
+
|
251
|
+
if str[0] == "O" # if we already converted the class, simply return that
|
252
|
+
return converted_classes[str[1...5].to_i]
|
253
|
+
end
|
254
|
+
|
255
|
+
# construct an instance of the class
|
256
|
+
cl = Object.module_eval("::#{@name}", __FILE__, __LINE__)
|
257
|
+
obj = cl.new if cl.respond_to? :new
|
258
|
+
obj = cl.instance if obj.nil? && cl.respond_to?(:instance)
|
259
|
+
raise InvalidSchemaClass.new("cannot create schema class #{@name}") if obj.nil?
|
260
|
+
|
261
|
+
# decode each member
|
262
|
+
mpos = 1 # start at 1 to skip the 'I'
|
263
|
+
@members.each { |member|
|
264
|
+
mlen = str[mpos...mpos+4].to_i
|
265
|
+
parsed = str[mpos+4...mpos+4+mlen]
|
266
|
+
parsed_o = member.from_s(parsed, schema_def, converted_classes)
|
267
|
+
unless parsed_o.nil? && member.ignore_null
|
268
|
+
member_method = (member.name + "=").intern
|
269
|
+
obj.send(member_method, parsed_o) if obj.respond_to? member_method # invoke member setter
|
270
|
+
end
|
271
|
+
mpos = mpos+4+mlen
|
272
|
+
}
|
273
|
+
|
274
|
+
# decode base object from string
|
275
|
+
base_class = base_class_def(schema_def)
|
276
|
+
until base_class.nil?
|
277
|
+
base_class.members.each { |member|
|
278
|
+
mlen = str[mpos...mpos+4].to_i
|
279
|
+
parsed = str[mpos+4...mpos+4+mlen]
|
280
|
+
parsed_o = member.from_s(parsed, schema_def, converted_classes)
|
281
|
+
unless parsed_o.nil? && member.ignore_null
|
282
|
+
member_method = (member.name + "=").intern
|
283
|
+
obj.send(member_method, parsed_o) if obj.respond_to? member_method # invoke member setter
|
284
|
+
end
|
285
|
+
mpos = mpos+4+mlen
|
286
|
+
}
|
287
|
+
|
288
|
+
base_class = base_class.base_class_def(schema_def)
|
289
|
+
end
|
290
|
+
|
291
|
+
return obj
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# method definition, containing parameters
|
296
|
+
# and return values. May optionally have
|
297
|
+
# a handler to be invoked when a remote
|
298
|
+
# entity invokes this method
|
299
|
+
class MethodDef
|
300
|
+
attr_accessor :name
|
301
|
+
|
302
|
+
# both are arrays of DataFieldDef
|
303
|
+
attr_accessor :parameters, :return_values
|
304
|
+
|
305
|
+
# should be a callable entity that takes
|
306
|
+
# the specified parameters, and returns
|
307
|
+
# the specified return values
|
308
|
+
attr_accessor :handler
|
309
|
+
|
310
|
+
def initialize
|
311
|
+
@parameters = []
|
312
|
+
@return_values = []
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# schema defintion including all defined classes and methods
|
317
|
+
class SchemaDef
|
318
|
+
# array of ClassDef
|
319
|
+
attr_accessor :classes
|
320
|
+
|
321
|
+
# array of MethodDef
|
322
|
+
attr_accessor :methods
|
323
|
+
|
324
|
+
def initialize
|
325
|
+
@classes = []
|
326
|
+
@methods = []
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# parse classes, methods, and data out of a xml definition
|
331
|
+
class Parser
|
332
|
+
|
333
|
+
# Parse and return a SchemaDef.
|
334
|
+
# Specify :schema argument containing xml schema to parse
|
335
|
+
# or :file containing location of file containing xml schema
|
336
|
+
def self.parse(args = {})
|
337
|
+
if(!args[:schema].nil?)
|
338
|
+
schema = args[:schema]
|
339
|
+
elsif(!args[:file].nil?)
|
340
|
+
schema = File.new(args[:file], "r")
|
341
|
+
end
|
342
|
+
|
343
|
+
schema_def = SchemaDef.new
|
344
|
+
|
345
|
+
unless schema.nil? || schema == ""
|
346
|
+
doc = REXML::Document.new(schema)
|
347
|
+
# grab each method definition
|
348
|
+
doc.elements.each('schema/method') do |ele|
|
349
|
+
method = MethodDef.new
|
350
|
+
method.name = ele.attributes["name"]
|
351
|
+
Logger.debug "parsed schema method #{method.name}"
|
352
|
+
|
353
|
+
ele.elements.each("param") do |param|
|
354
|
+
param = _parse_data_def(param)
|
355
|
+
method.parameters.push param
|
356
|
+
Logger.debug " parameter #{param.name}"
|
357
|
+
end
|
358
|
+
|
359
|
+
ele.elements.each("return_value") do |rv|
|
360
|
+
rv = _parse_data_def(rv)
|
361
|
+
method.return_values.push rv
|
362
|
+
Logger.debug " return_value #{rv.name}"
|
363
|
+
end
|
364
|
+
|
365
|
+
schema_def.methods.push method
|
366
|
+
end
|
367
|
+
|
368
|
+
# grab each class definition
|
369
|
+
doc.elements.each('schema/class') do |ele|
|
370
|
+
cl = ClassDef.new
|
371
|
+
cl.name = ele.attributes["name"]
|
372
|
+
cl.inherits = ele.attributes["inherits"]
|
373
|
+
Logger.debug "parsed schema class #{cl.name}"
|
374
|
+
|
375
|
+
ele.elements.each("member") do |mem|
|
376
|
+
mem = _parse_data_def(mem)
|
377
|
+
cl.members.push mem
|
378
|
+
Logger.debug " member #{mem.name}"
|
379
|
+
end
|
380
|
+
|
381
|
+
schema_def.classes.push cl
|
382
|
+
end
|
383
|
+
end
|
384
|
+
return schema_def
|
385
|
+
end
|
386
|
+
|
387
|
+
private
|
388
|
+
# helper method to parse a DataFieldDef out of an Element
|
389
|
+
def self._parse_data_def(element)
|
390
|
+
data_field = DataFieldDef.new
|
391
|
+
data_field.type = element.attributes["type"].intern
|
392
|
+
data_field.name = element.attributes["name"]
|
393
|
+
data_field.associated = element.attributes["associated"].intern unless element.attributes["associated"].nil?
|
394
|
+
data_field.ignore_null = true if element.attributes.include? "ignore_null"
|
395
|
+
return data_field
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
end # module Schema
|
400
|
+
end # module Simrpc
|