From e7a29380292902eae4799b2658507b3cfffb9cec Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Feb 2005 10:35:25 +0000 Subject: Added Action Service to the repository git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@658 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../lib/action_service/protocol/abstract.rb | 128 ++++++ .../lib/action_service/protocol/registry.rb | 55 +++ actionservice/lib/action_service/protocol/soap.rb | 484 +++++++++++++++++++++ .../lib/action_service/protocol/xmlrpc.rb | 187 ++++++++ 4 files changed, 854 insertions(+) create mode 100644 actionservice/lib/action_service/protocol/abstract.rb create mode 100644 actionservice/lib/action_service/protocol/registry.rb create mode 100644 actionservice/lib/action_service/protocol/soap.rb create mode 100644 actionservice/lib/action_service/protocol/xmlrpc.rb (limited to 'actionservice/lib/action_service/protocol') diff --git a/actionservice/lib/action_service/protocol/abstract.rb b/actionservice/lib/action_service/protocol/abstract.rb new file mode 100644 index 0000000000..ed41c49951 --- /dev/null +++ b/actionservice/lib/action_service/protocol/abstract.rb @@ -0,0 +1,128 @@ +module ActionService # :nodoc: + module Protocol # :nodoc: + CheckedMessage = :checked + UncheckedMessage = :unchecked + + class ProtocolError < ActionService::ActionServiceError # :nodoc: + end + + class AbstractProtocol # :nodoc: + attr :container_class + + def initialize(container_class) + @container_class = container_class + end + + def unmarshal_request(protocol_request) + raise NotImplementedError + end + + def marshal_response(protocol_request, return_value) + raise NotImplementedError + end + + def marshal_exception(exception) + raise NotImplementedError + end + + def self.create_protocol_request(container_class, action_pack_request) + nil + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + nil + end + end + + class AbstractProtocolMessage # :nodoc: + attr_accessor :signature + attr_accessor :return_signature + attr_accessor :type + attr :options + + def initialize(options={}) + @signature = @return_signature = nil + @options = options + @type = @options[:type] || CheckedMessage + end + + def signature=(value) + return if value.nil? + @signature = [] + value.each do |klass| + if klass.is_a?(Hash) + @signature << klass.values.shift + else + @signature << klass + end + end + @signature + end + + def checked? + @type == CheckedMessage + end + + def check_parameter_types(values, signature) + return unless checked? && signature + unless signature.length == values.length + raise(ProtocolError, "Signature and parameter lengths mismatch") + end + (1..signature.length).each do |i| + check_compatibility(signature[i-1], values[i-1].class) + end + end + + def check_compatibility(expected_class, received_class) + return if \ + (expected_class == TrueClass or expected_class == FalseClass) and \ + (received_class == TrueClass or received_class == FalseClass) + unless received_class.ancestors.include?(expected_class) or \ + expected_class.ancestors.include?(received_class) + raise(ProtocolError, "value of type #{received_class.name} is not " + + "compatible with expected type #{expected_class.name}") + end + end + end + + class ProtocolRequest < AbstractProtocolMessage # :nodoc: + attr :protocol + attr :raw_body + + attr_accessor :service_name + attr_accessor :public_method_name + attr_accessor :content_type + + def initialize(protocol, raw_body, service_name, public_method_name, content_type, options={}) + super(options) + @protocol = protocol + @raw_body = raw_body + @service_name = service_name + @public_method_name = public_method_name + @content_type = content_type + end + + def unmarshal + @protocol.unmarshal_request(self) + end + + def marshal(return_value) + @protocol.marshal_response(self, return_value) + end + end + + class ProtocolResponse < AbstractProtocolMessage # :nodoc: + attr :protocol + attr :raw_body + + attr_accessor :content_type + + def initialize(protocol, raw_body, content_type, options={}) + super(options) + @protocol = protocol + @raw_body = raw_body + @content_type = content_type + end + end + end +end diff --git a/actionservice/lib/action_service/protocol/registry.rb b/actionservice/lib/action_service/protocol/registry.rb new file mode 100644 index 0000000000..e06361f916 --- /dev/null +++ b/actionservice/lib/action_service/protocol/registry.rb @@ -0,0 +1,55 @@ +module ActionService # :nodoc: + module Protocol # :nodoc: + HeaderAndBody = :header_and_body + BodyOnly = :body_only + + module Registry # :nodoc: + def self.append_features(base) # :nodoc: + super + base.extend(ClassMethods) + base.send(:include, ActionService::Protocol::Registry::InstanceMethods) + end + + module ClassMethods # :nodoc: + def register_protocol(type, klass) # :nodoc: + case type + when HeaderAndBody + write_inheritable_array("header_and_body_protocols", [klass]) + when BodyOnly + write_inheritable_array("body_only_protocols", [klass]) + else + raise(ProtocolError, "unknown protocol type #{type}") + end + end + end + + module InstanceMethods # :nodoc: + private + def probe_request_protocol(action_pack_request) + (header_and_body_protocols + body_only_protocols).each do |protocol| + protocol_request = protocol.create_protocol_request(self.class, action_pack_request) + return protocol_request if protocol_request + end + raise(ProtocolError, "unsupported request message format") + end + + def probe_protocol_client(api, protocol_name, endpoint_uri, options) + (header_and_body_protocols + body_only_protocols).each do |protocol| + protocol_client = protocol.create_protocol_client(api, protocol_name, endpoint_uri, options) + return protocol_client if protocol_client + end + raise(ProtocolError, "unsupported client protocol :#{protocol_name}") + end + + def header_and_body_protocols + self.class.read_inheritable_attribute("header_and_body_protocols") || [] + end + + def body_only_protocols + self.class.read_inheritable_attribute("body_only_protocols") || [] + end + end + + end + end +end diff --git a/actionservice/lib/action_service/protocol/soap.rb b/actionservice/lib/action_service/protocol/soap.rb new file mode 100644 index 0000000000..24cc554b05 --- /dev/null +++ b/actionservice/lib/action_service/protocol/soap.rb @@ -0,0 +1,484 @@ +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/element' +require 'xsd/datatypes' +require 'xsd/ns' +require 'singleton' + +module ActionService # :nodoc: + module Protocol # :nodoc: + module Soap # :nodoc: + class ProtocolError < ActionService::ActionServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.register_protocol(HeaderAndBody, SoapProtocol) + base.extend(ClassMethods) + base.wsdl_service_name('ActionService') + end + + module ClassMethods + # Specifies the WSDL service name to use when generating WSDL. Highly + # recommended that you set this value, or code generators may generate + # classes with very generic names. + # + # === Example + # class MyController < ActionController::Base + # wsdl_service_name 'MyService' + # end + def wsdl_service_name(name) + write_inheritable_attribute("soap_mapper", SoapMapper.new("urn:#{name}")) + end + + def soap_mapper # :nodoc: + read_inheritable_attribute("soap_mapper") + end + end + + class SoapProtocol < AbstractProtocol # :nodoc: + attr :mapper + + def initialize(mapper) + @mapper = mapper + end + + def self.create_protocol_request(container_class, action_pack_request) + soap_action = extract_soap_action(action_pack_request) + return nil unless soap_action + service_name = action_pack_request.parameters['action'] + public_method_name = soap_action.gsub(/^[\/]+/, '').split(/[\/]+/)[-1] + content_type = action_pack_request.env['HTTP_CONTENT_TYPE'] + content_type ||= 'text/xml' + protocol = SoapProtocol.new(container_class.soap_mapper) + ProtocolRequest.new(protocol, + action_pack_request.raw_post, + service_name.to_sym, + public_method_name, + content_type) + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name.to_s.downcase.to_sym == :soap + ActionService::Client::Soap.new(api, endpoint_uri, options) + end + + def unmarshal_request(protocol_request) + unmarshal = lambda do + envelope = SOAP::Processor.unmarshal(protocol_request.raw_body) + request = envelope.body.request + values = request.collect{|k, v| request[k]} + soap_to_ruby_array(values) + end + signature = protocol_request.signature + if signature + map_signature_types(signature) + values = unmarshal.call + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types(values, signature) + values + else + if protocol_request.checked? + [] + else + unmarshal.call + end + end + end + + def marshal_response(protocol_request, return_value) + marshal = lambda do |signature| + mapping = mapper.lookup(signature[0]) + return_value = fixup_array_types(mapping, return_value) + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types([return_value], signature) + param_def = [['retval', 'return', mapping.registry_mapping]] + [param_def, ruby_to_soap(return_value)] + end + signature = protocol_request.return_signature + param_def = nil + if signature + param_def, return_value = marshal.call(signature) + else + if protocol_request.checked? + param_def, return_value = nil, nil + else + param_def, return_value = marshal.call([return_value.class]) + end + end + qname = XSD::QName.new(mapper.custom_namespace, + protocol_request.public_method_name) + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = return_value unless return_value.nil? + ProtocolResponse.new(self, create_response(response), 'text/xml') + end + + def marshal_exception(exc) + ProtocolResponse.new(self, create_exception_response(exc), 'text/xml') + end + + private + def self.extract_soap_action(request) + return nil unless request.method == :post + content_type = request.env['HTTP_CONTENT_TYPE'] || 'text/xml' + return nil unless content_type + soap_action = request.env['HTTP_SOAPACTION'] + return nil unless soap_action + soap_action.gsub!(/^"/, '') + soap_action.gsub!(/"$/, '') + soap_action.strip! + return nil if soap_action.empty? + soap_action + end + + def fixup_array_types(mapping, obj) + mapping.each_attribute do |name, type, attr_mapping| + if attr_mapping.custom_type? + attr_obj = obj.send(name) + new_obj = fixup_array_types(attr_mapping, attr_obj) + obj.send("#{name}=", new_obj) unless new_obj.equal?(attr_obj) + end + end + if mapping.is_a?(SoapArrayMapping) + obj = mapping.ruby_klass.new(obj) + # man, this is going to be slow for big arrays :( + (1..obj.size).each do |i| + i -= 1 + obj[i] = fixup_array_types(mapping.element_mapping, obj[i]) + end + else + if !mapping.generated_klass.nil? && mapping.generated_klass.respond_to?(:members) + # have to map the publically visible structure of the class + new_obj = mapping.generated_klass.new + mapping.generated_klass.members.each do |name, klass| + new_obj.send("#{name}=", obj.send(name)) + end + obj = new_obj + end + end + obj + end + + def map_signature_types(types) + types.collect{|type| mapper.map(type)} + end + + def create_response(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + envelope = SOAP::SOAPEnvelope.new(header, body) + SOAP::Processor.marshal(envelope) + end + + def create_exception_response(exc) + detail = SOAP::Mapping::SOAPException.new(exc) + body = SOAP::SOAPFault.new( + SOAP::SOAPString.new('Server'), + SOAP::SOAPString.new(exc.to_s), + SOAP::SOAPString.new(self.class.name), + SOAP::Mapping.obj2soap(detail)) + create_response(body) + end + + def ruby_to_soap(obj) + SOAP::Mapping.obj2soap(obj, mapper.registry) + end + + def soap_to_ruby(obj) + SOAP::Mapping.soap2obj(obj, mapper.registry) + end + + def soap_to_ruby_array(array) + array.map{|x| soap_to_ruby(x)} + end + end + + class SoapMapper # :nodoc: + attr :registry + attr :custom_namespace + attr :custom_types + + def initialize(custom_namespace) + @custom_namespace = custom_namespace + @registry = SOAP::Mapping::Registry.new + @klass2map = {} + @custom_types = {} + @ar2klass = {} + end + + def lookup(klass) + lookup_klass = klass.is_a?(Array) ? klass[0] : klass + generated_klass = nil + unless lookup_klass.respond_to?(:ancestors) + raise(ProtocolError, "expected parameter type definition to be a Class") + end + if lookup_klass.ancestors.include?(ActiveRecord::Base) + generated_klass = @ar2klass.has_key?(klass) ? @ar2klass[klass] : nil + klass = generated_klass if generated_klass + end + return @klass2map[klass] if @klass2map.has_key?(klass) + + custom_type = false + + ruby_klass = select_class(lookup_klass) + generated_klass = @ar2klass[lookup_klass] if @ar2klass.has_key?(lookup_klass) + type_name = ruby_klass.name + + # Array signatures generate a double-mapping and require generation + # of an Array subclass to represent the mapping in the SOAP + # registry + array_klass = nil + if klass.is_a?(Array) + array_klass = Class.new(Array) do + module_eval <<-END + def self.name + "#{type_name}Array" + end + END + end + end + + mapping = @registry.find_mapped_soap_class(ruby_klass) rescue nil + unless mapping + # Custom structured type, generate a mapping + info = { :type => XSD::QName.new(@custom_namespace, type_name) } + @registry.add(ruby_klass, + SOAP::SOAPStruct, + SOAP::Mapping::Registry::TypedStructFactory, + info) + mapping = ensure_mapped(ruby_klass) + custom_type = true + end + + array_mapping = nil + if array_klass + # Typed array always requires a custom type. The info of the array + # is the info of its element type (in mapping[2]), falling back + # to SOAP base types. + info = mapping[2] + info ||= {} + info[:type] ||= soap_base_type_qname(mapping[0]) + @registry.add(array_klass, + SOAP::SOAPArray, + SOAP::Mapping::Registry::TypedArrayFactory, + info) + array_mapping = ensure_mapped(array_klass) + end + + if array_mapping + @klass2map[ruby_klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @klass2map[klass] = SoapArrayMapping.new(self, + type_name, + array_klass, + array_mapping[0], + array_mapping, + @klass2map[ruby_klass]) + @custom_types[klass] = @klass2map[klass] + @custom_types[ruby_klass] = @klass2map[ruby_klass] if custom_type + else + @klass2map[klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @custom_types[klass] = @klass2map[klass] if custom_type + end + + @klass2map[klass] + end + alias :map :lookup + + def map_container_services(container, &block) + dispatching_mode = container.service_dispatching_mode + services = nil + case dispatching_mode + when :direct + api = container.class.service_api + if container.respond_to?(:controller_class_name) + service_name = container.controller_class_name.sub(/Controller$/, '').underscore + else + service_name = container.class.name.demodulize.underscore + end + services = { service_name => api } + when :delegated + services = {} + container.class.services.each do |service_name, service_info| + begin + object = container.service_object(service_name) + rescue Exception => e + raise(ProtocolError, "failed to retrieve service object for mapping: #{e.message}") + end + services[service_name] = object.class.service_api + end + end + services.each do |service_name, api| + if api.nil? + raise(ProtocolError, "no service API set while in :#{dispatching_mode} mode") + end + map_api(api) do |api_methods| + yield service_name, api, api_methods if block_given? + end + end + end + + def map_api(api, &block) + lookup_proc = lambda do |klass| + mapping = lookup(klass) + custom_mapping = nil + if mapping.respond_to?(:element_mapping) + custom_mapping = mapping.element_mapping + else + custom_mapping = mapping + end + if custom_mapping && custom_mapping.custom_type? + # What gives? This is required so that structure types + # referenced only by structures (and not signatures) still + # have a custom type mapping in the registry (needed for WSDL + # generation). + custom_mapping.each_attribute{} + end + mapping + end + api_methods = block.nil?? nil : {} + api.api_methods.each do |method_name, method_info| + expects = method_info[:expects] + expects_signature = nil + if expects + expects_signature = block ? [] : nil + expects.each do |klass| + lookup_klass = nil + if klass.is_a?(Hash) + lookup_klass = lookup_proc.call(klass.values[0]) + expects_signature << {klass.keys[0]=>lookup_klass} if block + else + lookup_klass = lookup_proc.call(klass) + expects_signature << lookup_klass if block + end + end + end + returns = method_info[:returns] + returns_signature = returns ? returns.map{|klass| lookup_proc.call(klass)} : nil + if block + api_methods[method_name] = { + :expects => expects_signature, + :returns => returns_signature + } + end + end + yield api_methods if block + end + + private + def select_class(klass) + return Integer if klass == Fixnum + if klass.ancestors.include?(ActiveRecord::Base) + new_klass = Class.new(ActionService::Struct) + new_klass.class_eval <<-EOS + def self.name + "#{klass.name}" + end + EOS + klass.columns.each do |column| + next if column.klass.nil? + new_klass.send(:member, column.name.to_sym, column.klass) + end + @ar2klass[klass] = new_klass + return new_klass + end + klass + end + + def ensure_mapped(klass) + mapping = @registry.find_mapped_soap_class(klass) rescue nil + raise(ProtocolError, "failed to register #{klass.name}") unless mapping + mapping + end + + def soap_base_type_qname(base_type) + xsd_type = base_type.ancestors.find{|c| c.const_defined? 'Type'} + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + end + + class SoapMapping # :nodoc: + attr :ruby_klass + attr :generated_klass + attr :soap_klass + attr :registry_mapping + + def initialize(mapper, type_name, ruby_klass, generated_klass, soap_klass, registry_mapping, + custom_type=false) + @mapper = mapper + @type_name = type_name + @ruby_klass = ruby_klass + @generated_klass = generated_klass + @soap_klass = soap_klass + @registry_mapping = registry_mapping + @custom_type = custom_type + end + + def type_name + @type_name + end + + def custom_type? + @custom_type + end + + def qualified_type_name + name = type_name + if custom_type? + "typens:#{name}" + else + xsd_type_for(@soap_klass) + end + end + + def each_attribute(&block) + if @ruby_klass.respond_to?(:members) + @ruby_klass.members.each do |name, klass| + name = name.to_s + mapping = @mapper.lookup(klass) + yield name, mapping.qualified_type_name, mapping + end + end + end + + def is_xsd_type?(klass) + klass.ancestors.include?(XSD::NSDBase) + end + + def xsd_type_for(klass) + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + xsd_klass = klass.ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + + class SoapArrayMapping < SoapMapping # :nodoc: + attr :element_mapping + + def initialize(mapper, type_name, ruby_klass, soap_klass, registry_mapping, element_mapping) + super(mapper, type_name, ruby_klass, nil, soap_klass, registry_mapping, true) + @element_mapping = element_mapping + end + + def type_name + super + "Array" + end + + def each_attribute(&block); end + end + end + end +end diff --git a/actionservice/lib/action_service/protocol/xmlrpc.rb b/actionservice/lib/action_service/protocol/xmlrpc.rb new file mode 100644 index 0000000000..7d29868b59 --- /dev/null +++ b/actionservice/lib/action_service/protocol/xmlrpc.rb @@ -0,0 +1,187 @@ +require 'xmlrpc/parser' +require 'xmlrpc/create' +require 'xmlrpc/config' +require 'xmlrpc/utils' +require 'singleton' + +module XMLRPC # :nodoc: + class XmlRpcHelper # :nodoc: + include Singleton + include ParserWriterChooseMixin + + def parse_method_call(message) + parser().parseMethodCall(message) + end + + def create_method_response(successful, return_value) + create().methodResponse(successful, return_value) + end + end +end + +module ActionService # :nodoc: + module Protocol # :nodoc: + module XmlRpc # :nodoc: + def self.append_features(base) # :nodoc: + super + base.register_protocol(BodyOnly, XmlRpcProtocol) + end + + class XmlRpcProtocol < AbstractProtocol # :nodoc: + + public + + def self.create_protocol_request(container_class, action_pack_request) + helper = XMLRPC::XmlRpcHelper.instance + service_name = action_pack_request.parameters['action'] + methodname, params = helper.parse_method_call(action_pack_request.raw_post) + methodname.gsub!(/^[^\.]+\./, '') unless methodname =~ /^system\./ # XXX + protocol = XmlRpcProtocol.new(container_class) + content_type = action_pack_request.env['HTTP_CONTENT_TYPE'] + content_type ||= 'text/xml' + request = ProtocolRequest.new(protocol, + action_pack_request.raw_post, + service_name.to_sym, + methodname, + content_type, + :xmlrpc_values => params) + request + rescue + nil + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name.to_s.downcase.to_sym == :xmlrpc + ActionService::Client::XmlRpc.new(api, endpoint_uri, options) + end + + def initialize(container_class) + super(container_class) + container_class.write_inheritable_hash('default_system_methods', XmlRpcProtocol => method(:xmlrpc_default_system_handler)) + end + + def unmarshal_request(protocol_request) + values = protocol_request.options[:xmlrpc_values] + signature = protocol_request.signature + if signature + values = self.class.transform_incoming_method_params(self.class.transform_array_types(signature), values) + protocol_request.check_parameter_types(values, check_array_types(signature)) + values + else + protocol_request.checked? ? [] : values + end + end + + def marshal_response(protocol_request, return_value) + helper = XMLRPC::XmlRpcHelper.instance + signature = protocol_request.return_signature + if signature + protocol_request.check_parameter_types([return_value], check_array_types(signature)) + return_value = self.class.transform_return_value(self.class.transform_array_types(signature), return_value) + raw_response = helper.create_method_response(true, return_value) + else + # XML-RPC doesn't have the concept of a void method, nor does it + # support a nil return value, so return true if we would have returned + # nil + if protocol_request.checked? + raw_response = helper.create_method_response(true, true) + else + return_value = true if return_value.nil? + raw_response = helper.create_method_response(true, return_value) + end + end + ProtocolResponse.new(self, raw_response, 'text/xml') + end + + def marshal_exception(exception) + helper = XMLRPC::XmlRpcHelper.instance + exception = XMLRPC::FaultException.new(1, exception.message) + raw_response = helper.create_method_response(false, exception) + ProtocolResponse.new(self, raw_response, 'text/xml') + end + + class << self + def transform_incoming_method_params(signature, params) + (1..signature.size).each do |i| + i -= 1 + params[i] = xmlrpc_to_ruby(params[i], signature[i]) + end + params + end + + def transform_return_value(signature, return_value) + ruby_to_xmlrpc(return_value, signature[0]) + end + + def ruby_to_xmlrpc(param, param_class) + if param_class.is_a?(XmlRpcArray) + param.map{|p| ruby_to_xmlrpc(p, param_class.klass)} + elsif param_class.ancestors.include?(ActiveRecord::Base) + param.instance_variable_get('@attributes') + elsif param_class.ancestors.include?(ActionService::Struct) + struct = {} + param_class.members.each do |name, klass| + value = param.send(name) + next if value.nil? + struct[name.to_s] = value + end + struct + else + param + end + end + + def xmlrpc_to_ruby(param, param_class) + if param_class.is_a?(XmlRpcArray) + param.map{|p| xmlrpc_to_ruby(p, param_class.klass)} + elsif param_class.ancestors.include?(ActiveRecord::Base) + raise(ProtocolError, "incoming ActiveRecord::Base types are not allowed") + elsif param_class.ancestors.include?(ActionService::Struct) + unless param.is_a?(Hash) + raise(ProtocolError, "expected parameter to be a Hash") + end + new_param = param_class.new + param_class.members.each do |name, klass| + new_param.send('%s=' % name.to_s, param[name.to_s]) + end + new_param + else + param + end + end + + def transform_array_types(signature) + signature.map{|x| x.is_a?(Array) ? XmlRpcArray.new(x[0]) : x} + end + end + + private + def xmlrpc_default_system_handler(name, service_class, *args) + case name + when 'system.listMethods' + methods = [] + api = service_class.service_api + api.api_methods.each do |name, info| + methods << api.public_api_method_name(name) + end + methods.sort + else + throw :try_default + end + end + + def check_array_types(signature) + signature.map{|x| x.is_a?(Array) ? Array : x} + end + + class XmlRpcArray + attr :klass + def initialize(klass) + @klass = klass + end + end + end + + end + end +end -- cgit v1.2.3