aboutsummaryrefslogtreecommitdiffstats
path: root/actionwebservice/lib/action_service
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-02-18 23:43:09 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-02-18 23:43:09 +0000
commit7a67d0f617db7d2962b6c3b80466e21570b244bf (patch)
tree56fa640e31f4f7f22d34a246bc1b197706a07e3a /actionwebservice/lib/action_service
parentfdecb6843ba8c5b0f718225f343017e11fa7f711 (diff)
downloadrails-7a67d0f617db7d2962b6c3b80466e21570b244bf.tar.gz
rails-7a67d0f617db7d2962b6c3b80466e21570b244bf.tar.bz2
rails-7a67d0f617db7d2962b6c3b80466e21570b244bf.zip
Renamed Action Service to Action Web Service
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@669 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionwebservice/lib/action_service')
-rw-r--r--actionwebservice/lib/action_service/api.rb2
-rw-r--r--actionwebservice/lib/action_service/api/abstract.rb192
-rw-r--r--actionwebservice/lib/action_service/api/action_controller.rb92
-rw-r--r--actionwebservice/lib/action_service/base.rb41
-rw-r--r--actionwebservice/lib/action_service/client.rb3
-rw-r--r--actionwebservice/lib/action_service/client/base.rb35
-rw-r--r--actionwebservice/lib/action_service/client/soap.rb87
-rw-r--r--actionwebservice/lib/action_service/client/xmlrpc.rb76
-rw-r--r--actionwebservice/lib/action_service/container.rb232
-rw-r--r--actionwebservice/lib/action_service/invocation.rb252
-rw-r--r--actionwebservice/lib/action_service/protocol.rb4
-rw-r--r--actionwebservice/lib/action_service/protocol/abstract.rb128
-rw-r--r--actionwebservice/lib/action_service/protocol/registry.rb55
-rw-r--r--actionwebservice/lib/action_service/protocol/soap.rb484
-rw-r--r--actionwebservice/lib/action_service/protocol/xmlrpc.rb183
-rw-r--r--actionwebservice/lib/action_service/router.rb2
-rw-r--r--actionwebservice/lib/action_service/router/action_controller.rb99
-rw-r--r--actionwebservice/lib/action_service/router/wsdl.rb210
-rw-r--r--actionwebservice/lib/action_service/struct.rb55
-rw-r--r--actionwebservice/lib/action_service/support/class_inheritable_options.rb26
-rw-r--r--actionwebservice/lib/action_service/support/signature.rb100
21 files changed, 2358 insertions, 0 deletions
diff --git a/actionwebservice/lib/action_service/api.rb b/actionwebservice/lib/action_service/api.rb
new file mode 100644
index 0000000000..61f36fff56
--- /dev/null
+++ b/actionwebservice/lib/action_service/api.rb
@@ -0,0 +1,2 @@
+require 'action_service/api/abstract'
+require 'action_service/api/action_controller'
diff --git a/actionwebservice/lib/action_service/api/abstract.rb b/actionwebservice/lib/action_service/api/abstract.rb
new file mode 100644
index 0000000000..aab37a285d
--- /dev/null
+++ b/actionwebservice/lib/action_service/api/abstract.rb
@@ -0,0 +1,192 @@
+module ActionService # :nodoc:
+ module API # :nodoc:
+ class APIError < ActionService::ActionServiceError # :nodoc:
+ end
+
+ def self.append_features(base) # :nodoc:
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ # Attaches ActionService API +definition+ to the calling class.
+ #
+ # Action Controllers can have a default associated API, removing the need
+ # to call this method if you follow the Action Service naming conventions.
+ #
+ # A controller with a class name of GoogleSearchController will
+ # implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the
+ # API definition class to be named <tt>GoogleSearchAPI</tt> or
+ # <tt>GoogleSearchApi</tt>.
+ #
+ # ==== Service class example
+ #
+ # class MyService < ActionService::Base
+ # web_service_api MyAPI
+ # end
+ #
+ # class MyAPI < ActionService::API::Base
+ # ...
+ # end
+ #
+ # ==== Controller class example
+ #
+ # class MyController < ActionController::Base
+ # web_service_api MyAPI
+ # end
+ #
+ # class MyAPI < ActionService::API::Base
+ # ...
+ # end
+ def web_service_api(definition=nil)
+ if definition.nil?
+ read_inheritable_attribute("web_service_api")
+ else
+ if definition.is_a?(Symbol)
+ raise(APIError, "symbols can only be used for #web_service_api inside of a controller")
+ end
+ unless definition.respond_to?(:ancestors) && definition.ancestors.include?(Base)
+ raise(APIError, "#{definition.to_s} is not a valid API definition")
+ end
+ write_inheritable_attribute("web_service_api", definition)
+ call_web_service_api_callbacks(self, definition)
+ end
+ end
+
+ def add_web_service_api_callback(&block) # :nodoc:
+ write_inheritable_array("web_service_api_callbacks", [block])
+ end
+
+ private
+ def call_web_service_api_callbacks(container_class, definition)
+ (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block|
+ block.call(container_class, definition)
+ end
+ end
+ 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.
+ #
+ # It is not intended to be instantiated.
+ #
+ # It is attached to web service implementation classes like
+ # ActionService::Base and ActionController::Base derivatives by using
+ # ClassMethods#web_service_api.
+ class Base
+ # Whether to transform the public API method names into camel-cased names
+ class_inheritable_option :inflect_names, true
+
+ # If present, the name of a method to call when the remote caller
+ # tried to call a nonexistent method. Semantically equivalent to
+ # +method_missing+.
+ class_inheritable_option :default_api_method
+
+ # Disallow instantiation
+ private_class_method :new, :allocate
+
+ class << self
+ include ActionService::Signature
+
+ # API methods have a +name+, which must be the Ruby method name to use when
+ # performing the invocation on the web service object.
+ #
+ # The signatures for the method input parameters and return value can
+ # by specified in +options+.
+ #
+ # A signature is an array of one or more parameter specifiers.
+ # A parameter specifier can be one of the following:
+ #
+ # * A symbol or string of representing one of the Action Service base types.
+ # See ActionService::Signature for a canonical list of the base types.
+ # * The Class object of the parameter type
+ # * A single-element Array containing one of the two preceding items. This
+ # will cause Action Service to treat the parameter at that position
+ # as an array containing only values of the given type.
+ # * A Hash containing as key the name of the parameter, and as value
+ # one of the three preceding items
+ #
+ # If no method input parameter or method return value signatures are given,
+ # the method is assumed to take no parameters and/or return no values of
+ # interest, and any values that are received by the server will be
+ # discarded and ignored.
+ #
+ # Valid options:
+ # [<tt>:expects</tt>] Signature for the method input parameters
+ # [<tt>:returns</tt>] Signature for the method return value
+ # [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
+ def api_method(name, options={})
+ validate_options([:expects, :returns, :expects_and_returns], options.keys)
+ if options[:expects_and_returns]
+ expects = options[:expects_and_returns]
+ returns = options[:expects_and_returns]
+ else
+ expects = options[:expects]
+ returns = options[:returns]
+ end
+ expects = canonical_signature(expects) if expects
+ returns = canonical_signature(returns) if returns
+ if expects
+ expects.each do |param|
+ klass = signature_parameter_class(param)
+ klass = klass[0] if klass.is_a?(Array)
+ if klass.ancestors.include?(ActiveRecord::Base)
+ raise(ActionServiceError, "ActiveRecord model classes not allowed in :expects")
+ end
+ end
+ end
+ name = name.to_sym
+ public_name = public_api_method_name(name)
+ info = { :expects => expects, :returns => returns }
+ write_inheritable_hash("api_methods", name => info)
+ write_inheritable_hash("api_public_method_names", public_name => name)
+ end
+
+ # Whether the given method name is a service method on this API
+ def has_api_method?(name)
+ api_methods.has_key?(name)
+ end
+
+ # Whether the given public method name has a corresponding service method
+ # on this API
+ def has_public_api_method?(public_name)
+ api_public_method_names.has_key?(public_name)
+ end
+
+ # The corresponding public method name for the given service method name
+ def public_api_method_name(name)
+ if inflect_names
+ name.to_s.camelize
+ else
+ name.to_s
+ end
+ end
+
+ # The corresponding service method name for the given public method name
+ def api_method_name(public_name)
+ api_public_method_names[public_name]
+ end
+
+ # A Hash containing all service methods on this API, and their
+ # associated metadata.
+ def api_methods
+ read_inheritable_attribute("api_methods") || {}
+ end
+
+ private
+ def api_public_method_names
+ read_inheritable_attribute("api_public_method_names") || {}
+ end
+
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ unless unknown_option_keys.empty?
+ raise(ActionServiceError, "Unknown options: #{unknown_option_keys}")
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/api/action_controller.rb b/actionwebservice/lib/action_service/api/action_controller.rb
new file mode 100644
index 0000000000..d603f3a570
--- /dev/null
+++ b/actionwebservice/lib/action_service/api/action_controller.rb
@@ -0,0 +1,92 @@
+module ActionService # :nodoc:
+ module API # :nodoc:
+ module ActionController # :nodoc:
+ def self.append_features(base) # :nodoc:
+ base.class_eval do
+ class << self
+ alias_method :inherited_without_api, :inherited
+ alias_method :web_service_api_without_require, :web_service_api
+ end
+ end
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ # Creates a client for accessing remote web services, using the
+ # given +protocol+ to communicate with the +endpoint_uri+.
+ #
+ # ==== Example
+ #
+ # class MyController < ActionController::Base
+ # web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger'
+ # end
+ #
+ # In this example, a protected method named <tt>blogger</tt> will
+ # now exist on the controller, and calling it will return the
+ # XML-RPC client object for working with that remote service.
+ #
+ # +options+ is the set of protocol client specific options,
+ # see a protocol client class for details.
+ #
+ # If your API definition does not exist on the load path with the
+ # correct rules for it to be found using +name+, you can pass through
+ # the API definition class in +options+, using a key of <tt>:api</tt>
+ def web_client_api(name, protocol, endpoint_uri, options={})
+ unless method_defined?(name)
+ api_klass = options.delete(:api) || require_web_service_api(name)
+ class_eval do
+ define_method(name) do
+ probe_protocol_client(api_klass, protocol, endpoint_uri, options)
+ end
+ protected name
+ end
+ end
+ end
+
+ def web_service_api(definition=nil) # :nodoc:
+ return web_service_api_without_require if definition.nil?
+ case definition
+ when String, Symbol
+ klass = require_web_service_api(definition)
+ else
+ klass = definition
+ end
+ web_service_api_without_require(klass)
+ end
+
+ def require_web_service_api(name) # :nodoc:
+ case name
+ when String, Symbol
+ file_name = name.to_s.underscore + "_api"
+ class_name = file_name.camelize
+ class_names = [class_name, class_name.sub(/Api$/, 'API')]
+ begin
+ require_dependency(file_name)
+ rescue LoadError => load_error
+ requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
+ raise LoadError, requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}"
+ end
+ klass = nil
+ class_names.each do |name|
+ klass = name.constantize rescue nil
+ break unless klass.nil?
+ end
+ unless klass
+ raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found")
+ end
+ klass
+ else
+ raise(ArgumentError, "expected String or Symbol argument")
+ end
+ end
+
+ private
+ def inherited(child)
+ inherited_without_api(child)
+ child.web_service_api(child.controller_path)
+ rescue Exception => e
+ end
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/base.rb b/actionwebservice/lib/action_service/base.rb
new file mode 100644
index 0000000000..05fd2afd34
--- /dev/null
+++ b/actionwebservice/lib/action_service/base.rb
@@ -0,0 +1,41 @@
+require 'action_service/support/class_inheritable_options'
+require 'action_service/support/signature'
+
+module ActionService # :nodoc:
+ class ActionServiceError < StandardError # :nodoc:
+ end
+
+ # An Action Service object implements a specified API.
+ #
+ # Used by controllers operating in _Delegated_ dispatching mode.
+ #
+ # ==== Example
+ #
+ # class PersonService < ActionService::Base
+ # web_service_api PersonAPI
+ #
+ # def find_person(criteria)
+ # Person.find_all [...]
+ # end
+ #
+ # def delete_person(id)
+ # Person.find_by_id(id).destroy
+ # end
+ # end
+ #
+ # class PersonAPI < ActionService::API::Base
+ # api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]]
+ # api_method :delete_person, :expects => [:int]
+ # end
+ #
+ # class SearchCriteria < ActionStruct::Base
+ # member :firstname, :string
+ # member :lastname, :string
+ # member :email, :string
+ # end
+ class Base
+ # Whether to report exceptions back to the caller in the protocol's exception
+ # format
+ class_inheritable_option :web_service_exception_reporting, true
+ end
+end
diff --git a/actionwebservice/lib/action_service/client.rb b/actionwebservice/lib/action_service/client.rb
new file mode 100644
index 0000000000..ce91529f20
--- /dev/null
+++ b/actionwebservice/lib/action_service/client.rb
@@ -0,0 +1,3 @@
+require 'action_service/client/base'
+require 'action_service/client/soap'
+require 'action_service/client/xmlrpc'
diff --git a/actionwebservice/lib/action_service/client/base.rb b/actionwebservice/lib/action_service/client/base.rb
new file mode 100644
index 0000000000..955887a4d8
--- /dev/null
+++ b/actionwebservice/lib/action_service/client/base.rb
@@ -0,0 +1,35 @@
+module ActionService # :nodoc:
+ module Client # :nodoc:
+ class ClientError < StandardError # :nodoc:
+ end
+
+ class Base # :nodoc:
+ def initialize(api, endpoint_uri)
+ @api = api
+ @endpoint_uri = endpoint_uri
+ end
+
+ def method_missing(name, *args) # :nodoc:
+ call_name = method_name(name)
+ return super(name, *args) if call_name.nil?
+ perform_invocation(call_name, args)
+ end
+
+ protected
+ def perform_invocation(method_name, args) # :nodoc:
+ raise NotImplementedError, "use a protocol-specific client"
+ end
+
+ private
+ def method_name(name)
+ if @api.has_api_method?(name.to_sym)
+ name.to_s
+ elsif @api.has_public_api_method?(name.to_s)
+ @api.api_method_name(name.to_s).to_s
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/client/soap.rb b/actionwebservice/lib/action_service/client/soap.rb
new file mode 100644
index 0000000000..c617f36589
--- /dev/null
+++ b/actionwebservice/lib/action_service/client/soap.rb
@@ -0,0 +1,87 @@
+require 'soap/rpc/driver'
+require 'uri'
+
+module ActionService # :nodoc:
+ module Client # :nodoc:
+
+ # Implements SOAP client support (using RPC encoding for the messages).
+ #
+ # ==== Example Usage
+ #
+ # class PersonAPI < ActionService::API::Base
+ # api_method :find_all, :returns => [[Person]]
+ # end
+ #
+ # soap_client = ActionService::Client::Soap.new(PersonAPI, "http://...")
+ # persons = soap_client.find_all
+ #
+ class Soap < Base
+
+ # Creates a new web service client using the SOAP RPC protocol.
+ #
+ # +api+ must be an ActionService::API::Base derivative, and
+ # +endpoint_uri+ must point at the relevant URL to which protocol requests
+ # will be sent with HTTP POST.
+ #
+ # Valid options:
+ # [<tt>:service_name</tt>] If the remote server has used a custom +wsdl_service_name+
+ # option, you must specify it here
+ def initialize(api, endpoint_uri, options={})
+ super(api, endpoint_uri)
+ @service_name = options[:service_name] || 'ActionService'
+ @namespace = "urn:#{@service_name}"
+ @mapper = ActionService::Protocol::Soap::SoapMapper.new(@namespace)
+ @protocol = ActionService::Protocol::Soap::SoapProtocol.new(@mapper)
+ @soap_action_base = options[:soap_action_base]
+ @soap_action_base ||= URI.parse(endpoint_uri).path
+ @driver = create_soap_rpc_driver(api, endpoint_uri)
+ end
+
+ protected
+ def perform_invocation(method_name, args)
+ @driver.send(method_name, *args)
+ end
+
+ def soap_action(method_name)
+ "#{@soap_action_base}/#{method_name}"
+ end
+
+ private
+ def create_soap_rpc_driver(api, endpoint_uri)
+ @mapper.map_api(api)
+ driver = SoapDriver.new(endpoint_uri, nil)
+ driver.mapping_registry = @mapper.registry
+ api.api_methods.each do |name, info|
+ public_name = api.public_api_method_name(name)
+ qname = XSD::QName.new(@namespace, public_name)
+ action = soap_action(public_name)
+ expects = info[:expects]
+ returns = info[:returns]
+ param_def = []
+ i = 1
+ if expects
+ expects.each do |klass|
+ param_name = klass.is_a?(Hash) ? klass.keys[0] : "param#{i}"
+ mapping = @mapper.lookup(klass)
+ param_def << ['in', param_name, mapping.registry_mapping]
+ i += 1
+ end
+ end
+ if returns
+ mapping = @mapper.lookup(returns[0])
+ param_def << ['retval', 'return', mapping.registry_mapping]
+ end
+ driver.add_method(qname, action, name.to_s, param_def)
+ end
+ driver
+ end
+
+ class SoapDriver < SOAP::RPC::Driver # :nodoc:
+ def add_method(qname, soapaction, name, param_def)
+ @proxy.add_rpc_method(qname, soapaction, name, param_def)
+ add_rpc_method_interface(name, param_def)
+ end
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/client/xmlrpc.rb b/actionwebservice/lib/action_service/client/xmlrpc.rb
new file mode 100644
index 0000000000..d0d007f871
--- /dev/null
+++ b/actionwebservice/lib/action_service/client/xmlrpc.rb
@@ -0,0 +1,76 @@
+require 'uri'
+require 'xmlrpc/client'
+
+module ActionService # :nodoc:
+ module Client # :nodoc:
+
+ # Implements XML-RPC client support
+ #
+ # ==== Example Usage
+ #
+ # class BloggerAPI < ActionService::API::Base
+ # inflect_names false
+ # api_method :getRecentPosts, :returns => [[Blog::Post]]
+ # end
+ #
+ # blog = ActionService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger")
+ # posts = blog.getRecentPosts
+ class XmlRpc < Base
+
+ # Creates a new web service client using the XML-RPC protocol.
+ #
+ # +api+ must be an ActionService::API::Base derivative, and
+ # +endpoint_uri+ must point at the relevant URL to which protocol requests
+ # will be sent with HTTP POST.
+ #
+ # Valid options:
+ # [<tt>:handler_name</tt>] If the remote server defines its services inside special
+ # handler (the Blogger API uses a <tt>"blogger"</tt> handler name for example),
+ # provide it here, or your method calls will fail
+ def initialize(api, endpoint_uri, options={})
+ @api = api
+ @handler_name = options[:handler_name]
+ @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout])
+ end
+
+ protected
+ def perform_invocation(method_name, args)
+ args = transform_outgoing_method_params(method_name, args)
+ ok, return_value = @client.call2(public_name(method_name), *args)
+ return transform_return_value(method_name, 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]
+ signature = info[:expects]
+ signature_length = signature.nil?? 0 : signature.length
+ if signature_length != params.length
+ raise(ProtocolError, "API declares #{public_name(method_name)} to accept " +
+ "#{signature_length} parameters, but #{params.length} parameters " +
+ "were supplied")
+ end
+ if signature_length > 0
+ signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature)
+ (1..signature.size).each do |i|
+ i -= 1
+ params[i] = Protocol::XmlRpc::XmlRpcProtocol.ruby_to_xmlrpc(params[i], signature[i])
+ end
+ end
+ params
+ end
+
+ def transform_return_value(method_name, return_value)
+ info = @api.api_methods[method_name.to_sym]
+ return true unless signature = info[:returns]
+ signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature)
+ Protocol::XmlRpc::XmlRpcProtocol.xmlrpc_to_ruby(return_value, signature[0])
+ end
+
+ def public_name(method_name)
+ public_name = @api.public_api_method_name(method_name)
+ @handler_name ? "#{@handler_name}.#{public_name}" : public_name
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/container.rb b/actionwebservice/lib/action_service/container.rb
new file mode 100644
index 0000000000..282e6ad928
--- /dev/null
+++ b/actionwebservice/lib/action_service/container.rb
@@ -0,0 +1,232 @@
+module ActionService # :nodoc:
+ module Container # :nodoc:
+ class ContainerError < ActionService::ActionServiceError # :nodoc:
+ end
+
+ def self.append_features(base) # :nodoc:
+ super
+ base.class_inheritable_option(:web_service_dispatching_mode, :direct)
+ base.class_inheritable_option(:web_service_exception_reporting, true)
+ base.extend(ClassMethods)
+ base.send(:include, ActionService::Container::InstanceMethods)
+ end
+
+ module ClassMethods
+ # Declares a web service that will provides access to the API of the given
+ # +object+. +object+ must be an ActionService::Base derivative.
+ #
+ # Web service object creation can either be _immediate_, where the object
+ # instance is given at class definition time, or _deferred_, where
+ # object instantiation is delayed until request time.
+ #
+ # ==== Immediate web service object example
+ #
+ # class ApiController < ApplicationController
+ # web_service_dispatching_mode :delegated
+ #
+ # web_service :person, PersonService.new
+ # end
+ #
+ # For deferred instantiation, a block should be given instead of an
+ # object instance. This block will be executed in controller instance
+ # context, so it can rely on controller instance variables being present.
+ #
+ # ==== Deferred web service object example
+ #
+ # class ApiController < ApplicationController
+ # web_service_dispatching_mode :delegated
+ #
+ # web_service(:person) { PersonService.new(@request.env) }
+ # end
+ def web_service(name, object=nil, &block)
+ if (object && block_given?) || (object.nil? && block.nil?)
+ raise(ContainerError, "either service, or a block must be given")
+ end
+ name = name.to_sym
+ if block_given?
+ info = { name => { :block => block } }
+ else
+ info = { name => { :object => object } }
+ end
+ write_inheritable_hash("web_services", info)
+ call_web_service_definition_callbacks(self, name, info)
+ end
+
+ # Whether this service contains a service with the given +name+
+ def has_web_service?(name)
+ web_services.has_key?(name.to_sym)
+ end
+
+ def web_services # :nodoc:
+ read_inheritable_attribute("web_services") || {}
+ end
+
+ def add_web_service_definition_callback(&block) # :nodoc:
+ write_inheritable_array("web_service_definition_callbacks", [block])
+ end
+
+ private
+ def call_web_service_definition_callbacks(container_class, web_service_name, service_info)
+ (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block|
+ block.call(container_class, web_service_name, service_info)
+ end
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ def web_service_object(web_service_name)
+ info = self.class.web_services[web_service_name.to_sym]
+ unless info
+ raise(ContainerError, "no such web service '#{web_service_name}'")
+ end
+ service = info[:block]
+ service ? instance_eval(&service) : info[:object]
+ end
+
+ private
+ def dispatch_web_service_request(protocol_request)
+ case web_service_dispatching_mode
+ when :direct
+ dispatch_direct_web_service_request(protocol_request)
+ when :delegated
+ dispatch_delegated_web_service_request(protocol_request)
+ else
+ raise(ContainerError, "unsupported dispatching mode :#{web_service_dispatching_mode}")
+ end
+ end
+
+ def dispatch_direct_web_service_request(protocol_request)
+ public_method_name = protocol_request.public_method_name
+ api = self.class.web_service_api
+ method_name = api.api_method_name(public_method_name)
+ block = nil
+ expects = nil
+ if method_name
+ signature = api.api_methods[method_name]
+ expects = signature[:expects]
+ protocol_request.type = Protocol::CheckedMessage
+ protocol_request.signature = expects
+ protocol_request.return_signature = signature[:returns]
+ else
+ protocol_request.type = Protocol::UncheckedMessage
+ system_methods = self.class.read_inheritable_attribute('default_system_methods') || {}
+ protocol = protocol_request.protocol
+ block = system_methods[protocol.class]
+ unless block
+ method_name = api.default_api_method
+ unless method_name && respond_to?(method_name)
+ raise(ContainerError, "no such method ##{public_method_name}")
+ end
+ end
+ end
+
+ @method_params = protocol_request.unmarshal
+ @params ||= {}
+ if expects
+ (1..@method_params.size).each do |i|
+ i -= 1
+ if expects[i].is_a?(Hash)
+ @params[expects[i].keys.shift.to_s] = @method_params[i]
+ else
+ @params["param#{i}"] = @method_params[i]
+ end
+ end
+ end
+
+ if respond_to?(:before_action)
+ @params['action'] = method_name.to_s
+ return protocol_request.marshal(nil) if before_action == false
+ end
+
+ perform_invoke = lambda do
+ if block
+ block.call(public_method_name, self.class, *@method_params)
+ else
+ send(method_name)
+ end
+ end
+ try_default = true
+ result = nil
+ catch(:try_default) do
+ result = perform_invoke.call
+ try_default = false
+ end
+ if try_default
+ method_name = api.default_api_method
+ if method_name
+ protocol_request.type = Protocol::UncheckedMessage
+ else
+ raise(ContainerError, "no such method ##{public_method_name}")
+ end
+ result = perform_invoke.call
+ end
+ after_action if respond_to?(:after_action)
+ protocol_request.marshal(result)
+ end
+
+ def dispatch_delegated_web_service_request(protocol_request)
+ web_service_name = protocol_request.web_service_name
+ service = web_service_object(web_service_name)
+ api = service.class.web_service_api
+ public_method_name = protocol_request.public_method_name
+ method_name = api.api_method_name(public_method_name)
+
+ invocation = ActionService::Invocation::InvocationRequest.new(
+ ActionService::Invocation::ConcreteInvocation,
+ public_method_name,
+ method_name)
+
+ if method_name
+ protocol_request.type = Protocol::CheckedMessage
+ signature = api.api_methods[method_name]
+ protocol_request.signature = signature[:expects]
+ protocol_request.return_signature = signature[:returns]
+ invocation.params = protocol_request.unmarshal
+ else
+ protocol_request.type = Protocol::UncheckedMessage
+ invocation.type = ActionService::Invocation::VirtualInvocation
+ system_methods = self.class.read_inheritable_attribute('default_system_methods') || {}
+ protocol = protocol_request.protocol
+ block = system_methods[protocol.class]
+ if block
+ invocation.block = block
+ invocation.block_params << service.class
+ else
+ method_name = api.default_api_method
+ if method_name && service.respond_to?(method_name)
+ invocation.params = protocol_request.unmarshal
+ invocation.method_name = method_name.to_sym
+ else
+ raise(ContainerError, "no such method /#{web_service_name}##{public_method_name}")
+ end
+ end
+ end
+
+ canceled_reason = nil
+ canceled_block = lambda{|r| canceled_reason = r}
+ perform_invoke = lambda do
+ service.perform_invocation(invocation, &canceled_block)
+ end
+ try_default = true
+ result = nil
+ catch(:try_default) do
+ result = perform_invoke.call
+ try_default = false
+ end
+ if try_default
+ method_name = api.default_api_method
+ if method_name
+ protocol_request.type = Protocol::UncheckedMessage
+ invocation.params = protocol_request.unmarshal
+ invocation.method_name = method_name.to_sym
+ invocation.type = ActionService::Invocation::UnpublishedConcreteInvocation
+ else
+ raise(ContainerError, "no such method /#{web_service_name}##{public_method_name}")
+ end
+ result = perform_invoke.call
+ end
+ protocol_request.marshal(result)
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/invocation.rb b/actionwebservice/lib/action_service/invocation.rb
new file mode 100644
index 0000000000..f35ab76386
--- /dev/null
+++ b/actionwebservice/lib/action_service/invocation.rb
@@ -0,0 +1,252 @@
+module ActionService # :nodoc:
+ module Invocation # :nodoc:
+ ConcreteInvocation = :concrete
+ VirtualInvocation = :virtual
+ UnpublishedConcreteInvocation = :unpublished_concrete
+
+ class InvocationError < ActionService::ActionServiceError # :nodoc:
+ end
+
+ def self.append_features(base) # :nodoc:
+ super
+ base.extend(ClassMethods)
+ base.send(:include, ActionService::Invocation::InstanceMethods)
+ end
+
+ # Invocation interceptors provide a means to execute custom code before
+ # and after method invocations on ActionService::Base objects.
+ #
+ # When running in _Direct_ dispatching mode, ActionController filters
+ # should be used for this functionality instead.
+ #
+ # The semantics of invocation interceptors are the same as ActionController
+ # filters, and accept the same parameters and options.
+ #
+ # A _before_ interceptor can also cancel execution by returning +false+,
+ # or returning a <tt>[false, "cancel reason"]</tt> array if it wishes to supply
+ # a reason for canceling the request.
+ #
+ # === Example
+ #
+ # class CustomService < ActionService::Base
+ # before_invocation :intercept_add, :only => [:add]
+ #
+ # def add(a, b)
+ # a + b
+ # end
+ #
+ # private
+ # def intercept_add
+ # return [false, "permission denied"] # cancel it
+ # end
+ # end
+ #
+ # Options:
+ # [<tt>:except</tt>] A list of methods for which the interceptor will NOT be called
+ # [<tt>:only</tt>] A list of methods for which the interceptor WILL be called
+ module ClassMethods
+ # Appends the given +interceptors+ to be called
+ # _before_ method invocation.
+ def append_before_invocation(*interceptors, &block)
+ conditions = extract_conditions!(interceptors)
+ interceptors << block if block_given?
+ add_interception_conditions(interceptors, conditions)
+ append_interceptors_to_chain("before", interceptors)
+ end
+
+ # Prepends the given +interceptors+ to be called
+ # _before_ method invocation.
+ def prepend_before_invocation(*interceptors, &block)
+ conditions = extract_conditions!(interceptors)
+ interceptors << block if block_given?
+ add_interception_conditions(interceptors, conditions)
+ prepend_interceptors_to_chain("before", interceptors)
+ end
+
+ alias :before_invocation :append_before_invocation
+
+ # Appends the given +interceptors+ to be called
+ # _after_ method invocation.
+ def append_after_invocation(*interceptors, &block)
+ conditions = extract_conditions!(interceptors)
+ interceptors << block if block_given?
+ add_interception_conditions(interceptors, conditions)
+ append_interceptors_to_chain("after", interceptors)
+ end
+
+ # Prepends the given +interceptors+ to be called
+ # _after_ method invocation.
+ def prepend_after_invocation(*interceptors, &block)
+ conditions = extract_conditions!(interceptors)
+ interceptors << block if block_given?
+ add_interception_conditions(interceptors, conditions)
+ prepend_interceptors_to_chain("after", interceptors)
+ end
+
+ alias :after_invocation :append_after_invocation
+
+ def before_invocation_interceptors # :nodoc:
+ read_inheritable_attribute("before_invocation_interceptors")
+ end
+
+ def after_invocation_interceptors # :nodoc:
+ read_inheritable_attribute("after_invocation_interceptors")
+ end
+
+ def included_intercepted_methods # :nodoc:
+ read_inheritable_attribute("included_intercepted_methods") || {}
+ end
+
+ def excluded_intercepted_methods # :nodoc:
+ read_inheritable_attribute("excluded_intercepted_methods") || {}
+ end
+
+ private
+ def append_interceptors_to_chain(condition, interceptors)
+ write_inheritable_array("#{condition}_invocation_interceptors", interceptors)
+ end
+
+ def prepend_interceptors_to_chain(condition, interceptors)
+ interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors")
+ write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors)
+ end
+
+ def extract_conditions!(interceptors)
+ return nil unless interceptors.last.is_a? Hash
+ interceptors.pop
+ end
+
+ def add_interception_conditions(interceptors, conditions)
+ return unless conditions
+ included, excluded = conditions[:only], conditions[:except]
+ write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included
+ write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded
+ end
+
+ def condition_hash(interceptors, *methods)
+ interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})}
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ def self.append_features(base)
+ super
+ base.class_eval do
+ alias_method :perform_invocation_without_interception, :perform_invocation
+ alias_method :perform_invocation, :perform_invocation_with_interception
+ end
+ end
+
+ def perform_invocation_with_interception(invocation, &block)
+ return if before_invocation(invocation.method_name, invocation.params, &block) == false
+ result = perform_invocation_without_interception(invocation)
+ after_invocation(invocation.method_name, invocation.params, result)
+ result
+ end
+
+ def perform_invocation(invocation)
+ if invocation.concrete?
+ unless self.respond_to?(invocation.method_name) && \
+ self.class.web_service_api.has_api_method?(invocation.method_name)
+ raise InvocationError, "no such web service method '#{invocation.method_name}' on service object"
+ end
+ end
+ params = invocation.params
+ if invocation.concrete? || invocation.unpublished_concrete?
+ self.send(invocation.method_name, *params)
+ else
+ if invocation.block
+ params = invocation.block_params + params
+ invocation.block.call(invocation.public_method_name, *params)
+ else
+ self.send(invocation.method_name, *params)
+ end
+ end
+ end
+
+ def before_invocation(name, args, &block)
+ call_interceptors(self.class.before_invocation_interceptors, [name, args], &block)
+ end
+
+ def after_invocation(name, args, result)
+ call_interceptors(self.class.after_invocation_interceptors, [name, args, result])
+ end
+
+ private
+
+ def call_interceptors(interceptors, interceptor_args, &block)
+ if interceptors and not interceptors.empty?
+ interceptors.each do |interceptor|
+ next if method_exempted?(interceptor, interceptor_args[0].to_s)
+ result = case
+ when interceptor.is_a?(Symbol)
+ self.send(interceptor, *interceptor_args)
+ when interceptor_block?(interceptor)
+ interceptor.call(self, *interceptor_args)
+ when interceptor_class?(interceptor)
+ interceptor.intercept(self, *interceptor_args)
+ else
+ raise(
+ InvocationError,
+ "Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method"
+ )
+ end
+ reason = nil
+ if result.is_a?(Array)
+ reason = result[1] if result[1]
+ result = result[0]
+ end
+ if result == false
+ block.call(reason) if block && reason
+ return false
+ end
+ end
+ end
+ end
+
+ def interceptor_block?(interceptor)
+ interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1)
+ end
+
+ def interceptor_class?(interceptor)
+ interceptor.respond_to?("intercept")
+ end
+
+ def method_exempted?(interceptor, method_name)
+ case
+ when self.class.included_intercepted_methods[interceptor]
+ !self.class.included_intercepted_methods[interceptor].include?(method_name)
+ when self.class.excluded_intercepted_methods[interceptor]
+ self.class.excluded_intercepted_methods[interceptor].include?(method_name)
+ end
+ end
+ end
+
+ class InvocationRequest # :nodoc:
+ attr_accessor :type
+ attr :public_method_name
+ attr_accessor :method_name
+ attr_accessor :params
+ attr_accessor :block
+ attr :block_params
+
+ def initialize(type, public_method_name, method_name, params=nil)
+ @type = type
+ @public_method_name = public_method_name
+ @method_name = method_name
+ @params = params || []
+ @block = nil
+ @block_params = []
+ end
+
+ def concrete?
+ @type == ConcreteInvocation ? true : false
+ end
+
+ def unpublished_concrete?
+ @type == UnpublishedConcreteInvocation ? true : false
+ end
+ end
+
+ end
+end
diff --git a/actionwebservice/lib/action_service/protocol.rb b/actionwebservice/lib/action_service/protocol.rb
new file mode 100644
index 0000000000..5e71b2bcfd
--- /dev/null
+++ b/actionwebservice/lib/action_service/protocol.rb
@@ -0,0 +1,4 @@
+require 'action_service/protocol/abstract'
+require 'action_service/protocol/registry'
+require 'action_service/protocol/soap'
+require 'action_service/protocol/xmlrpc'
diff --git a/actionwebservice/lib/action_service/protocol/abstract.rb b/actionwebservice/lib/action_service/protocol/abstract.rb
new file mode 100644
index 0000000000..bd02b6e829
--- /dev/null
+++ b/actionwebservice/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 :web_service_name
+ attr_accessor :public_method_name
+ attr_accessor :content_type
+
+ def initialize(protocol, raw_body, web_service_name, public_method_name, content_type, options={})
+ super(options)
+ @protocol = protocol
+ @raw_body = raw_body
+ @web_service_name = web_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/actionwebservice/lib/action_service/protocol/registry.rb b/actionwebservice/lib/action_service/protocol/registry.rb
new file mode 100644
index 0000000000..e06361f916
--- /dev/null
+++ b/actionwebservice/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/actionwebservice/lib/action_service/protocol/soap.rb b/actionwebservice/lib/action_service/protocol/soap.rb
new file mode 100644
index 0000000000..993e174e52
--- /dev/null
+++ b/actionwebservice/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.web_service_dispatching_mode
+ web_services = nil
+ case dispatching_mode
+ when :direct
+ api = container.class.web_service_api
+ if container.respond_to?(:controller_class_name)
+ web_service_name = container.controller_class_name.sub(/Controller$/, '').underscore
+ else
+ web_service_name = container.class.name.demodulize.underscore
+ end
+ web_services = { web_service_name => api }
+ when :delegated
+ web_services = {}
+ container.class.web_services.each do |web_service_name, web_service_info|
+ begin
+ object = container.web_service_object(web_service_name)
+ rescue Exception => e
+ raise(ProtocolError, "failed to retrieve web service object for web service '#{web_service_name}': #{e.message}")
+ end
+ web_services[web_service_name] = object.class.web_service_api
+ end
+ end
+ web_services.each do |web_service_name, api|
+ if api.nil?
+ raise(ProtocolError, "no web service API set while in :#{dispatching_mode} mode")
+ end
+ map_api(api) do |api_methods|
+ yield web_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/actionwebservice/lib/action_service/protocol/xmlrpc.rb b/actionwebservice/lib/action_service/protocol/xmlrpc.rb
new file mode 100644
index 0000000000..32b8e00327
--- /dev/null
+++ b/actionwebservice/lib/action_service/protocol/xmlrpc.rb
@@ -0,0 +1,183 @@
+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:
+ 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.web_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
diff --git a/actionwebservice/lib/action_service/router.rb b/actionwebservice/lib/action_service/router.rb
new file mode 100644
index 0000000000..16f0ae4463
--- /dev/null
+++ b/actionwebservice/lib/action_service/router.rb
@@ -0,0 +1,2 @@
+require 'action_service/router/action_controller'
+require 'action_service/router/wsdl'
diff --git a/actionwebservice/lib/action_service/router/action_controller.rb b/actionwebservice/lib/action_service/router/action_controller.rb
new file mode 100644
index 0000000000..ca9c94e35c
--- /dev/null
+++ b/actionwebservice/lib/action_service/router/action_controller.rb
@@ -0,0 +1,99 @@
+module ActionService # :nodoc:
+ module Router # :nodoc:
+ module ActionController # :nodoc:
+ def self.append_features(base) # :nodoc:
+ base.add_web_service_api_callback do |container_class, api|
+ if container_class.web_service_dispatching_mode == :direct
+ container_class.class_eval <<-EOS
+ def api
+ process_action_service_request
+ end
+ EOS
+ end
+ end
+ base.add_web_service_definition_callback do |klass, name, info|
+ if klass.web_service_dispatching_mode == :delegated
+ klass.class_eval <<-EOS
+ def #{name}
+ process_action_service_request
+ end
+ EOS
+ end
+ end
+ base.send(:include, ActionService::Router::ActionController::InstanceMethods)
+ end
+
+ module InstanceMethods # :nodoc:
+ private
+ def process_action_service_request
+ protocol_request = nil
+ begin
+ begin
+ protocol_request = probe_request_protocol(self.request)
+ rescue Exception => e
+ unless logger.nil?
+ logger.error "Invalid request: #{e.message}"
+ logger.error self.request.raw_post
+ end
+ raise
+ end
+ if protocol_request
+ log_request(protocol_request)
+ protocol_response = dispatch_web_service_request(protocol_request)
+ log_response(protocol_response)
+ response_options = {
+ :type => protocol_response.content_type,
+ :disposition => 'inline'
+ }
+ send_data(protocol_response.raw_body, response_options)
+ else
+ logger.fatal "Invalid Action Service service or method requested" unless logger.nil?
+ render_text 'Internal protocol error', "500 Invalid service/method"
+ end
+ rescue Exception => e
+ log_error e unless logger.nil?
+ exc_response = nil
+ case web_service_dispatching_mode
+ when :direct
+ if self.class.web_service_exception_reporting
+ exc_response = protocol_request.protocol.marshal_exception(e)
+ end
+ when :delegated
+ web_service = web_service_object(protocol_request.service_name) rescue nil
+ if web_service && web_service.class.web_service_exception_reporting
+ exc_response = protocol_request.protocol.marshal_exception(e) rescue nil
+ end
+ end
+ if exc_response
+ response_options = {
+ :type => exc_response.content_type,
+ :disposition => 'inline'
+ }
+ log_response exc_response
+ send_data(exc_response.raw_body, response_options)
+ else
+ render_text 'Internal protocol error', "500 #{e.message}"
+ end
+ end
+ end
+
+ def log_request(protocol_request)
+ unless logger.nil?
+ web_service_name = protocol_request.web_service_name
+ method_name = protocol_request.public_method_name
+ logger.info "\nProcessing Action Service Request: #{web_service_name}##{method_name}"
+ logger.info "Raw Request Body:"
+ logger.info protocol_request.raw_body
+ end
+ end
+
+ def log_response(protocol_response)
+ unless logger.nil?
+ logger.info "\nRaw Response Body:"
+ logger.info protocol_response.raw_body
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/router/wsdl.rb b/actionwebservice/lib/action_service/router/wsdl.rb
new file mode 100644
index 0000000000..c2f29da0b0
--- /dev/null
+++ b/actionwebservice/lib/action_service/router/wsdl.rb
@@ -0,0 +1,210 @@
+module ActionService # :nodoc:
+ module Router # :nodoc:
+ module Wsdl # :nodoc:
+ def self.append_features(base) # :nodoc:
+ base.class_eval do
+ class << self
+ alias_method :inherited_without_wsdl, :inherited
+ end
+ end
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def inherited(child)
+ inherited_without_wsdl(child)
+ child.send(:include, ActionService::Router::Wsdl::InstanceMethods)
+ end
+ end
+
+ module InstanceMethods # :nodoc:
+ XsdNs = 'http://www.w3.org/2001/XMLSchema'
+ WsdlNs = 'http://schemas.xmlsoap.org/wsdl/'
+ SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/'
+ SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/'
+ SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http'
+
+ def wsdl
+ case @request.method
+ when :get
+ begin
+ host_name = @request.env['HTTP_HOST']||@request.env['SERVER_NAME']
+ uri = "http://#{host_name}/#{controller_name}/"
+ soap_action_base = "/#{controller_name}"
+ xml = to_wsdl(self, uri, soap_action_base)
+ send_data(xml, :type => 'text/xml', :disposition => 'inline')
+ rescue Exception => e
+ log_error e unless logger.nil?
+ render_text('', "500 #{e.message}")
+ end
+ when :post
+ render_text('', "500 POST not supported")
+ end
+ end
+
+ private
+ def to_wsdl(container, uri, soap_action_base)
+ wsdl = ""
+
+ web_service_dispatching_mode = container.web_service_dispatching_mode
+ mapper = container.class.soap_mapper
+ namespace = mapper.custom_namespace
+ wsdl_service_name = namespace.split(/:/)[1]
+
+ services = {}
+ mapper.map_container_services(container) do |name, api, api_methods|
+ services[name] = [api, api_methods]
+ end
+ custom_types = mapper.custom_types
+
+
+ xm = Builder::XmlMarkup.new(:target => wsdl, :indent => 2)
+ xm.instruct!
+
+ xm.definitions('name' => wsdl_service_name,
+ 'targetNamespace' => namespace,
+ 'xmlns:typens' => namespace,
+ 'xmlns:xsd' => XsdNs,
+ 'xmlns:soap' => SoapNs,
+ 'xmlns:soapenc' => SoapEncodingNs,
+ 'xmlns:wsdl' => WsdlNs,
+ 'xmlns' => WsdlNs) do
+
+ # Custom type XSD generation
+ if custom_types.size > 0
+ xm.types do
+ xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do
+ custom_types.each do |klass, mapping|
+ case
+ when mapping.is_a?(ActionService::Protocol::Soap::SoapArrayMapping)
+ xm.xsd(:complexType, 'name' => mapping.type_name) do
+ xm.xsd(:complexContent) do
+ xm.xsd(:restriction, 'base' => 'soapenc:Array') do
+ xm.xsd(:attribute, 'ref' => 'soapenc:arrayType',
+ 'wsdl:arrayType' => mapping.element_mapping.qualified_type_name + '[]')
+ end
+ end
+ end
+ when mapping.is_a?(ActionService::Protocol::Soap::SoapMapping)
+ xm.xsd(:complexType, 'name' => mapping.type_name) do
+ xm.xsd(:all) do
+ mapping.each_attribute do |name, type_name|
+ xm.xsd(:element, 'name' => name, 'type' => type_name)
+ end
+ end
+ end
+ else
+ raise(WsdlError, "unsupported mapping type #{mapping.class.name}")
+ end
+ end
+ end
+ end
+ end
+
+ services.each do |service_name, service_values|
+ service_api, api_methods = service_values
+ # Parameter list message definitions
+ api_methods.each do |method_name, method_signature|
+ gen = lambda do |msg_name, direction|
+ xm.message('name' => msg_name) do
+ sym = nil
+ if direction == :out
+ if method_signature[:returns]
+ xm.part('name' => 'return', 'type' => method_signature[:returns][0].qualified_type_name)
+ end
+ else
+ mapping_list = method_signature[:expects]
+ i = 1
+ mapping_list.each do |mapping|
+ if mapping.is_a?(Hash)
+ param_name = mapping.keys.shift
+ mapping = mapping.values.shift
+ else
+ param_name = "param#{i}"
+ end
+ xm.part('name' => param_name, 'type' => mapping.qualified_type_name)
+ i += 1
+ end if mapping_list
+ end
+ end
+ end
+ public_name = service_api.public_api_method_name(method_name)
+ gen.call(public_name, :in)
+ gen.call("#{public_name}Response", :out)
+ end
+
+ # Declare the port
+ port_name = port_name_for(wsdl_service_name, service_name)
+ xm.portType('name' => port_name) do
+ api_methods.each do |method_name, method_signature|
+ public_name = service_api.public_api_method_name(method_name)
+ xm.operation('name' => public_name) do
+ xm.input('message' => "typens:#{public_name}")
+ xm.output('message' => "typens:#{public_name}Response")
+ end
+ end
+ end
+
+ # Bind the port to SOAP
+ binding_name = binding_name_for(wsdl_service_name, service_name)
+ xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do
+ xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport)
+ api_methods.each do |method_name, method_signature|
+ public_name = service_api.public_api_method_name(method_name)
+ xm.operation('name' => public_name) do
+ case web_service_dispatching_mode
+ when :direct
+ soap_action = soap_action_base + "/api/" + public_name
+ when :delegated
+ soap_action = soap_action_base \
+ + "/" + service_name.to_s \
+ + "/" + public_name
+ end
+ xm.soap(:operation, 'soapAction' => soap_action)
+ xm.input do
+ xm.soap(:body,
+ 'use' => 'encoded',
+ 'namespace' => namespace,
+ 'encodingStyle' => SoapEncodingNs)
+ end
+ xm.output do
+ xm.soap(:body,
+ 'use' => 'encoded',
+ 'namespace' => namespace,
+ 'encodingStyle' => SoapEncodingNs)
+ end
+ end
+ end
+ end
+ end
+
+ # Define the service
+ xm.service('name' => "#{wsdl_service_name}Service") do
+ services.each do |service_name, service_values|
+ port_name = port_name_for(wsdl_service_name, service_name)
+ binding_name = binding_name_for(wsdl_service_name, service_name)
+ case web_service_dispatching_mode
+ when :direct
+ binding_target = 'api'
+ when :delegated
+ binding_target = service_name.to_s
+ end
+ xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do
+ xm.soap(:address, 'location' => "#{uri}#{binding_target}")
+ end
+ end
+ end
+ end
+ end
+
+ def port_name_for(wsdl_service_name, service_name)
+ "#{wsdl_service_name}#{service_name.to_s.camelize}Port"
+ end
+
+ def binding_name_for(wsdl_service_name, service_name)
+ "#{wsdl_service_name}#{service_name.to_s.camelize}Binding"
+ end
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/struct.rb b/actionwebservice/lib/action_service/struct.rb
new file mode 100644
index 0000000000..142127b052
--- /dev/null
+++ b/actionwebservice/lib/action_service/struct.rb
@@ -0,0 +1,55 @@
+module ActionService
+ # To send structured types across the wire, derive from ActionService::Struct,
+ # and use +member+ to declare structure members.
+ #
+ # ActionService::Struct should be used in method signatures when you want to accept or return
+ # structured types that have no Active Record model class representations, or you don't
+ # want to expose your entire Active Record model to remote callers.
+ #
+ # === Example
+ #
+ # class Person < ActionService::Struct
+ # member :id, :int
+ # member :firstnames, [:string]
+ # member :lastname, :string
+ # member :email, :string
+ # end
+ # person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe')
+ #
+ # Active Record model classes are already implicitly supported for method
+ # return signatures. A structure containing its columns as members will be
+ # automatically generated if its present in a signature.
+ class Struct
+
+ # If a Hash is given as argument to an ActionService::Struct constructor,
+ # it can contain initial values for the structure member.
+ def initialize(values={})
+ if values.is_a?(Hash)
+ values.map{|k,v| send('%s=' % k.to_s, v)}
+ end
+ end
+
+ # The member with the given name
+ def [](name)
+ send(name.to_s)
+ end
+
+ class << self
+ include ActionService::Signature
+
+ # Creates a structure member with the specified +name+ and +type+. Generates
+ # accessor methods for reading and writing the member value.
+ def member(name, type)
+ write_inheritable_hash("struct_members", name => signature_parameter_class(type))
+ class_eval <<-END
+ def #{name}; @#{name}; end
+ def #{name}=(value); @#{name} = value; end
+ END
+ end
+
+ def members # :nodoc:
+ read_inheritable_attribute("struct_members") || {}
+ end
+ end
+ end
+end
diff --git a/actionwebservice/lib/action_service/support/class_inheritable_options.rb b/actionwebservice/lib/action_service/support/class_inheritable_options.rb
new file mode 100644
index 0000000000..4d1c2ed471
--- /dev/null
+++ b/actionwebservice/lib/action_service/support/class_inheritable_options.rb
@@ -0,0 +1,26 @@
+class Class # :nodoc:
+ def class_inheritable_option(sym, default_value=nil)
+ write_inheritable_attribute sym, default_value
+ class_eval <<-EOS
+ def self.#{sym}(value=nil)
+ if !value.nil?
+ write_inheritable_attribute(:#{sym}, value)
+ else
+ read_inheritable_attribute(:#{sym})
+ end
+ end
+
+ def self.#{sym}=(value)
+ write_inheritable_attribute(:#{sym}, value)
+ end
+
+ def #{sym}
+ self.class.#{sym}
+ end
+
+ def #{sym}=(value)
+ self.class.#{sym} = value
+ end
+ EOS
+ end
+end
diff --git a/actionwebservice/lib/action_service/support/signature.rb b/actionwebservice/lib/action_service/support/signature.rb
new file mode 100644
index 0000000000..946118c523
--- /dev/null
+++ b/actionwebservice/lib/action_service/support/signature.rb
@@ -0,0 +1,100 @@
+module ActionService # :nodoc:
+ # Action Service parameter specifiers may contain symbols or strings
+ # instead of Class objects, for a limited set of base types.
+ #
+ # This provides an unambiguous way to specify that a given parameter
+ # contains an integer or boolean value, for example.
+ #
+ # The allowed set of symbol/string aliases:
+ #
+ # [<tt>:int</tt>] any integer value
+ # [<tt>:float</tt>] any floating point value
+ # [<tt>:string</tt>] any string value
+ # [<tt>:bool</tt>] any boolean value
+ # [<tt>:time</tt>] any value containing both date and time
+ # [<tt>:date</tt>] any value containing only a date
+ module Signature
+ class SignatureError < StandardError # :nodoc:
+ end
+
+ private
+ def canonical_signature(params)
+ return nil if params.nil?
+ params.map do |param|
+ klass = signature_parameter_class(param)
+ if param.is_a?(Hash)
+ param[param.keys[0]] = klass
+ param
+ else
+ klass
+ end
+ end
+ end
+
+ def signature_parameter_class(param)
+ param = param.is_a?(Hash) ? param.values[0] : param
+ is_array = param.is_a?(Array)
+ param = is_array ? param[0] : param
+ param = param.is_a?(String) ? param.to_sym : param
+ param = param.is_a?(Symbol) ? signature_ruby_class(param) : param
+ is_array ? [param] : param
+ end
+
+
+ def canonical_signature_base_type(base_type)
+ base_type = base_type.to_sym
+ case base_type
+ 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(SignatureError, ":#{base_type} is not an ActionService base type")
+ end
+ end
+
+ def signature_ruby_class(base_type)
+ case canonical_signature_base_type(base_type)
+ when :int
+ Integer
+ when :string
+ String
+ when :bool
+ TrueClass
+ when :float
+ Float
+ when :time
+ Time
+ when :date
+ Date
+ end
+ end
+
+ def signature_base_type(ruby_class)
+ case ruby_class
+ when Bignum, Integer, Fixnum
+ :int
+ when String
+ :string
+ when TrueClass, FalseClass
+ :bool
+ when Float, Numeric, Precision
+ :float
+ when Time, DateTime
+ :time
+ when Date
+ :date
+ else
+ raise(SignatureError, "#{ruby_class.name} is not an ActionService base type")
+ end
+ end
+ end
+end