Checkout of Instiki Trunk 1/21/2007.

This commit is contained in:
Jacques Distler 2007-01-22 07:43:50 -06:00
commit 69b62b6f33
1138 changed files with 139586 additions and 0 deletions

View file

@ -0,0 +1,249 @@
module ActionWebService # :nodoc:
module API # :nodoc:
# 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
# ActionWebService::Base and ActionController::Base derivatives by using
# <tt>container.web_service_api</tt>, where <tt>container</tt> is an
# ActionController::Base or a ActionWebService::Base.
#
# See ActionWebService::Container::Direct::ClassMethods for an example
# of use.
class Base
# Action WebService API subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load.
include Reloadable::Subclasses
# Whether to transform the public API method names into camel-cased names
class_inheritable_option :inflect_names, true
# Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
# The default is +false+; you should be aware of the security implications
# of allowing this, and ensure that you don't allow remote callers to
# easily overwrite data they should not have access to.
class_inheritable_option :allow_active_record_expects, false
# 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 ActionWebService::SignatureTypes
# 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 representing one of the Action Web Service base types.
# See ActionWebService::SignatureTypes 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 Web 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={})
unless options.is_a?(Hash)
raise(ActionWebServiceError, "Expected a Hash for options")
end
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)
returns = canonical_signature(returns)
if expects
expects.each do |type|
type = type.element_type if type.is_a?(ArrayType)
if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
end
end
end
name = name.to_sym
public_name = public_api_method_name(name)
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
# 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
# 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
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(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
end
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
@caster = ActionWebService::Casting::BaseCaster.new(self)
end
# The list of parameter names for this method
def param_names
return [] unless @expects
@expects.map{ |type| type.name }
end
# Casts a set of Ruby values into the expected Ruby values
def cast_expects(params)
@caster.cast_expects(params)
end
# Cast a Ruby return value into the expected Ruby value
def cast_returns(return_value)
@caster.cast_returns(return_value)
end
# Returns the index of the first expected parameter
# with the given name
def expects_index_of(param_name)
return -1 if @expects.nil?
(0..(@expects.length-1)).each do |i|
return i if @expects[i].name.to_s == param_name.to_s
end
-1
end
# Returns a hash keyed by parameter name for the given
# parameter list
def expects_to_hash(params)
return {} if @expects.nil?
h = {}
@expects.zip(params){ |type, param| h[type.name] = param }
h
end
# Backwards compatibility with previous API
def [](sig_type)
case sig_type
when :expects
@expects.map{|x| compat_signature_entry(x)}
when :returns
@returns.map{|x| compat_signature_entry(x)}
end
end
# String representation of this method
def to_s
fqn = ""
fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ")
fqn << "#{@public_name}("
fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects
fqn << ")"
fqn
end
private
def compat_signature_entry(entry)
if entry.array?
[compat_signature_entry(entry.element_type)]
else
if entry.spec.is_a?(Hash)
{entry.spec.keys.first => entry.type_class}
else
entry.type_class
end
end
end
end
end
end

View file

@ -0,0 +1,42 @@
module ActionWebService # :nodoc:
class ActionWebServiceError < StandardError # :nodoc:
end
# An Action Web Service object implements a specified API.
#
# Used by controllers operating in _Delegated_ dispatching mode.
#
# ==== Example
#
# class PersonService < ActionWebService::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 < ActionWebService::API::Base
# api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]]
# api_method :delete_person, :expects => [:int]
# end
#
# class SearchCriteria < ActionWebService::Struct
# member :firstname, :string
# member :lastname, :string
# member :email, :string
# end
class Base
# Action WebService subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load.
include Reloadable::Subclasses
# Whether to report exceptions back to the caller in the protocol's exception
# format
class_inheritable_option :web_service_exception_reporting, true
end
end

View file

@ -0,0 +1,136 @@
require 'time'
require 'date'
require 'xmlrpc/datetime'
module ActionWebService # :nodoc:
module Casting # :nodoc:
class CastingError < ActionWebServiceError # :nodoc:
end
# Performs casting of arbitrary values into the correct types for the signature
class BaseCaster # :nodoc:
def initialize(api_method)
@api_method = api_method
end
# Coerces the parameters in +params+ (an Enumerable) into the types
# this method expects
def cast_expects(params)
self.class.cast_expects(@api_method, params)
end
# Coerces the given +return_value+ into the type returned by this
# method
def cast_returns(return_value)
self.class.cast_returns(@api_method, return_value)
end
class << self
include ActionWebService::SignatureTypes
def cast_expects(api_method, params) # :nodoc:
return [] if api_method.expects.nil?
api_method.expects.zip(params).map{ |type, param| cast(param, type) }
end
def cast_returns(api_method, return_value) # :nodoc:
return nil if api_method.returns.nil?
cast(return_value, api_method.returns[0])
end
def cast(value, signature_type) # :nodoc:
return value if signature_type.nil? # signature.length != params.length
return nil if value.nil?
# XMLRPC protocol doesn't support nil values. It uses false instead.
# It should never happen for SOAP.
if signature_type.structured? && value.equal?(false)
return nil
end
unless signature_type.array? || signature_type.structured?
return value if canonical_type(value.class) == signature_type.type
end
if signature_type.array?
unless value.respond_to?(:entries) && !value.is_a?(String)
raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}"
end
value.entries.map do |entry|
cast(entry, signature_type.element_type)
end
elsif signature_type.structured?
cast_to_structured_type(value, signature_type)
elsif !signature_type.custom?
cast_base_type(value, signature_type)
end
end
def cast_base_type(value, signature_type) # :nodoc:
# This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type
# in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time,
# with the caveat that we won't be able to handle pre-1970 dates that are sent to us.
#
# See http://dev.rubyonrails.com/ticket/2516
value = value.to_time if value.is_a?(XMLRPC::DateTime)
case signature_type.type
when :int
Integer(value)
when :string
value.to_s
when :base64
if value.is_a?(ActionWebService::Base64)
value
else
ActionWebService::Base64.new(value.to_s)
end
when :bool
return false if value.nil?
return value if value == true || value == false
case value.to_s.downcase
when '1', 'true', 'y', 'yes'
true
when '0', 'false', 'n', 'no'
false
else
raise CastingError, "Don't know how to cast #{value.class} into Boolean"
end
when :float
Float(value)
when :time
value = "#{value['2']}/#{value['3']}/#{value['1']} #{value['4']}:#{value['5']}:#{value['6']}" if value.kind_of?(Hash)
Time.parse(value.to_s)
when :date
value = "#{value['2']}/#{value['3']}/#{value['1']}" if value.kind_of?(Hash)
Date.parse(value.to_s)
when :datetime
value = "#{value['2']}/#{value['3']}/#{value['1']} #{value['4']}:#{value['5']}:#{value['6']}" if value.kind_of?(Hash)
DateTime.parse(value.to_s)
end
end
def cast_to_structured_type(value, signature_type) # :nodoc:
obj = nil
obj = value if canonical_type(value.class) == canonical_type(signature_type.type)
obj ||= signature_type.type_class.new
if value.respond_to?(:each_pair)
klass = signature_type.type_class
value.each_pair do |name, val|
type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil
val = cast(val, type) if type
# See http://dev.rubyonrails.com/ticket/3567
val = val.to_time if val.is_a?(XMLRPC::DateTime)
obj.__send__("#{name}=", val) if obj.respond_to?(name)
end
elsif value.respond_to?(:attributes)
signature_type.each_member do |name, type|
val = value.__send__(name)
obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name)
end
else
raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}"
end
obj
end
end
end
end
end

View file

@ -0,0 +1,3 @@
require 'action_web_service/client/base'
require 'action_web_service/client/soap_client'
require 'action_web_service/client/xmlrpc_client'

View file

@ -0,0 +1,28 @@
module ActionWebService # :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?
self.perform_invocation(call_name, args)
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
end
end
end
end
end

View file

@ -0,0 +1,111 @@
require 'soap/rpc/driver'
require 'uri'
module ActionWebService # :nodoc:
module Client # :nodoc:
# Implements SOAP client support (using RPC encoding for the messages).
#
# ==== Example Usage
#
# class PersonAPI < ActionWebService::API::Base
# api_method :find_all, :returns => [[Person]]
# end
#
# soap_client = ActionWebService::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 ActionWebService::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>:namespace</tt>] If the remote server has used a custom namespace to
# declare its custom types, you can specify it here. This would
# be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute
# in .NET, for example.
# [<tt>:driver_options</tt>] If you want to supply any custom SOAP RPC driver
# options, you can provide them as a Hash here
#
# The <tt>:driver_options</tt> option can be used to configure the backend SOAP
# RPC driver. An example of configuring the SOAP backend to do
# client-certificate authenticated SSL connections to the server:
#
# opts = {}
# opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER'
# opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path
# opts['protocol.http.ssl_config.client_key'] = client_key_file_path
# opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path
# client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts)
def initialize(api, endpoint_uri, options={})
super(api, endpoint_uri)
@namespace = options[:namespace] || 'urn:ActionWebService'
@driver_options = options[:driver_options] || {}
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace
@soap_action_base = options[:soap_action_base]
@soap_action_base ||= URI.parse(endpoint_uri).path
@driver = create_soap_rpc_driver(api, endpoint_uri)
@driver_options.each do |name, value|
@driver.options[name.to_s] = value
end
end
protected
def perform_invocation(method_name, args)
method = @api.api_methods[method_name.to_sym]
args = method.cast_expects(args.dup) rescue args
return_value = @driver.send(method_name, *args)
method.cast_returns(return_value.dup) rescue return_value
end
def soap_action(method_name)
"#{@soap_action_base}/#{method_name}"
end
private
def create_soap_rpc_driver(api, endpoint_uri)
@protocol.register_api(api)
driver = SoapDriver.new(endpoint_uri, nil)
driver.mapping_registry = @protocol.marshaler.registry
api.api_methods.each do |name, method|
qname = XSD::QName.new(@namespace, method.public_name)
action = soap_action(method.public_name)
expects = method.expects
returns = method.returns
param_def = []
if expects
expects.each do |type|
type_binding = @protocol.marshaler.lookup_type(type)
if SOAP::Version >= "1.5.5"
param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]]
else
param_def << ['in', type.name, type_binding.mapping]
end
end
end
if returns
type_binding = @protocol.marshaler.lookup_type(returns[0])
if SOAP::Version >= "1.5.5"
param_def << ['retval', 'return', [type_binding.type.type_class.to_s]]
else
param_def << ['retval', 'return', type_binding.mapping]
end
end
driver.add_method(qname, action, method.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

View file

@ -0,0 +1,58 @@
require 'uri'
require 'xmlrpc/client'
module ActionWebService # :nodoc:
module Client # :nodoc:
# Implements XML-RPC client support
#
# ==== Example Usage
#
# class BloggerAPI < ActionWebService::API::Base
# inflect_names false
# api_method :getRecentPosts, :returns => [[Blog::Post]]
# end
#
# blog = ActionWebService::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 ActionWebService::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]
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new
@client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout])
end
protected
def perform_invocation(method_name, args)
method = @api.api_methods[method_name.to_sym]
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(args.dup) rescue args
if method.expects
method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) }
end
ok, return_value = @client.call2(public_name(method_name), *args)
return (method.cast_returns(return_value.dup) rescue return_value) if ok
raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}")
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

View file

@ -0,0 +1,3 @@
require 'action_web_service/container/direct_container'
require 'action_web_service/container/delegated_container'
require 'action_web_service/container/action_controller_container'

View file

@ -0,0 +1,95 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module ActionController # :nodoc:
def self.append_features(base) # :nodoc:
class << base
include ClassMethods
alias_method :inherited_without_api, :inherited
alias_method :inherited, :inherited_with_api
alias_method :web_service_api_without_require, :web_service_api
alias_method :web_service_api, :web_service_api_with_require
end
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 in
# the API definition class via +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
create_web_service_client(api_klass, protocol, endpoint_uri, options)
end
protected name
end
end
end
def web_service_api_with_require(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]
msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}"
raise LoadError.new(msg).copy_blame!(load_error)
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_with_api(child)
inherited_without_api(child)
begin child.web_service_api(child.controller_path)
rescue MissingSourceFile => e
raise unless e.is_missing?("apis/#{child.controller_path}_api")
end
end
end
end
end
end

View file

@ -0,0 +1,87 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module Delegated # :nodoc:
class ContainerError < ActionWebServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
base.send(:include, ActionWebService::Container::Delegated::InstanceMethods)
end
module ClassMethods
# Declares a web service that will provide access to the API of the given
# +object+. +object+ must be an ActionWebService::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 ? self.instance_eval(&service) : info[:object]
end
end
end
end
end

View file

@ -0,0 +1,70 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module Direct # :nodoc:
class ContainerError < ActionWebServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
end
module ClassMethods
# Attaches ActionWebService 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 Web 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 < ActionWebService::Base
# web_service_api MyAPI
# end
#
# class MyAPI < ActionWebService::API::Base
# ...
# end
#
# ==== Controller class example
#
# class MyController < ActionController::Base
# web_service_api MyAPI
# end
#
# class MyAPI < ActionWebService::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(ContainerError, "symbols can only be used for #web_service_api inside of a controller")
end
unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base)
raise(ContainerError, "#{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
end
end
end

View file

@ -0,0 +1,2 @@
require 'action_web_service/dispatcher/abstract'
require 'action_web_service/dispatcher/action_controller_dispatcher'

View file

@ -0,0 +1,201 @@
require 'benchmark'
module ActionWebService # :nodoc:
module Dispatcher # :nodoc:
class DispatcherError < ActionWebService::ActionWebServiceError # :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.send(:include, ActionWebService::Dispatcher::InstanceMethods)
end
module InstanceMethods # :nodoc:
private
def invoke_web_service_request(protocol_request)
invocation = web_service_invocation(protocol_request)
if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
xmlrpc_multicall_invoke(invocation)
else
web_service_invoke(invocation)
end
end
def web_service_direct_invoke(invocation)
@method_params = invocation.method_ordered_params
arity = method(invocation.api_method.name).arity rescue 0
if arity < 0 || arity > 0
params = @method_params
else
params = []
end
web_service_filtered_invoke(invocation, params)
end
def web_service_delegated_invoke(invocation)
web_service_filtered_invoke(invocation, invocation.method_ordered_params)
end
def web_service_filtered_invoke(invocation, params)
cancellation_reason = nil
return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x|
cancellation_reason = x
end
if cancellation_reason
raise(DispatcherError, "request canceled: #{cancellation_reason}")
end
return_value
end
def web_service_invoke(invocation)
case web_service_dispatching_mode
when :direct
return_value = web_service_direct_invoke(invocation)
when :delegated, :layered
return_value = web_service_delegated_invoke(invocation)
end
web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value)
end
def xmlrpc_multicall_invoke(invocations)
responses = []
invocations.each do |invocation|
if invocation.is_a?(Hash)
responses << invocation
next
end
begin
case web_service_dispatching_mode
when :direct
return_value = web_service_direct_invoke(invocation)
when :delegated, :layered
return_value = web_service_delegated_invoke(invocation)
end
api_method = invocation.api_method
if invocation.api.has_api_method?(api_method.name)
return_value = api_method.cast_returns(return_value)
end
responses << [return_value]
rescue Exception => e
responses << { 'faultCode' => 3, 'faultString' => e.message }
end
end
invocation = invocations[0]
invocation.protocol.encode_response('system.multicall', responses, nil, invocation.protocol_options)
end
def web_service_invocation(request, level = 0)
public_method_name = request.method_name
invocation = Invocation.new
invocation.protocol = request.protocol
invocation.protocol_options = request.protocol_options
invocation.service_name = request.service_name
if web_service_dispatching_mode == :layered
case invocation.protocol
when Protocol::Soap::SoapProtocol
soap_action = request.protocol_options[:soap_action]
if soap_action && soap_action =~ /^\/\w+\/(\w+)\//
invocation.service_name = $1
end
when Protocol::XmlRpc::XmlRpcProtocol
if request.method_name =~ /^([^\.]+)\.(.*)$/
public_method_name = $2
invocation.service_name = $1
end
end
end
if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol
if public_method_name == 'multicall' && invocation.service_name == 'system'
if level > 0
raise(DispatcherError, "Recursive system.multicall invocations not allowed")
end
multicall = request.method_params.dup
unless multicall.is_a?(Array) && multicall[0].is_a?(Array)
raise(DispatcherError, "Malformed multicall (expected array of Hash elements)")
end
multicall = multicall[0]
return multicall.map do |item|
raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash)
raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName')
method_name = item['methodName']
params = item.has_key?('params') ? item['params'] : []
multicall_request = request.dup
multicall_request.method_name = method_name
multicall_request.method_params = params
begin
web_service_invocation(multicall_request, level + 1)
rescue Exception => e
{'faultCode' => 4, 'faultMessage' => e.message}
end
end
end
end
case web_service_dispatching_mode
when :direct
invocation.api = self.class.web_service_api
invocation.service = self
when :delegated, :layered
invocation.service = web_service_object(invocation.service_name)
invocation.api = invocation.service.class.web_service_api
end
if invocation.api.nil?
raise(DispatcherError, "no API attached to #{invocation.service.class}")
end
invocation.protocol.register_api(invocation.api)
request.api = invocation.api
if invocation.api.has_public_api_method?(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 = invocation.api.default_api_method_instance
end
end
if invocation.service.nil?
raise(DispatcherError, "no service available for service name #{invocation.service_name}")
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})")
end
request.api_method = invocation.api_method
begin
invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup)
rescue
logger.warn "Casting of method parameters failed" unless logger.nil?
invocation.method_ordered_params = request.method_params
end
request.method_params = invocation.method_ordered_params
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
def web_service_create_response(protocol, protocol_options, api, api_method, return_value)
if api.has_api_method?(api_method.name)
return_type = api_method.returns ? api_method.returns[0] : nil
return_value = api_method.cast_returns(return_value)
else
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
end
protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options)
end
class Invocation # :nodoc:
attr_accessor :protocol
attr_accessor :protocol_options
attr_accessor :service_name
attr_accessor :api
attr_accessor :api_method
attr_accessor :method_ordered_params
attr_accessor :method_named_params
attr_accessor :service
end
end
end
end

View file

@ -0,0 +1,380 @@
require 'benchmark'
require 'builder/xmlmarkup'
module ActionWebService # :nodoc:
module Dispatcher # :nodoc:
module ActionController # :nodoc:
def self.append_features(base) # :nodoc:
super
class << base
include ClassMethods
alias_method :inherited_without_action_controller, :inherited
alias_method :inherited, :inherited_with_action_controller
end
base.class_eval do
alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke
end
base.add_web_service_api_callback do |klass, api|
if klass.web_service_dispatching_mode == :direct
klass.class_eval 'def api; dispatch_web_service_request; end'
end
end
base.add_web_service_definition_callback do |klass, name, info|
if klass.web_service_dispatching_mode == :delegated
klass.class_eval "def #{name}; dispatch_web_service_request; end"
elsif klass.web_service_dispatching_mode == :layered
klass.class_eval 'def api; dispatch_web_service_request; end'
end
end
base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods)
end
module ClassMethods # :nodoc:
def inherited_with_action_controller(child)
inherited_without_action_controller(child)
child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction)
end
end
module InstanceMethods # :nodoc:
private
def dispatch_web_service_request
exception = nil
begin
ws_request = discover_web_service_request(request)
rescue Exception => e
exception = e
end
if ws_request
ws_response = nil
exception = nil
bm = Benchmark.measure do
begin
ws_response = invoke_web_service_request(ws_request)
rescue Exception => e
exception = e
end
end
log_request(ws_request, request.raw_post)
if exception
log_error(exception) unless logger.nil?
send_web_service_error_response(ws_request, exception)
else
send_web_service_response(ws_response, bm.real)
end
else
exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
log_error(exception) unless logger.nil?
send_web_service_error_response(ws_request, exception)
end
rescue Exception => e
log_error(e) unless logger.nil?
send_web_service_error_response(ws_request, e)
end
def send_web_service_response(ws_response, elapsed=nil)
log_response(ws_response, elapsed)
options = { :type => ws_response.content_type, :disposition => 'inline' }
send_data(ws_response.body, options)
end
def send_web_service_error_response(ws_request, exception)
if ws_request
unless self.class.web_service_exception_reporting
exception = DispatcherError.new("Internal server error (exception raised)")
end
api_method = ws_request.api_method
public_method_name = api_method ? api_method.public_name : ws_request.method_name
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(Exception, 0)
ws_response = ws_request.protocol.encode_response(public_method_name + 'Response', exception, return_type, ws_request.protocol_options)
send_web_service_response(ws_response)
else
if self.class.web_service_exception_reporting
message = exception.message
backtrace = "\nBacktrace:\n#{exception.backtrace.join("\n")}"
else
message = "Exception raised"
backtrace = ""
end
render_text("Internal protocol error: #{message}#{backtrace}", "500 Internal Protocol Error")
end
end
def web_service_direct_invoke(invocation)
invocation.method_named_params.each do |name, value|
params[name] = value
end
params['action'] = invocation.api_method.name.to_s
if before_action == false
raise(DispatcherError, "Method filtered")
end
return_value = web_service_direct_invoke_without_controller(invocation)
after_action
return_value
end
def log_request(ws_request, body)
unless logger.nil?
name = ws_request.method_name
api_method = ws_request.api_method
params = ws_request.method_params
if api_method && api_method.expects
params = api_method.expects.zip(params).map{ |type, param| "#{type.name}=>#{param.inspect}" }
else
params = params.map{ |param| param.inspect }
end
service = ws_request.service_name
logger.debug("\nWeb Service Request: #{name}(#{params.join(", ")}) Entrypoint: #{service}")
logger.debug(indent(body))
end
end
def log_response(ws_response, elapsed=nil)
unless logger.nil?
elapsed = (elapsed ? " (%f):" % elapsed : ":")
logger.debug("\nWeb Service Response" + elapsed + " => #{ws_response.return_value.inspect}")
logger.debug(indent(ws_response.body))
end
end
def indent(body)
body.split(/\n/).map{|x| " #{x}"}.join("\n")
end
end
module WsdlAction # :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
options = { :type => 'text/xml', :disposition => 'inline' }
send_data(to_wsdl, options)
rescue Exception => e
log_error(e) unless logger.nil?
end
when :post
render_text('POST not supported', '500 POST not supported')
end
end
private
def base_uri
host = request.env['HTTP_HOST'] || request.env['SERVER_NAME'] || 'localhost'
relative_url_root = request.relative_url_root
scheme = request.ssl? ? 'https' : 'http'
'%s://%s%s/%s/' % [scheme, host, relative_url_root, self.class.controller_path]
end
def to_wsdl
xml = ''
dispatching_mode = web_service_dispatching_mode
global_service_name = wsdl_service_name
namespace = wsdl_namespace || 'urn:ActionWebService'
soap_action_base = "/#{controller_name}"
marshaler = ActionWebService::Protocol::Soap::SoapMarshaler.new(namespace)
apis = {}
case dispatching_mode
when :direct
api = self.class.web_service_api
web_service_name = controller_class_name.sub(/Controller$/, '').underscore
apis[web_service_name] = [api, register_api(api, marshaler)]
when :delegated, :layered
self.class.web_services.each do |web_service_name, info|
service = web_service_object(web_service_name)
api = service.class.web_service_api
apis[web_service_name] = [api, register_api(api, marshaler)]
end
end
custom_types = []
apis.values.each do |api, bindings|
bindings.each do |b|
custom_types << b unless custom_types.include?(b)
end
end
xm = Builder::XmlMarkup.new(:target => xml, :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
# Generate XSD
if custom_types.size > 0
xm.types do
xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do
custom_types.each do |binding|
case
when binding.type.array?
xm.xsd(:complexType, 'name' => binding.type_name) do
xm.xsd(:complexContent) do
xm.xsd(:restriction, 'base' => 'soapenc:Array') do
xm.xsd(:attribute, 'ref' => 'soapenc:arrayType',
'wsdl:arrayType' => binding.element_binding.qualified_type_name('typens') + '[]')
end
end
end
when binding.type.structured?
xm.xsd(:complexType, 'name' => binding.type_name) do
xm.xsd(:all) do
binding.type.each_member do |name, type|
b = marshaler.register_type(type)
xm.xsd(:element, 'name' => name, 'type' => b.qualified_type_name('typens'))
end
end
end
end
end
end
end
end
# APIs
apis.each do |api_name, values|
api = values[0]
api.api_methods.each do |name, method|
gen = lambda do |msg_name, direction|
xm.message('name' => message_name_for(api_name, msg_name)) do
sym = nil
if direction == :out
returns = method.returns
if returns
binding = marshaler.register_type(returns[0])
xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens'))
end
else
expects = method.expects
expects.each do |type|
binding = marshaler.register_type(type)
xm.part('name' => type.name, 'type' => binding.qualified_type_name('typens'))
end if expects
end
end
end
public_name = method.public_name
gen.call(public_name, :in)
gen.call("#{public_name}Response", :out)
end
# Port
port_name = port_name_for(global_service_name, api_name)
xm.portType('name' => port_name) do
api.api_methods.each do |name, method|
xm.operation('name' => method.public_name) do
xm.input('message' => "typens:" + message_name_for(api_name, method.public_name))
xm.output('message' => "typens:" + message_name_for(api_name, "#{method.public_name}Response"))
end
end
end
# Bind it
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, method|
xm.operation('name' => method.public_name) do
case web_service_dispatching_mode
when :direct
soap_action = soap_action_base + "/api/" + method.public_name
when :delegated, :layered
soap_action = soap_action_base \
+ "/" + api_name.to_s \
+ "/" + method.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 it
xm.service('name' => "#{global_service_name}Service") do
apis.each do |api_name, values|
port_name = port_name_for(global_service_name, api_name)
binding_name = binding_name_for(global_service_name, api_name)
case web_service_dispatching_mode
when :direct, :layered
binding_target = 'api'
when :delegated
binding_target = api_name.to_s
end
xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do
xm.soap(:address, 'location' => "#{base_uri}#{binding_target}")
end
end
end
end
end
def port_name_for(global_service, service)
"#{global_service}#{service.to_s.camelize}Port"
end
def binding_name_for(global_service, service)
"#{global_service}#{service.to_s.camelize}Binding"
end
def message_name_for(api_name, message_name)
mode = web_service_dispatching_mode
if mode == :layered || mode == :delegated
api_name.to_s + '-' + message_name
else
message_name
end
end
def register_api(api, marshaler)
bindings = {}
traverse_custom_types(api, marshaler, bindings) do |binding|
bindings[binding] = nil unless bindings.has_key?(binding)
element_binding = binding.element_binding
bindings[element_binding] = nil if element_binding && !bindings.has_key?(element_binding)
end
bindings.keys
end
def traverse_custom_types(api, marshaler, bindings, &block)
api.api_methods.each do |name, method|
expects, returns = method.expects, method.returns
expects.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if expects
returns.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if returns
end
end
def traverse_type(marshaler, type, bindings, &block)
binding = marshaler.register_type(type)
return if bindings.has_key?(binding)
bindings[binding] = nil
yield binding
if type.array?
yield marshaler.register_type(type.element_type)
type = type.element_type
end
type.each_member{ |name, type| traverse_type(marshaler, type, bindings, &block) } if type.structured?
end
end
end
end
end

View file

@ -0,0 +1,205 @@
module ActionWebService # :nodoc:
module Invocation # :nodoc:
class InvocationError < ActionWebService::ActionWebServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
base.send(:include, ActionWebService::Invocation::InstanceMethods)
end
# Invocation interceptors provide a means to execute custom code before
# and after method invocations on ActionWebService::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 < ActionWebService::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(method_name, params, &block)
return if before_invocation(method_name, params, &block) == false
return_value = perform_invocation_without_interception(method_name, params)
after_invocation(method_name, params, return_value)
return_value
end
def perform_invocation(method_name, params)
send(method_name, *params)
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
end
end

View file

@ -0,0 +1,4 @@
require 'action_web_service/protocol/abstract'
require 'action_web_service/protocol/discovery'
require 'action_web_service/protocol/soap_protocol'
require 'action_web_service/protocol/xmlrpc_protocol'

View file

@ -0,0 +1,112 @@
module ActionWebService # :nodoc:
module Protocol # :nodoc:
class ProtocolError < ActionWebServiceError # :nodoc:
end
class AbstractProtocol # :nodoc:
def setup(controller)
end
def decode_action_pack_request(action_pack_request)
end
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
klass = options[:request_class] || SimpleActionPackRequest
request = klass.new
request.request_parameters['action'] = service_name.to_s
request.env['RAW_POST_DATA'] = raw_body
request.env['REQUEST_METHOD'] = 'POST'
request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
request
end
def decode_request(raw_request, service_name, protocol_options={})
end
def encode_request(method_name, params, param_types)
end
def decode_response(raw_response)
end
def encode_response(method_name, return_value, return_type, protocol_options={})
end
def protocol_client(api, protocol_name, endpoint_uri, options)
end
def register_api(api)
end
end
class Request # :nodoc:
attr :protocol
attr_accessor :method_name
attr_accessor :method_params
attr :service_name
attr_accessor :api
attr_accessor :api_method
attr :protocol_options
def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil)
@protocol = protocol
@method_name = method_name
@method_params = method_params
@service_name = service_name
@api = api
@api_method = api_method
@protocol_options = protocol_options || {}
end
end
class Response # :nodoc:
attr :body
attr :content_type
attr :return_value
def initialize(body, content_type, return_value)
@body = body
@content_type = content_type
@return_value = return_value
end
end
class SimpleActionPackRequest < ActionController::AbstractRequest # :nodoc:
def initialize
@env = {}
@qparams = {}
@rparams = {}
@cookies = {}
reset_session
end
def query_parameters
@qparams
end
def request_parameters
@rparams
end
def env
@env
end
def host
''
end
def cookies
@cookies
end
def session
@session
end
def reset_session
@session = {}
end
end
end
end

View file

@ -0,0 +1,37 @@
module ActionWebService # :nodoc:
module Protocol # :nodoc:
module Discovery # :nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods)
end
module ClassMethods # :nodoc:
def register_protocol(klass)
write_inheritable_array("web_service_protocols", [klass])
end
end
module InstanceMethods # :nodoc:
private
def discover_web_service_request(action_pack_request)
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
protocol = protocol.create(self)
request = protocol.decode_action_pack_request(action_pack_request)
return request unless request.nil?
end
nil
end
def create_web_service_client(api, protocol_name, endpoint_uri, options)
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
protocol = protocol.create(self)
client = protocol.protocol_client(api, protocol_name, endpoint_uri, options)
return client unless client.nil?
end
nil
end
end
end
end
end

View file

@ -0,0 +1,176 @@
require 'action_web_service/protocol/soap_protocol/marshaler'
require 'soap/streamHandler'
require 'action_web_service/client/soap_client'
module ActionWebService # :nodoc:
module API # :nodoc:
class Base # :nodoc:
def self.soap_client(endpoint_uri, options={})
ActionWebService::Client::Soap.new self, endpoint_uri, options
end
end
end
module Protocol # :nodoc:
module Soap # :nodoc:
def self.included(base)
base.register_protocol(SoapProtocol)
base.class_inheritable_option(:wsdl_service_name)
base.class_inheritable_option(:wsdl_namespace)
end
class SoapProtocol < AbstractProtocol # :nodoc:
AWSEncoding = 'UTF-8'
XSDEncoding = 'UTF8'
attr :marshaler
def initialize(namespace=nil)
namespace ||= 'urn:ActionWebService'
@marshaler = SoapMarshaler.new namespace
end
def self.create(controller)
SoapProtocol.new(controller.wsdl_namespace)
end
def decode_action_pack_request(action_pack_request)
return nil unless soap_action = has_valid_soap_action?(action_pack_request)
service_name = action_pack_request.parameters['action']
input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE'])
protocol_options = {
:soap_action => soap_action,
:charset => input_encoding
}
decode_request(action_pack_request.raw_post, service_name, protocol_options)
end
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
request = super
request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name]
request
end
def decode_request(raw_request, service_name, protocol_options={})
envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset])
unless envelope
raise ProtocolError, "Failed to parse SOAP request message"
end
request = envelope.body.request
method_name = request.elename.name
params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) }
Request.new(self, method_name, params, service_name, nil, nil, protocol_options)
end
def encode_request(method_name, params, param_types)
param_types.each{ |type| marshaler.register_type(type) } if param_types
qname = XSD::QName.new(marshaler.namespace, method_name)
param_def = []
if param_types
params = param_types.zip(params).map do |type, param|
param_def << ['in', type.name, marshaler.lookup_type(type).mapping]
[type.name, marshaler.ruby_to_soap(param)]
end
else
params = []
end
request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def)
request.set_param(params)
envelope = create_soap_envelope(request)
SOAP::Processor.marshal(envelope)
end
def decode_response(raw_response)
envelope = SOAP::Processor.unmarshal(raw_response)
unless envelope
raise ProtocolError, "Failed to parse SOAP request message"
end
method_name = envelope.body.request.elename.name
return_value = envelope.body.response
return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil?
[method_name, return_value]
end
def encode_response(method_name, return_value, return_type, protocol_options={})
if return_type
return_binding = marshaler.register_type(return_type)
marshaler.annotate_arrays(return_binding, return_value)
end
qname = XSD::QName.new(marshaler.namespace, method_name)
if return_value.nil?
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
else
if return_value.is_a?(Exception)
detail = SOAP::Mapping::SOAPException.new(return_value)
response = SOAP::SOAPFault.new(
SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']),
SOAP::SOAPString.new(return_value.to_s),
SOAP::SOAPString.new(self.class.name),
marshaler.ruby_to_soap(detail))
else
if return_type
param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]]
response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def)
response.retval = marshaler.ruby_to_soap(return_value)
else
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
end
end
end
envelope = create_soap_envelope(response)
# FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only
# reads target encoding from the XSD::Charset.encoding variable.
# This is required to ensure $KCODE strings are converted
# correctly to UTF-8 for any values of $KCODE.
previous_encoding = XSD::Charset.encoding
XSD::Charset.encoding = XSDEncoding
response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding)
XSD::Charset.encoding = previous_encoding
Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value)
end
def protocol_client(api, protocol_name, endpoint_uri, options={})
return nil unless protocol_name == :soap
ActionWebService::Client::Soap.new(api, endpoint_uri, options)
end
def register_api(api)
api.api_methods.each do |name, method|
method.expects.each{ |type| marshaler.register_type(type) } if method.expects
method.returns.each{ |type| marshaler.register_type(type) } if method.returns
end
end
private
def has_valid_soap_action?(request)
return nil unless request.method == :post
soap_action = request.env['HTTP_SOAPACTION']
return nil unless soap_action
soap_action = soap_action.dup
soap_action.gsub!(/^"/, '')
soap_action.gsub!(/"$/, '')
soap_action.strip!
return nil if soap_action.empty?
soap_action
end
def create_soap_envelope(body)
header = SOAP::SOAPHeader.new
body = SOAP::SOAPBody.new(body)
SOAP::SOAPEnvelope.new(header, body)
end
def parse_charset(content_type)
return AWSEncoding if content_type.nil?
if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type
$1
else
AWSEncoding
end
end
end
end
end
end

View file

@ -0,0 +1,241 @@
require 'soap/mapping'
module ActionWebService
module Protocol
module Soap
# Workaround for SOAP4R return values changing
class Registry < SOAP::Mapping::Registry
if SOAP::Version >= "1.5.4"
def find_mapped_soap_class(obj_class)
return @map.instance_eval { @obj2soap[obj_class][0] }
end
def find_mapped_obj_class(soap_class)
return @map.instance_eval { @soap2obj[soap_class][0] }
end
end
end
class SoapMarshaler
attr :namespace
attr :registry
def initialize(namespace=nil)
@namespace = namespace || 'urn:ActionWebService'
@registry = Registry.new
@type2binding = {}
register_static_factories
end
def soap_to_ruby(obj)
SOAP::Mapping.soap2obj(obj, @registry)
end
def ruby_to_soap(obj)
soap = SOAP::Mapping.obj2soap(obj, @registry)
soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY
soap
end
def register_type(type)
return @type2binding[type] if @type2binding.has_key?(type)
if type.array?
array_mapping = @registry.find_mapped_soap_class(Array)
qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array')
element_type_binding = register_type(type.element_type)
@type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding)
elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil)
qname = mapping[2] ? mapping[2][:type] : nil
qname ||= soap_base_type_name(mapping[0])
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
else
qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name))
@registry.add(type.type_class,
SOAP::SOAPStruct,
typed_struct_factory(type.type_class),
{ :type => qname })
mapping = @registry.find_mapped_soap_class(type.type_class)
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
end
if type.structured?
type.each_member do |m_name, m_type|
register_type(m_type)
end
end
@type2binding[type]
end
alias :lookup_type :register_type
def annotate_arrays(binding, value)
if value.nil?
return
elsif binding.type.array?
mark_typed_array(value, binding.element_binding.qname)
if binding.element_binding.type.custom?
value.each do |element|
annotate_arrays(binding.element_binding, element)
end
end
elsif binding.type.structured?
binding.type.each_member do |name, type|
member_binding = register_type(type)
member_value = value.respond_to?('[]') ? value[name] : value.send(name)
annotate_arrays(member_binding, member_value) if type.custom?
end
end
end
private
def typed_struct_factory(type_class)
if Object.const_defined?('ActiveRecord')
if type_class.ancestors.include?(ActiveRecord::Base)
qname = XSD::QName.new(@namespace, soap_type_name(type_class.name))
type_class.instance_variable_set('@qname', qname)
return SoapActiveRecordStructFactory.new
end
end
SOAP::Mapping::Registry::TypedStructFactory
end
def mark_typed_array(array, qname)
(class << array; self; end).class_eval do
define_method(:arytype) do
qname
end
end
end
def soap_base_type_name(type)
xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' }
xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type
end
def soap_type_name(type_name)
type_name.gsub(/::/, '..')
end
def register_static_factories
@registry.add(ActionWebService::Base64,
SOAP::SOAPBase64,
SoapBase64Factory.new,
nil)
mapping = @registry.find_mapped_soap_class(ActionWebService::Base64)
@type2binding[ActionWebService::Base64] =
SoapBinding.new(self, SOAP::SOAPBase64::Type,
ActionWebService::Base64, mapping)
@registry.add(Array,
SOAP::SOAPArray,
SoapTypedArrayFactory.new,
nil)
end
end
class SoapBinding
attr :qname
attr :type
attr :mapping
attr :element_binding
def initialize(marshaler, qname, type, mapping, element_binding=nil)
@marshaler = marshaler
@qname = qname
@type = type
@mapping = mapping
@element_binding = element_binding
end
def type_name
@type.custom? ? @qname.name : nil
end
def qualified_type_name(ns=nil)
if @type.custom?
"#{ns ? ns : @qname.namespace}:#{@qname.name}"
else
ns = XSD::NS.new
ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag)
ns.assign(SOAP::EncodingNamespace, "soapenc")
xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')}
return ns.name(XSD::AnyTypeName) unless xsd_klass
ns.name(xsd_klass.const_get('Type'))
end
end
def eql?(other)
@qname == other.qname
end
alias :== :eql?
def hash
@qname.hash
end
end
class SoapActiveRecordStructFactory < SOAP::Mapping::Factory
def obj2soap(soap_class, obj, info, map)
unless obj.is_a?(ActiveRecord::Base)
return nil
end
soap_obj = soap_class.new(obj.class.instance_variable_get('@qname'))
obj.class.columns.each do |column|
key = column.name.to_s
value = obj.send(key)
soap_obj[key] = SOAP::Mapping._obj2soap(value, map)
end
soap_obj
end
def soap2obj(obj_class, node, info, map)
unless node.type == obj_class.instance_variable_get('@qname')
return false
end
obj = obj_class.new
node.each do |key, value|
obj[key] = value.data
end
obj.instance_variable_set('@new_record', false)
return true, obj
end
end
class SoapTypedArrayFactory < SOAP::Mapping::Factory
def obj2soap(soap_class, obj, info, map)
unless obj.respond_to?(:arytype)
return nil
end
soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype)
mark_marshalled_obj(obj, soap_obj)
obj.each do |item|
child = SOAP::Mapping._obj2soap(item, map)
soap_obj.add(child)
end
soap_obj
end
def soap2obj(obj_class, node, info, map)
return false
end
end
class SoapBase64Factory < SOAP::Mapping::Factory
def obj2soap(soap_class, obj, info, map)
unless obj.is_a?(ActionWebService::Base64)
return nil
end
return soap_class.new(obj)
end
def soap2obj(obj_class, node, info, map)
unless node.type == SOAP::SOAPBase64::Type
return false
end
return true, obj_class.new(node.string)
end
end
end
end
end

View file

@ -0,0 +1,97 @@
require 'xmlrpc/marshal'
require 'action_web_service/client/xmlrpc_client'
module XMLRPC # :nodoc:
class FaultException # :nodoc:
alias :message :faultString
end
end
module ActionWebService # :nodoc:
module API # :nodoc:
class Base # :nodoc:
def self.xmlrpc_client(endpoint_uri, options={})
ActionWebService::Client::XmlRpc.new self, endpoint_uri, options
end
end
end
module Protocol # :nodoc:
module XmlRpc # :nodoc:
def self.included(base)
base.register_protocol(XmlRpcProtocol)
end
class XmlRpcProtocol < AbstractProtocol # :nodoc:
def self.create(controller)
XmlRpcProtocol.new
end
def decode_action_pack_request(action_pack_request)
service_name = action_pack_request.parameters['action']
decode_request(action_pack_request.raw_post, service_name)
end
def decode_request(raw_request, service_name)
method_name, params = XMLRPC::Marshal.load_call(raw_request)
Request.new(self, method_name, params, service_name)
end
def encode_request(method_name, params, param_types)
if param_types
params = params.dup
param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) }
end
XMLRPC::Marshal.dump_call(method_name, *params)
end
def decode_response(raw_response)
[nil, XMLRPC::Marshal.load_response(raw_response)]
end
def encode_response(method_name, return_value, return_type, protocol_options={})
if return_value && return_type
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
end
return_value = false if return_value.nil?
raw_response = XMLRPC::Marshal.dump_response(return_value)
Response.new(raw_response, 'text/xml', return_value)
end
def protocol_client(api, protocol_name, endpoint_uri, options={})
return nil unless protocol_name == :xmlrpc
ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options)
end
def value_to_xmlrpc_wire_format(value, value_type)
if value_type.array?
value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) }
else
if value.is_a?(ActionWebService::Struct)
struct = {}
value.class.members.each do |name, type|
member_value = value[name]
next if member_value.nil?
struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type)
end
struct
elsif value.is_a?(ActiveRecord::Base)
struct = {}
value.attributes.each do |key, member_value|
next if member_value.nil?
struct[key.to_s] = member_value
end
struct
elsif value.is_a?(ActionWebService::Base64)
XMLRPC::Base64.new(value)
elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException)
XMLRPC::FaultException.new(2, value.message)
else
value
end
end
end
end
end
end
end

View file

@ -0,0 +1,284 @@
require 'benchmark'
require 'pathname'
module ActionWebService
module Scaffolding # :nodoc:
class ScaffoldingError < ActionWebServiceError # :nodoc:
end
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The
# generated scaffold actions have default views to let you enter the method parameters and view the
# results.
#
# Example:
#
# class ApiController < ActionController
# web_service_scaffold :invoke
# end
#
# This example generates an +invoke+ action in the +ApiController+ that you can navigate to from
# your browser, select the API method, enter its parameters, and perform the invocation.
#
# If you want to customize the default views, create the following views in "app/views":
#
# * <tt>action_name/methods.rhtml</tt>
# * <tt>action_name/parameters.rhtml</tt>
# * <tt>action_name/result.rhtml</tt>
# * <tt>action_name/layout.rhtml</tt>
#
# Where <tt>action_name</tt> is the name of the action you gave to ClassMethods#web_service_scaffold.
#
# You can use the default views in <tt>RAILS_DIR/lib/action_web_service/templates/scaffolds</tt> as
# a guide.
module ClassMethods
# Generates web service invocation scaffolding for the current controller. The given action name
# can then be used as the entry point for invoking API methods from a web browser.
def web_service_scaffold(action_name)
add_template_helper(Helpers)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{action_name}
if request.method == :get
setup_invocation_assigns
render_invocation_scaffold 'methods'
end
end
def #{action_name}_method_params
if request.method == :get
setup_invocation_assigns
render_invocation_scaffold 'parameters'
end
end
def #{action_name}_submit
if request.method == :post
setup_invocation_assigns
protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap
case protocol_name
when :soap
@protocol = Protocol::Soap::SoapProtocol.create(self)
when :xmlrpc
@protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self)
end
bm = Benchmark.measure do
@protocol.register_api(@scaffold_service.api)
post_params = params['method_params'] ? params['method_params'].dup : nil
params = []
@scaffold_method.expects.each_with_index do |spec, i|
params << post_params[i.to_s]
end if @scaffold_method.expects
params = @scaffold_method.cast_expects(params)
method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name)
@method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects)
new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml)
prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name)
@request = new_request
if @scaffold_container.dispatching_mode != :direct
request.parameters['action'] = @scaffold_service.name
end
dispatch_web_service_request
@method_response_xml = @response.body
method_name, obj = @protocol.decode_response(@method_response_xml)
return if handle_invocation_exception(obj)
@method_return_value = @scaffold_method.cast_returns(obj)
end
@method_elapsed = bm.real
add_instance_variables_to_assigns
reset_invocation_response
render_invocation_scaffold 'result'
end
end
private
def setup_invocation_assigns
@scaffold_class = self.class
@scaffold_action_name = "#{action_name}"
@scaffold_container = WebServiceModel::Container.new(self)
if params['service'] && params['method']
@scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] }
@scaffold_method = @scaffold_service.api_methods[params['method']]
end
add_instance_variables_to_assigns
end
def render_invocation_scaffold(action)
customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
default_template = scaffold_path(action)
if template_exists?(customized_template)
content = @template.render_file(customized_template)
else
content = @template.render_file(default_template, false)
end
@template.instance_variable_set("@content_for_layout", content)
if self.active_layout.nil?
render_file(scaffold_path("layout"))
else
render_file(self.active_layout, "200 OK", true)
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def reset_invocation_response
erase_render_results
@response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => [])
end
def public_method_name(service_name, method_name)
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
service_name + '.' + method_name
else
method_name
end
end
def prepare_request(new_request, service_name, method_name)
new_request.parameters.update(request.parameters)
request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) }
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol)
new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}"
end
end
def handle_invocation_exception(obj)
exception = nil
if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception)
exception = obj.detail.cause
elsif obj.is_a?(XMLRPC::FaultException)
exception = obj
end
return unless exception
reset_invocation_response
rescue_action(exception)
true
end
end_eval
end
end
module Helpers # :nodoc:
def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false)
if type.array?
return content_tag('em', "Typed array input fields not supported yet (#{type.name})")
end
if type.structured?
return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured
parameters = ""
type.each_member do |member_name, member_type|
label = method_parameter_label(member_name, member_type)
nested_content = method_parameter_input_fields(
method,
member_type,
"#{field_name_base}[#{idx}][#{member_name}]",
idx,
true)
if member_type.custom?
parameters << content_tag('li', label)
parameters << content_tag('ul', nested_content)
else
parameters << content_tag('li', label + ' ' + nested_content)
end
end
content_tag('ul', parameters)
else
# If the data source was structured previously we already have the index set
field_name_base = "#{field_name_base}[#{idx}]" unless was_structured
case type.type
when :int
text_field_tag "#{field_name_base}"
when :string
text_field_tag "#{field_name_base}"
when :base64
text_area_tag "#{field_name_base}", nil, :size => "40x5"
when :bool
radio_button_tag("#{field_name_base}", "true") + " True" +
radio_button_tag("#{field_name_base}", "false") + "False"
when :float
text_field_tag "#{field_name_base}"
when :time, :datetime
time = Time.now
i = 0
%w|year month day hour minute second|.map do |name|
i += 1
send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
end.join
when :date
date = Date.today
i = 0
%w|year month day|.map do |name|
i += 1
send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
end.join
end
end
end
def method_parameter_label(name, type)
name.to_s.capitalize + ' (' + type.human_name(false) + ')'
end
def service_method_list(service)
action = @scaffold_action_name + '_method_params'
methods = service.api_methods_full.map do |desc, name|
content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name))
end
content_tag("ul", methods.join("\n"))
end
end
module WebServiceModel # :nodoc:
class Container # :nodoc:
attr :services
attr :dispatching_mode
def initialize(real_container)
@real_container = real_container
@dispatching_mode = @real_container.class.web_service_dispatching_mode
@services = []
if @dispatching_mode == :direct
@services << Service.new(@real_container.controller_name, @real_container)
else
@real_container.class.web_services.each do |name, obj|
@services << Service.new(name, @real_container.instance_eval{ web_service_object(name) })
end
end
end
end
class Service # :nodoc:
attr :name
attr :object
attr :api
attr :api_methods
attr :api_methods_full
def initialize(name, real_service)
@name = name.to_s
@object = real_service
@api = @object.class.web_service_api
if @api.nil?
raise ScaffoldingError, "No web service API attached to #{object.class}"
end
@api_methods = {}
@api_methods_full = []
@api.api_methods.each do |name, method|
@api_methods[method.public_name.to_s] = method
@api_methods_full << [method.to_s, method.public_name.to_s]
end
end
def to_s
self.name.camelize
end
end
end
end
end

View file

@ -0,0 +1,68 @@
module ActionWebService
# To send structured types across the wire, derive from ActionWebService::Struct,
# and use +member+ to declare structure members.
#
# ActionWebService::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 < ActionWebService::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 in method
# signatures.
class Struct
# Action WebService Struct subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load.
include Reloadable::Subclasses
# If a Hash is given as argument to an ActionWebService::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
# Iterates through each member
def each_pair(&block)
self.class.members.each do |name, type|
yield name, self.__send__(name)
end
end
class << self
# Creates a structure member with the specified +name+ and +type+. Generates
# accessor methods for reading and writing the member value.
def member(name, type)
name = name.to_sym
type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0)
write_inheritable_hash("struct_members", name => 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
def member_type(name) # :nodoc:
members[name.to_sym]
end
end
end
end

View file

@ -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

View file

@ -0,0 +1,222 @@
module ActionWebService # :nodoc:
# Action Web Service supports the following base types in a signature:
#
# [<tt>:int</tt>] Represents an integer value, will be cast to an integer using <tt>Integer(value)</tt>
# [<tt>:string</tt>] Represents a string value, will be cast to an string using the <tt>to_s</tt> method on an object
# [<tt>:base64</tt>] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller
# [<tt>:bool</tt>] Represents a boolean value, whatever is passed will be cast to boolean (<tt>true</tt>, '1', 'true', 'y', 'yes' are taken to represent true; <tt>false</tt>, '0', 'false', 'n', 'no' and <tt>nil</tt> represent false)
# [<tt>:float</tt>] Represents a floating point value, will be cast to a float using <tt>Float(value)</tt>
# [<tt>:time</tt>] Represents a timestamp, will be cast to a <tt>Time</tt> object
# [<tt>:datetime</tt>] Represents a timestamp, will be cast to a <tt>DateTime</tt> object
# [<tt>:date</tt>] Represents a date, will be cast to a <tt>Date</tt> object
#
# For structured types, you'll need to pass in the Class objects of
# ActionWebService::Struct and ActiveRecord::Base derivatives.
module SignatureTypes
def canonical_signature(signature) # :nodoc:
return nil if signature.nil?
unless signature.is_a?(Array)
raise(ActionWebServiceError, "Expected signature to be an Array")
end
i = -1
signature.map{ |spec| canonical_signature_entry(spec, i += 1) }
end
def canonical_signature_entry(spec, i) # :nodoc:
orig_spec = spec
name = "param#{i}"
if spec.is_a?(Hash)
name, spec = spec.keys.first, spec.values.first
end
type = spec
if spec.is_a?(Array)
ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name)
else
type = canonical_type(type)
if type.is_a?(Symbol)
BaseType.new(orig_spec, type, name)
else
StructuredType.new(orig_spec, type, name)
end
end
end
def canonical_type(type) # :nodoc:
type_name = symbol_name(type) || class_to_type_name(type)
type = type_name || type
return canonical_type_name(type) if type.is_a?(Symbol)
type
end
def canonical_type_name(name) # :nodoc:
name = name.to_sym
case name
when :int, :integer, :fixnum, :bignum
:int
when :string, :text
:string
when :base64, :binary
:base64
when :bool, :boolean
:bool
when :float, :double
:float
when :time, :timestamp
:time
when :datetime
:datetime
when :date
:date
else
raise(TypeError, "#{name} is not a valid base type")
end
end
def canonical_type_class(type) # :nodoc:
type = canonical_type(type)
type.is_a?(Symbol) ? type_name_to_class(type) : type
end
def symbol_name(name) # :nodoc:
return name.to_sym if name.is_a?(Symbol) || name.is_a?(String)
nil
end
def class_to_type_name(klass) # :nodoc:
klass = klass.class unless klass.is_a?(Class)
if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass)
:int
elsif klass == String
:string
elsif klass == Base64
:base64
elsif klass == TrueClass || klass == FalseClass
:bool
elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass)
:float
elsif klass == Time
:time
elsif klass == DateTime
:datetime
elsif klass == Date
:date
else
nil
end
end
def type_name_to_class(name) # :nodoc:
case canonical_type_name(name)
when :int
Integer
when :string
String
when :base64
Base64
when :bool
TrueClass
when :float
Float
when :time
Time
when :date
Date
when :datetime
DateTime
else
nil
end
end
def derived_from?(ancestor, child) # :nodoc:
child.ancestors.include?(ancestor)
end
module_function :type_name_to_class
module_function :class_to_type_name
module_function :symbol_name
module_function :canonical_type_class
module_function :canonical_type_name
module_function :canonical_type
module_function :canonical_signature_entry
module_function :canonical_signature
module_function :derived_from?
end
class BaseType # :nodoc:
include SignatureTypes
attr :spec
attr :type
attr :type_class
attr :name
def initialize(spec, type, name)
@spec = spec
@type = canonical_type(type)
@type_class = canonical_type_class(@type)
@name = name
end
def custom?
false
end
def array?
false
end
def structured?
false
end
def human_name(show_name=true)
type_type = array? ? element_type.type.to_s : self.type.to_s
str = array? ? (type_type + '[]') : type_type
show_name ? (str + " " + name.to_s) : str
end
end
class ArrayType < BaseType # :nodoc:
attr :element_type
def initialize(spec, element_type, name)
super(spec, Array, name)
@element_type = element_type
end
def custom?
true
end
def array?
true
end
end
class StructuredType < BaseType # :nodoc:
def each_member
if @type_class.respond_to?(:members)
@type_class.members.each do |name, type|
yield name, type
end
elsif @type_class.respond_to?(:columns)
i = -1
@type_class.columns.each do |column|
yield column.name, canonical_signature_entry(column.type, i += 1)
end
end
end
def custom?
true
end
def structured?
true
end
end
class Base64 < String # :nodoc:
end
end

View file

@ -0,0 +1,65 @@
<html>
<head>
<title><%= @scaffold_class.wsdl_service_name %> Web Service</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
</style>
</head>
<body>
<%= @content_for_layout %>
</body>
</html>

View file

@ -0,0 +1,6 @@
<% @scaffold_container.services.each do |service| %>
<h4>API Methods for <%= service %></h4>
<%= service_method_list(service) %>
<% end %>

View file

@ -0,0 +1,29 @@
<h4>Method Invocation Details for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
<%= form_tag :action => @scaffold_action_name + '_submit' %>
<%= hidden_field_tag "service", @scaffold_service.name %>
<%= hidden_field_tag "method", @scaffold_method.public_name %>
<p>
<label for="protocol">Protocol:</label><br />
<%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], @params['protocol']) %>
</p>
<% if @scaffold_method.expects %>
<strong>Method Parameters:</strong><br />
<% @scaffold_method.expects.each_with_index do |type, i| %>
<p>
<label for="method_params[<%= i %>]"><%= method_parameter_label(type.name, type) %> </label><br />
<%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %>
</p>
<% end %>
<% end %>
<%= submit_tag "Invoke" %>
<%= end_form_tag %>
<p>
<%= link_to "Back", :action => @scaffold_action_name %>
</p>

View file

@ -0,0 +1,30 @@
<h4>Method Invocation Result for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
<p>
Invocation took <tt><%= '%f' % @method_elapsed %></tt> seconds
</p>
<p>
<strong>Return Value:</strong><br />
<pre>
<%= h @method_return_value.inspect %>
</pre>
</p>
<p>
<strong>Request XML:</strong><br />
<pre>
<%= h @method_request_xml %>
</pre>
</p>
<p>
<strong>Response XML:</strong><br />
<pre>
<%= h @method_response_xml %>
</pre>
</p>
<p>
<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %>
</p>

View file

@ -0,0 +1,110 @@
require 'test/unit'
module Test # :nodoc:
module Unit # :nodoc:
class TestCase # :nodoc:
private
# invoke the specified API method
def invoke_direct(method_name, *args)
prepare_request('api', 'api', method_name, *args)
@controller.process(@request, @response)
decode_rpc_response
end
alias_method :invoke, :invoke_direct
# invoke the specified API method on the specified service
def invoke_delegated(service_name, method_name, *args)
prepare_request(service_name.to_s, service_name, method_name, *args)
@controller.process(@request, @response)
decode_rpc_response
end
# invoke the specified layered API method on the correct service
def invoke_layered(service_name, method_name, *args)
prepare_request('api', service_name, method_name, *args)
@controller.process(@request, @response)
decode_rpc_response
end
# ---------------------- internal ---------------------------
def prepare_request(action, service_name, api_method_name, *args)
@request.recycle!
@request.request_parameters['action'] = action
@request.env['REQUEST_METHOD'] = 'POST'
@request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
@request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args)
case protocol
when ActionWebService::Protocol::Soap::SoapProtocol
soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}"
@request.env['HTTP_SOAPACTION'] = soap_action
when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
@request.env.delete('HTTP_SOAPACTION')
end
end
def encode_rpc_call(service_name, api_method_name, *args)
case @controller.web_service_dispatching_mode
when :direct
api = @controller.class.web_service_api
when :delegated, :layered
api = @controller.web_service_object(service_name.to_sym).class.web_service_api
end
protocol.register_api(api)
method = api.api_methods[api_method_name.to_sym]
raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" unless args.length == method.expects.length
protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects)
end
def decode_rpc_response
public_method_name, return_value = protocol.decode_response(@response.body)
exception = is_exception?(return_value)
raise exception if exception
return_value
end
def public_method_name(service_name, api_method_name)
public_name = service_api(service_name).public_api_method_name(api_method_name)
if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
'%s.%s' % [service_name.to_s, public_name]
else
public_name
end
end
def service_api(service_name)
case @controller.web_service_dispatching_mode
when :direct
@controller.class.web_service_api
when :delegated, :layered
@controller.web_service_object(service_name.to_sym).class.web_service_api
end
end
def protocol
if @protocol.nil?
@protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
else
case @protocol
when :xmlrpc
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller)
when :soap
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
else
@protocol
end
end
end
def is_exception?(obj)
case protocol
when :soap, ActionWebService::Protocol::Soap::SoapProtocol
(obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \
obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil
when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
obj.is_a?(XMLRPC::FaultException) ? obj : nil
end
end
end
end
end

View file

@ -0,0 +1,9 @@
module ActionWebService
module VERSION #:nodoc:
MAJOR = 1
MINOR = 1
TINY = 6
STRING = [MAJOR, MINOR, TINY].join('.')
end
end