diff options
author | Leon Breedt <bitserf@gmail.com> | 2005-02-25 23:39:39 +0000 |
---|---|---|
committer | Leon Breedt <bitserf@gmail.com> | 2005-02-25 23:39:39 +0000 |
commit | 6f5a7b200443baf209d2f33c428ed4a4059782f7 (patch) | |
tree | 9c3942fe27be69c102873d9fdaa13f66dc12853d /actionwebservice/lib/action_web_service/vendor/ws | |
parent | 10faf204b712763f05a2b3155a4fd9c5338f1fb2 (diff) | |
download | rails-6f5a7b200443baf209d2f33c428ed4a4059782f7.tar.gz rails-6f5a7b200443baf209d2f33c428ed4a4059782f7.tar.bz2 rails-6f5a7b200443baf209d2f33c428ed4a4059782f7.zip |
merged the changes for the upcoming 0.6.0:
seperate out protocol marshaling into a small 'ws' library in vendor, so that
AWS itself only does integration with ActionPack, and so we can keep protocol
specific code in AWS proper to a minimum. refactor unit tests to get 95%
code coverage (for a baseline).
be far more relaxed about the types given to us by the remote side, don't do
any poor man's type checking, just try to cast and marshal to the correct types if
possible, and if not, return what they gave us anyway. this should make interoperating
with fuzzy XML-RPC clients easier.
if exception reporting is turned on, do best-effort error responses, so that
we can avoid "Internal protocol error" with no details if there is a bug in
AWS itself.
also perform extensive cleanups on AWS proper.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@800 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionwebservice/lib/action_web_service/vendor/ws')
10 files changed, 702 insertions, 0 deletions
diff --git a/actionwebservice/lib/action_web_service/vendor/ws/common.rb b/actionwebservice/lib/action_web_service/vendor/ws/common.rb new file mode 100644 index 0000000000..4266a7141d --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/common.rb @@ -0,0 +1,8 @@ +module WS + class WSError < StandardError + end + + def self.derived_from?(ancestor, child) + child.ancestors.include?(ancestor) + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb new file mode 100644 index 0000000000..790317639b --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding.rb @@ -0,0 +1,3 @@ +require 'ws/encoding/abstract' +require 'ws/encoding/soap_rpc_encoding' +require 'ws/encoding/xmlrpc_encoding' diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb new file mode 100644 index 0000000000..257c7d0993 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/abstract.rb @@ -0,0 +1,26 @@ +module WS + module Encoding + # Encoders operate on _foreign_ objects. That is, Ruby object + # instances that are the _marshaling format specific_ representation + # of objects. In other words, objects that have not yet been marshaled, but + # are in protocol-specific form (such as an AST or DOM element), and not + # native Ruby form. + class AbstractEncoding + def encode_rpc_call(method_name, params) + raise NotImplementedError + end + + def decode_rpc_call(obj) + raise NotImplementedError + end + + def encode_rpc_response(method_name, return_value) + raise NotImplementedError + end + + def decode_rpc_response(obj) + raise NotImplementedError + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb new file mode 100644 index 0000000000..f4d2f5a7d6 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/soap_rpc_encoding.rb @@ -0,0 +1,90 @@ +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/element' + +module WS + module Encoding + class SoapRpcError < WSError + end + + class SoapRpcEncoding < AbstractEncoding + attr_accessor :method_namespace + + def initialize(method_namespace='') + @method_namespace = method_namespace + end + + def encode_rpc_call(method_name, foreign_params) + qname = create_method_qname(method_name) + param_def = [] + params = foreign_params.map do |p| + param_def << ['in', p.param.info.name, p.param.info.data.mapping] + [p.param.info.name, p.soap_object] + end + request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def) + request.set_param(params) + envelope = create_soap_envelope(request) + SOAP::Processor.marshal(envelope) + end + + def decode_rpc_call(obj) + envelope = SOAP::Processor.unmarshal(obj) + unless envelope + raise(SoapRpcError, "Malformed SOAP request") + end + request = envelope.body.request + method_name = request.elename.name + params = request.collect do |key, value| + info = ParamInfo.new(key, nil, nil) + param = Param.new(nil, info) + Marshaling::SoapForeignObject.new(param, request[key]) + end + [method_name, params] + end + + def encode_rpc_response(method_name, return_value) + response = nil + qname = create_method_qname(method_name) + if return_value.nil? + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + else + param = return_value.param + soap_object = return_value.soap_object + param_def = [['retval', 'return', param.info.data.mapping]] + if soap_object.is_a?(SOAP::SOAPFault) + response = soap_object + else + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = soap_object + end + end + envelope = create_soap_envelope(response) + SOAP::Processor.marshal(envelope) + end + + def decode_rpc_response(obj) + envelope = SOAP::Processor.unmarshal(obj) + unless envelope + raise(SoapRpcError, "Malformed SOAP response") + end + method_name = envelope.body.request.elename.name + return_value = envelope.body.response + info = ParamInfo.new('return', nil, nil) + param = Param.new(nil, info) + return_value = Marshaling::SoapForeignObject.new(param, return_value) + [method_name, return_value] + end + + private + def create_soap_envelope(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + SOAP::SOAPEnvelope.new(header, body) + end + + def create_method_qname(method_name) + XSD::QName.new(@method_namespace, method_name) + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb new file mode 100644 index 0000000000..b38ae81abf --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb @@ -0,0 +1,53 @@ +require 'xmlrpc/marshal' + +module WS + module Encoding + class XmlRpcError < WSError + end + + class XmlRpcEncoding < AbstractEncoding + def encode_rpc_call(method_name, params) + XMLRPC::Marshal.dump_call(method_name, *params) + end + + def decode_rpc_call(obj) + method_name, params = XMLRPC::Marshal.load_call(obj) rescue nil + unless method_name && params + raise(XmlRpcError, "Malformed XML-RPC request") + end + i = 0 + params = params.map do |value| + param = XmlRpcDecodedParam.new("param#{i}", value) + i += 1 + param + end + [method_name, params] + end + + def encode_rpc_response(method_name, return_value) + if return_value.nil? + XMLRPC::Marshal.dump_response(true) + else + XMLRPC::Marshal.dump_response(return_value) + end + end + + def decode_rpc_response(obj) + return_value = XMLRPC::Marshal.load_response(obj) rescue nil + if return_value.nil? + raise(XmlRpcError, "Malformed XML-RPC response") + end + [nil, XmlRpcDecodedParam.new('return', return_value)] + end + end + + class XmlRpcDecodedParam + attr :param + + def initialize(name, value) + info = ParamInfo.new(name, value.class) + @param = Param.new(value, info) + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb new file mode 100644 index 0000000000..3a0a2e8cc1 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling.rb @@ -0,0 +1,3 @@ +require 'ws/marshaling/abstract' +require 'ws/marshaling/soap_marshaling' +require 'ws/marshaling/xmlrpc_marshaling' diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb new file mode 100644 index 0000000000..53120e1447 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/abstract.rb @@ -0,0 +1,17 @@ +module WS + module Marshaling + class AbstractMarshaler + def marshal(param) + raise NotImplementedError + end + + def unmarshal(param) + raise NotImplementedError + end + + def register_type(type) + nil + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb new file mode 100644 index 0000000000..99e2a7ff28 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/soap_marshaling.rb @@ -0,0 +1,224 @@ +require 'soap/mapping' +require 'xsd/ns' + +module WS + module Marshaling + SoapEncodingNS = 'http://schemas.xmlsoap.org/soap/encoding/' + + class SoapError < WSError + end + + class SoapMarshaler < AbstractMarshaler + attr :registry + attr_accessor :type_namespace + + def initialize(type_namespace='') + @type_namespace = type_namespace + @registry = SOAP::Mapping::Registry.new + @spec2binding = {} + end + + def marshal(param) + if param.info.type.is_a?(Array) + (class << param.value; self; end).class_eval do + define_method(:arytype) do + param.info.data.qname + end + end + end + if param.value.is_a?(Exception) + detail = SOAP::Mapping::SOAPException.new(param.value) + soap_obj = SOAP::SOAPFault.new( + SOAP::SOAPString.new('Server'), + SOAP::SOAPString.new(param.value.to_s), + SOAP::SOAPString.new(self.class.name), + SOAP::Mapping.obj2soap(detail)) + else + soap_obj = SOAP::Mapping.obj2soap(param.value, @registry) + end + SoapForeignObject.new(param, soap_obj) + end + + def unmarshal(obj) + param = obj.param + soap_object = obj.soap_object + soap_type = soap_object ? soap_object.type : nil + value = soap_object ? SOAP::Mapping.soap2obj(soap_object, @registry) : nil + param.value = value + param.info.type = value.class + mapping = @registry.find_mapped_soap_class(param.info.type) rescue nil + if soap_type && soap_type.name == 'Array' && soap_type.namespace == SoapEncodingNS + param.info.data = SoapBinding.new(soap_object.arytype, mapping) + else + param.info.data = SoapBinding.new(soap_type, mapping) + end + param + end + + def register_type(spec) + if @spec2binding.has_key?(spec) + return @spec2binding[spec] + end + + klass = BaseTypes.canonical_param_type_class(spec) + if klass.is_a?(Array) + type_class = klass[0] + else + type_class = klass + end + + type_binding = nil + if (mapping = @registry.find_mapped_soap_class(type_class) rescue nil) + qname = mapping[2] ? mapping[2][:type] : nil + qname ||= soap_base_type_name(mapping[0]) + type_binding = SoapBinding.new(qname, mapping) + else + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name)) + @registry.add(type_class, + SOAP::SOAPStruct, + typed_struct_factory(type_class), + { :type => qname }) + mapping = @registry.find_mapped_soap_class(type_class) + type_binding = SoapBinding.new(qname, mapping) + end + + array_binding = nil + if klass.is_a?(Array) + array_mapping = @registry.find_mapped_soap_class(Array) rescue nil + if (array_mapping && !array_mapping[1].is_a?(SoapTypedArrayFactory)) || array_mapping.nil? + @registry.set(Array, + SOAP::SOAPArray, + SoapTypedArrayFactory.new) + array_mapping = @registry.find_mapped_soap_class(Array) + end + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name) + 'Array') + array_binding = SoapBinding.new(qname, array_mapping, type_binding) + end + + @spec2binding[spec] = array_binding ? array_binding : type_binding + end + + protected + def typed_struct_factory(type_class) + if Object.const_defined?('ActiveRecord') + if WS.derived_from?(ActiveRecord::Base, type_class) + qname = XSD::QName.new(@type_namespace, soap_type_name(type_class.name)) + type_class.instance_variable_set('@qname', qname) + return SoapActiveRecordStructFactory.new + end + end + SOAP::Mapping::Registry::TypedStructFactory + end + + def soap_type_name(type_name) + type_name.gsub(/::/, '..') + end + + def soap_base_type_name(type) + xsd_type = type.ancestors.find{|c| c.const_defined? 'Type'} + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + end + + class SoapForeignObject + attr_accessor :param + attr_accessor :soap_object + + def initialize(param, soap_object) + @param = param + @soap_object = soap_object + end + end + + class SoapBinding + attr :qname + attr :mapping + attr :element_binding + + def initialize(qname, mapping, element_binding=nil) + @qname = qname + @mapping = mapping + @element_binding = element_binding + end + + def is_custom_type? + is_typed_array? || is_typed_struct? + end + + def is_typed_array? + @mapping[1].is_a?(WS::Marshaling::SoapTypedArrayFactory) + end + + def is_typed_struct? + @mapping[1] == SOAP::Mapping::Registry::TypedStructFactory || \ + @mapping[1].is_a?(WS::Marshaling::SoapActiveRecordStructFactory) + end + + def each_member(&block) + unless is_typed_struct? + raise(SoapError, "not a structured type") + end + end + + def type_name + is_custom_type? ? @qname.name : nil + end + + def qualified_type_name(ns=nil) + if is_custom_type? + "#{ns ? ns : @qname.namespace}:#{@qname.name}" + else + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + end + + class SoapActiveRecordStructFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActiveRecord::Base) + return nil + end + soap_obj = soap_class.new(obj.class.instance_variable_get('@qname')) + obj.attributes.each do |key, value| + soap_obj[key] = SOAP::Mapping._obj2soap(value, map) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + unless node.type == obj_class.instance_variable_get('@qname') + return false + end + obj = obj_class.new + node.each do |key, value| + obj[key] = value.data + end + obj.instance_variable_set('@new_record', false) + return true, obj + end + end + + class SoapTypedArrayFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.respond_to?(:arytype) + return nil + end + soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype) + mark_marshalled_obj(obj, soap_obj) + obj.each do |item| + child = SOAP::Mapping._obj2soap(item, map) + soap_obj.add(child) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + return false + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb new file mode 100644 index 0000000000..87154f87e1 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/marshaling/xmlrpc_marshaling.rb @@ -0,0 +1,116 @@ +module WS + module Marshaling + class XmlRpcError < WSError + end + + class XmlRpcMarshaler < AbstractMarshaler + def initialize + @caster = BaseTypeCaster.new + @spec2binding = {} + end + + def marshal(param) + transform_outbound(param) + end + + def unmarshal(obj) + obj.param.value = transform_inbound(obj.param) + obj.param + end + + def typed_unmarshal(obj, spec) + param = obj.param + param.info.data = register_type(spec) + param.value = transform_inbound(param) + param + end + + def register_type(spec) + if @spec2binding.has_key?(spec) + return @spec2binding[spec] + end + + klass = BaseTypes.canonical_param_type_class(spec) + type_binding = nil + if klass.is_a?(Array) + type_binding = XmlRpcArrayBinding.new(klass[0]) + else + type_binding = XmlRpcBinding.new(klass) + end + + @spec2binding[spec] = type_binding + end + + def transform_outbound(param) + binding = param.info.data + case binding + when XmlRpcArrayBinding + param.value.map{|x| cast_outbound(x, binding.element_klass)} + when XmlRpcBinding + cast_outbound(param.value, param.info.type) + end + end + + def transform_inbound(param) + return param.value if param.info.data.nil? + binding = param.info.data + param.info.type = binding.klass + case binding + when XmlRpcArrayBinding + param.value.map{|x| cast_inbound(x, binding.element_klass)} + when XmlRpcBinding + cast_inbound(param.value, param.info.type) + end + end + + def cast_outbound(value, klass) + if BaseTypes.base_type?(klass) + @caster.cast(value, klass) + elsif value.is_a?(Exception) + XMLRPC::FaultException.new(2, value.message) + elsif Object.const_defined?('ActiveRecord') && value.is_a?(ActiveRecord::Base) + value.attributes + else + struct = {} + value.instance_variables.each do |name| + key = name.sub(/^@/, '') + struct[key] = value.instance_variable_get(name) + end + struct + end + end + + def cast_inbound(value, klass) + if BaseTypes.base_type?(klass) + value = value.to_time if value.is_a?(XMLRPC::DateTime) + @caster.cast(value, klass) + elsif value.is_a?(XMLRPC::FaultException) + value + else + obj = klass.new + value.each do |name, val| + obj.send('%s=' % name.to_s, val) + end + obj + end + end + end + + class XmlRpcBinding + attr :klass + + def initialize(klass) + @klass = klass + end + end + + class XmlRpcArrayBinding < XmlRpcBinding + attr :element_klass + + def initialize(element_klass) + super(Array) + @element_klass = element_klass + end + end + end +end diff --git a/actionwebservice/lib/action_web_service/vendor/ws/types.rb b/actionwebservice/lib/action_web_service/vendor/ws/types.rb new file mode 100644 index 0000000000..24b96dc327 --- /dev/null +++ b/actionwebservice/lib/action_web_service/vendor/ws/types.rb @@ -0,0 +1,162 @@ +require 'time' +require 'date' + +module WS + module BaseTypes + class << self + def type_name_to_class(name) + case canonical_type_name(name) + when :int + Integer + when :string + String + when :bool + TrueClass + when :float + Float + when :time + Time + when :date + Date + end + end + + def class_to_type_name(klass) + if WS.derived_from?(Integer, klass) || WS.derived_from?(Fixnum, klass) || WS.derived_from?(Bignum, klass) + :int + elsif klass == String + :string + elsif klass == TrueClass || klass == FalseClass + :bool + elsif WS.derived_from?(Float, klass) || WS.derived_from?(Precision, klass) || WS.derived_from?(Numeric, klass) + :float + elsif klass == Time || klass == DateTime + :time + elsif klass == Date + :date + else + raise(TypeError, "#{klass} is not a valid base type") + end + end + + def base_type?(klass) + !(canonical_type_class(klass) rescue nil).nil? + end + + def canonical_type_class(klass) + type_name_to_class(class_to_type_name(klass)) + end + + def canonical_param_type_class(spec) + klass = spec.is_a?(Hash) ? spec.values[0] : spec + array_element_class = klass.is_a?(Array) ? klass[0] : nil + klass = array_element_class ? array_element_class : klass + klass = type_name_to_class(klass) if klass.is_a?(Symbol) || klass.is_a?(String) + base_class = canonical_type_class(klass) rescue nil + klass = base_class unless base_class.nil? + array_element_class ? [klass] : klass + end + + def canonical_param_type_spec(spec) + klass = canonical_param_type_class(spec) + spec.is_a?(Hash) ? {spec.keys[0]=>klass} : klass + end + + def canonical_type_name(name) + name = name.to_sym + case name + when :int, :integer, :fixnum, :bignum + :int + when :string, :base64 + :string + when :bool, :boolean + :bool + when :float, :double + :float + when :time, :datetime, :timestamp + :time + when :date + :date + else + raise(TypeError, "#{name} is not a valid base type") + end + end + end + end + + class Param + attr_accessor :value + attr_accessor :info + + def initialize(value, info) + @value = value + @info = info + end + end + + class ParamInfo + attr_accessor :name + attr_accessor :type + attr_accessor :data + + def initialize(name, type, data=nil) + @name = name + @type = type + @data = data + end + + def self.create(spec, index=nil, data=nil) + name = spec.is_a?(Hash) ? spec.keys[0].to_s : (index ? "param#{index}" : nil) + type = BaseTypes.canonical_param_type_class(spec) + ParamInfo.new(name, type, data) + end + end + + class BaseTypeCaster + def initialize + @handlers = {} + install_handlers + end + + def cast(value, klass) + type_class = BaseTypes.canonical_type_class(klass) + return value unless type_class + @handlers[type_class].call(value, type_class) + end + + protected + def install_handlers + handler = method(:cast_base_type) + [:int, :string, :bool, :float, :time, :date].each do |name| + type = BaseTypes.type_name_to_class(name) + @handlers[type] = handler + end + @handlers[Fixnum] = handler + end + + def cast_base_type(value, type_class) + desired_class = BaseTypes.canonical_type_class(type_class) + value_class = BaseTypes.canonical_type_class(value.class) + return value if desired_class == value_class + desired_name = BaseTypes.class_to_type_name(desired_class) + case desired_name + when :int + Integer(value) + when :string + value.to_s + when :bool + return false if value.nil? + value = value.to_s + return true if value == 'true' + return false if value == 'false' + raise(TypeError, "can't convert #{value} to boolean") + when :float + Float(value) + when :time + Time.parse(value.to_s) + when :date + Date.parse(value.to_s) + end + end + end +end |