1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

generalize casting code to be used by both SOAP and XML-RPC (previously only XML-RPC). switch

to better model for API methods, and improve the ability to generate protocol requests/response,
will be required by upcoming scaffolding.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1030 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Leon Breedt 2005-03-28 03:20:13 +00:00
parent 439a216dcb
commit 594063f23c
19 changed files with 433 additions and 250 deletions

View file

@ -1,3 +1,8 @@
*0.7.0* (Unreleased)
* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC)
*0.6.2* (27th March, 2005)
* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939.

View file

@ -1,5 +1,8 @@
module ActionWebService # :nodoc:
module API # :nodoc:
class CastingError < ActionWebService::ActionWebServiceError
end
# A web service API class specifies the methods that will be available for
# invocation for an API. It also contains metadata such as the method type
# signature hints.
@ -77,8 +80,8 @@ module ActionWebService # :nodoc:
end
name = name.to_sym
public_name = public_api_method_name(name)
info = { :expects => expects, :returns => returns }
write_inheritable_hash("api_methods", name => info)
method = Method.new(name, public_name, expects, returns)
write_inheritable_hash("api_methods", name => method)
write_inheritable_hash("api_public_method_names", public_name => name)
end
@ -112,7 +115,39 @@ module ActionWebService # :nodoc:
def api_methods
read_inheritable_attribute("api_methods") || {}
end
# The Method instance for the given public API method name, if any
def public_api_method_instance(public_method_name)
api_method_instance(api_method_name(public_method_name))
end
# The Method instance for the given API method name, if any
def api_method_instance(method_name)
api_methods[method_name]
end
# The Method instance for the default API method, if any
def default_api_method_instance
return nil unless name = default_api_method
instance = read_inheritable_attribute("default_api_method_instance")
if instance && instance.name == name
return instance
end
instance = Method.new(name, public_api_method_name(name), nil, nil)
write_inheritable_attribute("default_api_method_instance", instance)
instance
end
# Creates a dummy API Method instance for the given public method name
def dummy_public_api_method_instance(public_method_name)
Method.new(public_method_name.underscore.to_sym, public_method_name, nil, nil)
end
# Creates a dummy API Method instance for the given method name
def dummy_api_method_instance(method_name)
Method.new(method_name, public_api_method_name(method_name), nil, nil)
end
private
def api_public_method_names
read_inheritable_attribute("api_public_method_names") || {}
@ -131,5 +166,128 @@ module ActionWebService # :nodoc:
end
end
end
# Represents an API method and its associated metadata, and provides functionality
# to assist in commonly performed API method tasks.
class Method
attr :name
attr :public_name
attr :expects
attr :returns
def initialize(name, public_name, expects, returns)
@name = name
@public_name = public_name
@expects = expects
@returns = returns
end
# The list of parameter names for this method
def param_names
return [] unless @expects
i = 0
@expects.map{ |spec| param_name(spec, i += 1) }
end
# The name for the given parameter
def param_name(spec, i=1)
spec.is_a?(Hash) ? spec.keys.first.to_s : "p#{i}"
end
# The type of the parameter declared in +spec+. Is either
# the Class of the parameter, or its canonical name (if its a
# base type). Typed array specifications will return the type of
# their elements.
def param_type(spec)
spec = spec.values.first if spec.is_a?(Hash)
param_type = spec.is_a?(Array) ? spec[0] : spec
WS::BaseTypes::class_to_type_name(param_type) rescue param_type
end
# The Class of the parameter declared in +spec+.
def param_class(spec)
type = param_type(spec)
type.is_a?(Symbol) ? WS::BaseTypes.type_name_to_class(type) : type
end
# Registers all types known to this method with the given marshaler
def register_types(marshaler)
@expects.each{ |x| marshaler.register_type(x) } if @expects
@returns.each{ |x| marshaler.register_type(x) } if @returns
end
# Encodes an RPC call for this method. Casting is performed if
# the <tt>:strict</tt> option is given.
def encode_rpc_call(marshaler, encoder, params, options={})
name = options[:method_name] || @public_name
expects = @expects || []
returns = @returns || []
(expects + returns).each { |spec| marshaler.register_type spec }
(0..(params.length-1)).each do |i|
spec = expects[i] || params[i].class
type_binding = marshaler.lookup_type(spec)
param_info = WS::ParamInfo.create(spec, type_binding, i)
if options[:strict]
value = marshaler.cast_outbound_recursive(params[i], spec)
else
value = params[i]
end
param = WS::Param.new(value, param_info)
params[i] = marshaler.marshal(param)
end
encoder.encode_rpc_call(name, params)
end
# Encodes an RPC response for this method. Casting is performed if
# the <tt>:strict</tt> option is given.
def encode_rpc_response(marshaler, encoder, return_value, options={})
if !return_value.nil? && @returns
return_type = @returns[0]
type_binding = marshaler.register_type(return_type)
param_info = WS::ParamInfo.create(return_type, type_binding, 0)
if options[:strict]
return_value = marshaler.cast_inbound_recursive(return_value, return_type)
end
return_value = marshaler.marshal(WS::Param.new(return_value, param_info))
else
return_value = nil
end
encoder.encode_rpc_response(response_name(encoder), return_value)
end
# Casts a set of WS::Param values into the appropriate
# Ruby values
def cast_expects_ws2ruby(marshaler, params)
return [] if @expects.nil?
i = 0
@expects.map do |spec|
value = marshaler.cast_inbound_recursive(params[i].value, spec)
i += 1
value
end
end
# Casts a set of Ruby values into the expected Ruby values
def cast_expects(marshaler, params)
return [] if @expects.nil?
i = 0
@expects.map do |spec|
value = marshaler.cast_outbound_recursive(params[i], spec)
i += 1
value
end
end
# Cast a Ruby return value into the expected Ruby value
def cast_returns(marshaler, return_value)
return nil if @returns.nil?
marshaler.cast_inbound_recursive(return_value, @returns[0])
end
private
def response_name(encoder)
encoder.is_a?(WS::Encoding::SoapRpcEncoding) ? (@public_name + "Response") : @public_name
end
end
end
end

View file

@ -58,7 +58,10 @@ module ActionWebService # :nodoc:
protected
def perform_invocation(method_name, args)
@driver.send(method_name, *args)
method = @api.api_methods[method_name.to_sym]
args = method.cast_expects(@marshaler, args)
return_value = @driver.send(method_name, *args)
method.cast_returns(@marshaler, return_value)
end
def soap_action(method_name)
@ -67,48 +70,33 @@ module ActionWebService # :nodoc:
private
def create_soap_rpc_driver(api, endpoint_uri)
register_api(@marshaler, api)
api.api_methods.each{ |name, method| method.register_types(@marshaler) }
driver = SoapDriver.new(endpoint_uri, nil)
driver.mapping_registry = @marshaler.registry
api.api_methods.each do |name, info|
public_name = api.public_api_method_name(name)
qname = XSD::QName.new(@method_namespace, public_name)
action = soap_action(public_name)
expects = info[:expects]
returns = info[:returns]
api.api_methods.each do |name, method|
qname = XSD::QName.new(@method_namespace, method.public_name)
action = soap_action(method.public_name)
expects = method.expects
returns = method.returns
param_def = []
i = 0
if expects
expects.each do |spec|
param_name = spec.is_a?(Hash) ? spec.keys[0].to_s : "param#{i}"
type_binding = @marshaler.register_type(spec)
param_name = method.param_name(spec, i)
type_binding = @marshaler.lookup_type(spec)
param_def << ['in', param_name, type_binding.mapping]
i += 1
end
end
if returns
type_binding = @marshaler.register_type(returns[0])
type_binding = @marshaler.lookup_type(returns[0])
param_def << ['retval', 'return', type_binding.mapping]
end
driver.add_method(qname, action, name.to_s, param_def)
driver.add_method(qname, action, method.name.to_s, param_def)
end
driver
end
def register_api(marshaler, api)
type_bindings = []
api.api_methods.each do |name, info|
expects, returns = info[:expects], info[:returns]
if expects
expects.each{|type| type_bindings << marshaler.register_type(type)}
end
if returns
returns.each{|type| type_bindings << marshaler.register_type(type)}
end
end
type_bindings
end
class SoapDriver < SOAP::RPC::Driver # :nodoc:
def add_method(qname, soapaction, name, param_def)
@proxy.add_rpc_method(qname, soapaction, name, param_def)

View file

@ -36,43 +36,17 @@ module ActionWebService # :nodoc:
protected
def perform_invocation(method_name, args)
args = transform_outgoing_method_params(method_name, args)
method = @api.api_methods[method_name.to_sym]
method.register_types(@marshaler)
if method.expects && method.expects.length != args.length
raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})")
end
args = method.cast_expects(@marshaler, args)
ok, return_value = @client.call2(public_name(method_name), *args)
return transform_return_value(method_name, return_value) if ok
return method.cast_returns(@marshaler, return_value) if ok
raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}")
end
def transform_outgoing_method_params(method_name, params)
info = @api.api_methods[method_name.to_sym]
expects = info[:expects]
expects_length = expects.nil?? 0 : expects.length
if expects_length != params.length
raise(ClientError, "API declares #{public_name(method_name)} to accept " +
"#{expects_length} parameters, but #{params.length} parameters " +
"were supplied")
end
params = params.dup
if expects_length > 0
i = 0
expects.each do |spec|
type_binding = @marshaler.register_type(spec)
info = WS::ParamInfo.create(spec, type_binding, i)
params[i] = @marshaler.marshal(WS::Param.new(params[i], info))
i += 1
end
end
params
end
def transform_return_value(method_name, return_value)
info = @api.api_methods[method_name.to_sym]
return true unless returns = info[:returns]
type_binding = @marshaler.register_type(returns[0])
info = WS::ParamInfo.create(returns[0], type_binding, 0)
info.name = 'return'
@marshaler.transform_inbound(WS::Param.new(return_value, info))
end
def public_name(method_name)
public_name = @api.public_api_method_name(method_name)
@handler_name ? "#{@handler_name}.#{public_name}" : public_name

View file

@ -34,30 +34,30 @@ module ActionWebService # :nodoc:
def web_service_direct_invoke(invocation)
@method_params = invocation.method_ordered_params
arity = method(invocation.api_method_name).arity rescue 0
arity = method(invocation.api_method.name).arity rescue 0
if arity < 0 || arity > 0
return_value = self.__send__(invocation.api_method_name, *@method_params)
return_value = self.__send__(invocation.api_method.name, *@method_params)
else
return_value = self.__send__(invocation.api_method_name)
return_value = self.__send__(invocation.api_method.name)
end
if invocation.api.has_api_method?(invocation.api_method_name)
returns = invocation.returns ? invocation.returns[0] : nil
if invocation.api.has_api_method?(invocation.api_method.name)
api_method = invocation.api_method
else
returns = return_value.class
api_method = invocation.api_method.dup
api_method.instance_eval{ @returns = [ return_value.class ] }
end
invocation.protocol.marshal_response(invocation.public_method_name, return_value, returns)
invocation.protocol.marshal_response(api_method, return_value)
end
def web_service_delegated_invoke(invocation)
cancellation_reason = nil
return_value = invocation.service.perform_invocation(invocation.api_method_name, invocation.method_ordered_params) do |x|
return_value = invocation.service.perform_invocation(invocation.api_method.name, invocation.method_ordered_params) do |x|
cancellation_reason = x
end
if cancellation_reason
raise(DispatcherError, "request canceled: #{cancellation_reason}")
end
returns = invocation.returns ? invocation.returns[0] : nil
invocation.protocol.marshal_response(invocation.public_method_name, return_value, returns)
invocation.protocol.marshal_response(invocation.api_method, return_value)
end
def web_service_invocation(request)
@ -71,7 +71,6 @@ module ActionWebService # :nodoc:
invocation.service_name = $1
end
end
invocation.public_method_name = public_method_name
case web_service_dispatching_mode
when :direct
invocation.api = self.class.web_service_api
@ -83,54 +82,29 @@ module ActionWebService # :nodoc:
end
invocation.api = invocation.service.class.web_service_api
end
request.api = invocation.api
if invocation.api.has_public_api_method?(public_method_name)
invocation.api_method_name = invocation.api.api_method_name(public_method_name)
invocation.api_method = invocation.api.public_api_method_instance(public_method_name)
else
if invocation.api.default_api_method.nil?
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}")
else
invocation.api_method_name = invocation.api.default_api_method.to_s.to_sym
invocation.api_method = invocation.api.default_api_method_instance
end
end
unless invocation.service.respond_to?(invocation.api_method_name)
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method_name})")
unless invocation.service.respond_to?(invocation.api_method.name)
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})")
end
info = invocation.api.api_methods[invocation.api_method_name]
invocation.expects = info ? info[:expects] : nil
invocation.returns = info ? info[:returns] : nil
if invocation.expects
i = 0
invocation.method_ordered_params = request.method_params.map do |param|
if invocation.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
marshaler = invocation.protocol.marshaler
decoded_param = WS::Encoding::XmlRpcDecodedParam.new(param.info.name, param.value)
marshaled_param = marshaler.typed_unmarshal(decoded_param, invocation.expects[i]) rescue nil
param = marshaled_param ? marshaled_param : param
end
i += 1
param.value
end
i = 0
params = []
invocation.expects.each do |spec|
type_binding = invocation.protocol.register_signature_type(spec)
info = WS::ParamInfo.create(spec, type_binding, i)
params << WS::Param.new(invocation.method_ordered_params[i], info)
i += 1
end
invocation.method_ws_params = params
invocation.method_named_params = {}
invocation.method_ws_params.each do |param|
invocation.method_named_params[param.info.name] = param.value
end
else
invocation.method_ordered_params = []
invocation.method_named_params = {}
request.api_method = invocation.api_method
begin
invocation.method_ordered_params = invocation.api_method.cast_expects_ws2ruby(request.protocol.marshaler, request.method_params)
rescue
invocation.method_ordered_params = request.method_params.map{ |x| x.value }
end
if invocation.returns
invocation.returns.each do |spec|
invocation.protocol.register_signature_type(spec)
end
invocation.method_named_params = {}
invocation.api_method.param_names.inject(0) do |m, n|
invocation.method_named_params[n] = invocation.method_ordered_params[m]
m + 1
end
invocation
end
@ -139,13 +113,9 @@ module ActionWebService # :nodoc:
attr_accessor :protocol
attr_accessor :service_name
attr_accessor :api
attr_accessor :public_method_name
attr_accessor :api_method_name
attr_accessor :api_method
attr_accessor :method_ordered_params
attr_accessor :method_named_params
attr_accessor :method_ws_params
attr_accessor :expects
attr_accessor :returns
attr_accessor :service
end
end

View file

@ -76,7 +76,10 @@ module ActionWebService # :nodoc:
unless self.class.web_service_exception_reporting
exception = DispatcherError.new("Internal server error (exception raised)")
end
response = request.protocol.marshal_response(request.method_name, exception, exception.class)
api_method = request.api_method ? request.api_method.dup : nil
api_method ||= request.api.dummy_api_method_instance(request.method_name)
api_method.instance_eval{ @returns = [ exception.class ] }
response = request.protocol.marshal_response(api_method, exception)
send_web_service_response(response)
else
if self.class.web_service_exception_reporting
@ -95,7 +98,7 @@ module ActionWebService # :nodoc:
end
@session ||= {}
@assigns ||= {}
@params['action'] = invocation.api_method_name.to_s
@params['action'] = invocation.api_method.name.to_s
if before_action == false
raise(DispatcherError, "Method filtered")
end
@ -224,18 +227,18 @@ module ActionWebService # :nodoc:
# APIs
apis.each do |api_name, values|
api = values[0]
api.api_methods.each do |name, info|
api.api_methods.each do |name, method|
gen = lambda do |msg_name, direction|
xm.message('name' => msg_name) do
sym = nil
if direction == :out
returns = info[:returns]
returns = method.returns
if returns
binding = marshaler.register_type(returns[0])
xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens'))
end
else
expects = info[:expects]
expects = method.expects
i = 1
expects.each do |type|
if type.is_a?(Hash)
@ -251,7 +254,7 @@ module ActionWebService # :nodoc:
end
end
end
public_name = api.public_api_method_name(name)
public_name = method.public_name
gen.call(public_name, :in)
gen.call("#{public_name}Response", :out)
end
@ -259,11 +262,10 @@ module ActionWebService # :nodoc:
# Port
port_name = port_name_for(global_service_name, api_name)
xm.portType('name' => port_name) do
api.api_methods.each do |name, info|
public_name = api.public_api_method_name(name)
xm.operation('name' => public_name) do
xm.input('message' => "typens:#{public_name}")
xm.output('message' => "typens:#{public_name}Response")
api.api_methods.each do |name, method|
xm.operation('name' => method.public_name) do
xm.input('message' => "typens:#{method.public_name}")
xm.output('message' => "typens:#{method.public_name}Response")
end
end
end
@ -272,16 +274,15 @@ module ActionWebService # :nodoc:
binding_name = binding_name_for(global_service_name, api_name)
xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do
xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport)
api.api_methods.each do |name, info|
public_name = api.public_api_method_name(name)
xm.operation('name' => public_name) do
api.api_methods.each do |name, method|
xm.operation('name' => method.public_name) do
case web_service_dispatching_mode
when :direct, :layered
soap_action = soap_action_base + "/api/" + public_name
soap_action = soap_action_base + "/api/" + method.public_name
when :delegated
soap_action = soap_action_base \
+ "/" + api_name.to_s \
+ "/" + public_name
+ "/" + method.public_name
end
xm.soap(:operation, 'soapAction' => soap_action)
xm.input do
@ -337,8 +338,8 @@ module ActionWebService # :nodoc:
end
def traverse_custom_types(api, marshaler, &block)
api.api_methods.each do |name, info|
expects, returns = info[:expects], info[:returns]
api.api_methods.each do |name, method|
expects, returns = method.expects, method.returns
expects.each{|x| traverse_custom_type_spec(marshaler, x, &block)} if expects
returns.each{|x| traverse_custom_type_spec(marshaler, x, &block)} if returns
end

View file

@ -3,17 +3,31 @@ module ActionWebService # :nodoc:
class ProtocolError < ActionWebServiceError # :nodoc:
end
class AbstractProtocol
attr :marshaler
attr :encoder
def marshal_response(method, return_value)
body = method.encode_rpc_response(marshaler, encoder, return_value)
Response.new(body, 'text/xml')
end
end
class Request # :nodoc:
attr :protocol
attr :method_name
attr :method_params
attr :service_name
attr_accessor :api
attr_accessor :api_method
def initialize(protocol, method_name, method_params, service_name)
def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil)
@protocol = protocol
@method_name = method_name
@method_params = method_params
@service_name = service_name
@api = api
@api_method = api_method
end
end

View file

@ -6,7 +6,7 @@ module ActionWebService # :nodoc:
base.class_inheritable_option(:wsdl_service_name)
end
class SoapProtocol # :nodoc:
class SoapProtocol < AbstractProtocol # :nodoc:
def initialize
@encoder = WS::Encoding::SoapRpcEncoding.new 'urn:ActionWebService'
@marshaler = WS::Marshaling::SoapMarshaler.new 'urn:ActionWebService'
@ -20,22 +20,6 @@ module ActionWebService # :nodoc:
Request.new(self, method_name, params, service_name)
end
def marshal_response(method_name, return_value, signature_type)
if !return_value.nil? && signature_type
type_binding = @marshaler.register_type(signature_type)
info = WS::ParamInfo.create(signature_type, type_binding, 0)
return_value = @marshaler.marshal(WS::Param.new(return_value, info))
else
return_value = nil
end
body = @encoder.encode_rpc_response(method_name + 'Response', return_value)
Response.new(body, 'text/xml')
end
def register_signature_type(spec)
@marshaler.register_type(spec)
end
def protocol_client(api, protocol_name, endpoint_uri, options)
return nil unless protocol_name == :soap
ActionWebService::Client::Soap.new(api, endpoint_uri, options)

View file

@ -5,9 +5,7 @@ module ActionWebService # :nodoc:
base.register_protocol(XmlRpcProtocol)
end
class XmlRpcProtocol # :nodoc:
attr :marshaler
class XmlRpcProtocol < AbstractProtocol # :nodoc:
def initialize
@encoder = WS::Encoding::XmlRpcEncoding.new
@marshaler = WS::Marshaling::XmlRpcMarshaler.new
@ -22,22 +20,6 @@ module ActionWebService # :nodoc:
nil
end
def marshal_response(method_name, return_value, signature_type)
if !return_value.nil? && signature_type
type_binding = @marshaler.register_type(signature_type)
info = WS::ParamInfo.create(signature_type, type_binding, 0)
return_value = @marshaler.marshal(WS::Param.new(return_value, info))
else
return_value = nil
end
body = @encoder.encode_rpc_response(method_name, return_value)
Response.new(body, 'text/xml')
end
def register_signature_type(spec)
nil
end
def protocol_client(api, protocol_name, endpoint_uri, options)
return nil unless protocol_name == :xmlrpc
ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options)

View file

@ -52,18 +52,9 @@ module Test # :nodoc:
when :delegated, :layered
api = @controller.web_service_object(service_name.to_sym).class.web_service_api
end
info = api.api_methods[api_method_name.to_sym]
((info[:expects] || []) + (info[:returns] || [])).each do |spec|
marshaler.register_type spec
end
expects = info[:expects]
args = args.dup
(0..(args.length-1)).each do |i|
type_binding = marshaler.register_type(expects ? expects[i] : args[i].class)
info = WS::ParamInfo.create(expects ? expects[i] : args[i].class, type_binding, i)
args[i] = marshaler.marshal(WS::Param.new(args[i], info))
end
encoder.encode_rpc_call(public_method_name(service_name, api_method_name), args)
method = api.api_methods[api_method_name.to_sym]
method.register_types(marshaler)
method.encode_rpc_call(marshaler, encoder, args.dup, :method_name => public_method_name(service_name, api_method_name))
end
def decode_rpc_response

View file

@ -1,6 +1,10 @@
module WS
module Marshaling
class AbstractMarshaler
def initialize
@base_type_caster = BaseTypeCaster.new
end
def marshal(param)
raise NotImplementedError
end
@ -12,6 +16,18 @@ module WS
def register_type(type)
nil
end
alias :lookup_type :register_type
def cast_inbound_recursive(value, spec)
raise NotImplementedError
end
def cast_outbound_recursive(value, spec)
raise NotImplementedError
end
attr :base_type_caster
protected :base_type_caster
end
end
end

View file

@ -13,6 +13,7 @@ module WS
attr_accessor :type_namespace
def initialize(type_namespace='')
super()
@type_namespace = type_namespace
@registry = SOAP::Mapping::Registry.new
@spec2binding = {}
@ -92,6 +93,25 @@ module WS
@spec2binding[spec] = array_binding ? array_binding : type_binding
@spec2binding[spec]
end
alias :lookup_type :register_type
def cast_inbound_recursive(value, spec)
binding = lookup_type(spec)
if binding.is_custom_type?
value
else
base_type_caster.cast(value, binding.type_class)
end
end
def cast_outbound_recursive(value, spec)
binding = lookup_type(spec)
if binding.is_custom_type?
value
else
base_type_caster.cast(value, binding.type_class)
end
end
protected
def annotate_arrays(binding, value)
@ -106,7 +126,7 @@ module WS
if binding.type_class.respond_to?(:members)
binding.type_class.members.each do |name, spec|
member_binding = register_type(spec)
member_value = value.send(name)
member_value = value.respond_to?('[]') ? value[name] : value.send(name)
if member_binding.is_custom_type?
annotate_arrays(member_binding, member_value)
end

View file

@ -5,24 +5,24 @@ module WS
class XmlRpcMarshaler < AbstractMarshaler
def initialize
@caster = BaseTypeCaster.new
super()
@spec2binding = {}
end
def marshal(param)
transform_outbound(param)
value = param.value
cast_outbound_recursive(param.value, spec_for(param)) rescue value
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
obj.param.info.data = lookup_type(spec)
value = obj.param.value
obj.param.value = cast_inbound_recursive(value, spec) rescue value
obj.param
end
def register_type(spec)
@ -40,60 +40,87 @@ module WS
@spec2binding[spec] = type_binding
end
alias :lookup_type :register_type
def transform_outbound(param)
binding = param.info.data
def cast_inbound_recursive(value, spec)
binding = lookup_type(spec)
case binding
when XmlRpcArrayBinding
param.value.map{|x| cast_outbound(x, binding.element_klass)}
value.map{ |x| cast_inbound(x, binding.element_klass) }
when XmlRpcBinding
cast_outbound(param.value, param.info.type)
cast_inbound(value, binding.klass)
end
end
def transform_inbound(param)
return param.value if param.info.data.nil?
binding = param.info.data
param.info.type = binding.klass
def cast_outbound_recursive(value, spec)
binding = lookup_type(spec)
case binding
when XmlRpcArrayBinding
param.value.map{|x| cast_inbound(x, binding.element_klass)}
value.map{ |x| cast_outbound(x, binding.element_klass) }
when XmlRpcBinding
cast_inbound(param.value, param.info.type)
cast_outbound(value, binding.klass)
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
private
def spec_for(param)
binding = param.info.data
binding.is_a?(XmlRpcArrayBinding) ? [binding.element_klass] : binding.klass
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)
def cast_inbound(value, klass)
if BaseTypes.base_type?(klass)
value = value.to_time if value.is_a?(XMLRPC::DateTime)
base_type_caster.cast(value, klass)
elsif value.is_a?(XMLRPC::FaultException)
value
elsif klass.ancestors.include?(ActionWebService::Struct)
obj = klass.new
klass.members.each do |name, klass|
name = name.to_s
obj.send('%s=' % name, cast_inbound_recursive(value[name], klass))
end
obj
else
obj = klass.new
if obj.respond_to?(:update)
obj.update(value)
else
value.each do |name, val|
obj.send('%s=' % name.to_s, val)
end
end
obj
end
end
def cast_outbound(value, klass)
if BaseTypes.base_type?(klass)
base_type_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
elsif value.is_a?(ActionWebService::Struct)
struct = {}
value.class.members.each do |name, klass|
name = name.to_s
struct[name] = cast_outbound_recursive(value[name], klass)
end
struct
else
struct = {}
if value.respond_to?(:each_pair)
value.each_pair{ |key, value| struct[key] = value }
else
value.instance_variables.each do |name|
key = name.sub(/^@/, '')
struct[key] = value.instance_variable_get(name)
end
end
struct
end
obj
end
end
end
class XmlRpcBinding

View file

@ -1,5 +1,7 @@
require File.dirname(__FILE__) + '/abstract_unit'
class ActionController::Base; def rescue_action(e) raise e end; end
module DispatcherTest
class Node < ActiveRecord::Base
def initialize(*args)
@ -29,6 +31,10 @@ module DispatcherTest
class Person < ActionWebService::Struct
member :id, :int
member :name, :string
def ==(other)
self.id == other.id && self.name == other.name
end
end
class API < ActionWebService::API::Base
@ -44,6 +50,7 @@ module DispatcherTest
api_method :before_filtered
api_method :after_filtered, :returns => [[:int]]
api_method :struct_return, :returns => [[Node]]
api_method :struct_pass, :expects => [Person]
api_method :base_struct_return, :returns => [[Person]]
api_method :thrower
api_method :void
@ -148,6 +155,7 @@ module DispatcherTest
attr :after_filter_called
attr :after_filter_target_called
attr :void_called
attr :struct_pass_value
def initialize
@before_filter_called = false
@ -155,6 +163,7 @@ module DispatcherTest
@after_filter_called = false
@after_filter_target_called = false
@void_called = false
@struct_pass_value = false
end
def add
@ -184,6 +193,10 @@ module DispatcherTest
[n1, n2]
end
def struct_pass(person)
@struct_pass_value = person
end
def base_struct_return
p1 = Person.new('id' => 1, 'name' => 'person1')
p2 = Person.new('id' => 2, 'name' => 'person2')
@ -328,6 +341,26 @@ module DispatcherCommonTests
end
end
def test_casting
assert_equal 70, do_method_call(@direct_controller, 'Add', "50", "20")
assert_equal false, @direct_controller.struct_pass_value
person = DispatcherTest::Person.new(:id => 1, :name => 'test')
result = do_method_call(@direct_controller, 'StructPass', person)
assert(nil == result || true == result)
assert_equal person, @direct_controller.struct_pass_value
assert !person.equal?(@direct_controller.struct_pass_value)
result = do_method_call(@direct_controller, 'StructPass', {'id' => '1', 'name' => 'test'})
case @encoder
when WS::Encoding::SoapRpcEncoding
# We don't cast complex types for SOAP. SOAP clients should have used the WSDL to
# send the correct types.
assert_equal({'id' => '1', 'name' => 'test'}, @direct_controller.struct_pass_value)
when WS::Encoding::XmlRpcEncoding
assert_equal(person, @direct_controller.struct_pass_value)
assert !person.equal?(@direct_controller.struct_pass_value)
end
end
protected
def service_name(container)
raise NotImplementedError
@ -355,24 +388,20 @@ module DispatcherCommonTests
end
api = container.web_service_object(service_name.to_sym).class.web_service_api
end
info = api.api_methods[method_name] || {}
params = params.dup
((info[:expects] || []) + (info[:returns] || [])).each do |spec|
@marshaler.register_type(spec)
end
expects = info[:expects]
(0..(params.length-1)).each do |i|
type_binding = @marshaler.register_type(expects ? expects[i] : params[i].class)
info = WS::ParamInfo.create(expects ? expects[i] : params[i].class, type_binding, i)
params[i] = @marshaler.marshal(WS::Param.new(params[i], info))
end
body = @encoder.encode_rpc_call(public_method_name, params)
method = api.public_api_method_instance(public_method_name)
method ||= api.dummy_public_api_method_instance(public_method_name)
# we turn off strict so we can test our own handling of incorrectly typed parameters
body = method.encode_rpc_call(@marshaler, @encoder, params.dup, :strict => false)
# puts body
ap_request = create_ap_request(container, body, public_method_name, *params)
ap_response = ActionController::TestResponse.new
container.process(ap_request, ap_response)
# puts ap_response.body
public_method_name, return_value = @encoder.decode_rpc_response(ap_response.body)
if @encoder.is_a?(WS::Encoding::SoapRpcEncoding)
# http://dev.rubyonrails.com/changeset/920
assert_match(/Response$/, public_method_name) unless public_method_name == "fault"
end
@marshaler.unmarshal(return_value).value
end
end

View file

@ -35,13 +35,20 @@ class TC_API < Test::Unit::TestCase
end
def test_signature_canonicalization
assert_equal({:expects=>nil, :returns=>nil}, API.api_methods[:void])
assert_equal({:expects=>[String], :returns=>[String]}, API.api_methods[:expects_and_returns])
assert_equal({:expects=>[Integer, TrueClass], :returns=>nil}, API.api_methods[:expects])
assert_equal({:expects=>nil, :returns=>[Integer, [String]]}, API.api_methods[:returns])
assert_equal({:expects=>[{:appkey=>Integer}, {:publish=>TrueClass}], :returns=>nil}, API.api_methods[:named_signature])
assert_equal({:expects=>[Integer, String, TrueClass], :returns=>nil}, API.api_methods[:string_types])
assert_equal({:expects=>[TrueClass, Integer, String], :returns=>nil}, API.api_methods[:class_types])
assert_equal(nil, API.api_methods[:void].expects)
assert_equal(nil, API.api_methods[:void].returns)
assert_equal([String], API.api_methods[:expects_and_returns].expects)
assert_equal([String], API.api_methods[:expects_and_returns].returns)
assert_equal([Integer, TrueClass], API.api_methods[:expects].expects)
assert_equal(nil, API.api_methods[:expects].returns)
assert_equal(nil, API.api_methods[:returns].expects)
assert_equal([Integer, [String]], API.api_methods[:returns].returns)
assert_equal([{:appkey=>Integer}, {:publish=>TrueClass}], API.api_methods[:named_signature].expects)
assert_equal(nil, API.api_methods[:named_signature].returns)
assert_equal([Integer, String, TrueClass], API.api_methods[:string_types].expects)
assert_equal(nil, API.api_methods[:string_types].returns)
assert_equal([TrueClass, Integer, String], API.api_methods[:class_types].expects)
assert_equal(nil, API.api_methods[:class_types].returns)
end
def test_not_instantiable

View file

@ -66,6 +66,11 @@ class TC_ClientSoap < Test::Unit::TestCase
assert(@container.value_normal.nil?)
assert_equal(5, @client.normal(5, 6))
assert_equal([5, 6], @container.value_normal)
assert_equal(5, @client.normal("7", "8"))
assert_equal([7, 8], @container.value_normal)
assert_raises(TypeError) do
assert_equal(5, @client.normal(true, false))
end
end
def test_array_return

View file

@ -60,6 +60,11 @@ class TC_ClientXmlRpc < Test::Unit::TestCase
assert(@container.value_normal.nil?)
assert_equal(5, @client.normal(5, 6))
assert_equal([5, 6], @container.value_normal)
assert_equal(5, @client.normal("7", "8"))
assert_equal([7, 8], @container.value_normal)
assert_raises(TypeError) do
assert_equal(5, @client.normal(true, false))
end
end
def test_array_return
@ -86,7 +91,7 @@ class TC_ClientXmlRpc < Test::Unit::TestCase
def test_named_parameters
assert(@container.value_named_parameters.nil?)
assert_equal(true, @client.named_parameters("xxx", 7))
assert_equal(nil, @client.named_parameters("xxx", 7))
assert_equal(["xxx", 7], @container.value_named_parameters)
end
@ -97,7 +102,7 @@ class TC_ClientXmlRpc < Test::Unit::TestCase
end
def test_invalid_signature
assert_raises(ActionWebService::Client::ClientError) do
assert_raises(ArgumentError) do
@client.normal
end
end

View file

@ -1,5 +1,6 @@
#!/usr/bin/env ruby
require 'test/unit'
$:.unshift(File.dirname(__FILE__) + '/../lib')
args = Dir[File.join(File.dirname(__FILE__), '*_test.rb')] + Dir[File.join(File.dirname(__FILE__), 'ws/*_test.rb')]
(r = Test::Unit::AutoRunner.new(true)).process_args(args)
exit r.run

View file

@ -30,6 +30,12 @@ class SoapMarshalingTest < Test::Unit::TestCase
marshaler.unmarshal(nil)
end
assert_equal(nil, marshaler.register_type(nil))
assert_raises(NotImplementedError) do
marshaler.cast_inbound_recursive(nil, nil)
end
assert_raises(NotImplementedError) do
marshaler.cast_outbound_recursive(nil, nil)
end
end
def test_marshaling