Install ActionWebService ported to Rails 2.3

This commit is contained in:
Eric Allen 2009-12-07 12:51:01 -05:00
parent 3c94609583
commit 804d59c542
80 changed files with 8731 additions and 2 deletions

View file

@ -17,8 +17,6 @@ end
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
# config.frameworks -= [ :action_web_service, :action_mailer ]
config.frameworks += [ :action_web_service]
config.action_web_service = Rails::OrderedOptions.new
config.load_paths += %W( #{RAILS_ROOT}/app/apis )
config.gem "highline"
@ -26,6 +24,7 @@ Rails::Initializer.run do |config|
# Need to do rspec here and not in test.rb. Needed for rake to work which loads
# the rspec.task file
config.gem "rspec", :lib => false, :version => ">=1.2.2"
config.gem 'datanoise-actionwebservice', :lib => 'actionwebservice'
config.action_controller.use_accept_header = true

View file

@ -0,0 +1,172 @@
--- !ruby/object:Gem::Specification
name: datanoise-actionwebservice
version: !ruby/object:Gem::Version
version: 2.3.2
platform: ruby
authors:
- Leon Breedt, Kent Sibilev
autorequire:
bindir: bin
cert_chain: []
date: 2009-03-29 00:00:00 -04:00
default_executable:
dependencies:
- !ruby/object:Gem::Dependency
name: actionpack
type: :runtime
version_requirement:
version_requirements: !ruby/object:Gem::Requirement
requirements:
- - "="
- !ruby/object:Gem::Version
version: 2.3.2
version:
- !ruby/object:Gem::Dependency
name: activerecord
type: :runtime
version_requirement:
version_requirements: !ruby/object:Gem::Requirement
requirements:
- - "="
- !ruby/object:Gem::Version
version: 2.3.2
version:
description: Adds WSDL/SOAP and XML-RPC web service support to Action Pack
email: bitserf@gmail.com, ksibilev@yahoo.com
executables: []
extensions: []
extra_rdoc_files: []
files:
- Rakefile
- setup.rb
- README
- TODO
- CHANGELOG
- MIT-LICENSE
- examples/googlesearch
- examples/googlesearch/autoloading
- examples/googlesearch/autoloading/google_search_api.rb
- examples/googlesearch/autoloading/google_search_controller.rb
- examples/googlesearch/delegated
- examples/googlesearch/delegated/google_search_service.rb
- examples/googlesearch/delegated/search_controller.rb
- examples/googlesearch/direct
- examples/googlesearch/direct/google_search_api.rb
- examples/googlesearch/direct/search_controller.rb
- examples/googlesearch/README
- examples/metaWeblog
- examples/metaWeblog/apis
- examples/metaWeblog/apis/blogger_api.rb
- examples/metaWeblog/apis/blogger_service.rb
- examples/metaWeblog/apis/meta_weblog_api.rb
- examples/metaWeblog/apis/meta_weblog_service.rb
- examples/metaWeblog/controllers
- examples/metaWeblog/controllers/xmlrpc_controller.rb
- examples/metaWeblog/README
- lib/action_web_service
- lib/action_web_service/api.rb
- lib/action_web_service/base.rb
- lib/action_web_service/casting.rb
- lib/action_web_service/client
- lib/action_web_service/client/base.rb
- lib/action_web_service/client/soap_client.rb
- lib/action_web_service/client/xmlrpc_client.rb
- lib/action_web_service/client.rb
- lib/action_web_service/container
- lib/action_web_service/container/action_controller_container.rb
- lib/action_web_service/container/delegated_container.rb
- lib/action_web_service/container/direct_container.rb
- lib/action_web_service/container.rb
- lib/action_web_service/dispatcher
- lib/action_web_service/dispatcher/abstract.rb
- lib/action_web_service/dispatcher/action_controller_dispatcher.rb
- lib/action_web_service/dispatcher.rb
- lib/action_web_service/invocation.rb
- lib/action_web_service/protocol
- lib/action_web_service/protocol/abstract.rb
- lib/action_web_service/protocol/discovery.rb
- lib/action_web_service/protocol/soap_protocol
- lib/action_web_service/protocol/soap_protocol/marshaler.rb
- lib/action_web_service/protocol/soap_protocol.rb
- lib/action_web_service/protocol/xmlrpc_protocol.rb
- lib/action_web_service/protocol.rb
- lib/action_web_service/scaffolding.rb
- lib/action_web_service/struct.rb
- lib/action_web_service/support
- lib/action_web_service/support/class_inheritable_options.rb
- lib/action_web_service/support/signature_types.rb
- lib/action_web_service/templates
- lib/action_web_service/templates/scaffolds
- lib/action_web_service/templates/scaffolds/layout.html.erb
- lib/action_web_service/templates/scaffolds/methods.html.erb
- lib/action_web_service/templates/scaffolds/parameters.html.erb
- lib/action_web_service/templates/scaffolds/result.html.erb
- lib/action_web_service/test_invoke.rb
- lib/action_web_service/version.rb
- lib/action_web_service.rb
- lib/actionwebservice.rb
- test/abstract_client.rb
- test/abstract_dispatcher.rb
- test/abstract_unit.rb
- test/api_test.rb
- test/apis
- test/apis/auto_load_api.rb
- test/apis/broken_auto_load_api.rb
- test/base_test.rb
- test/casting_test.rb
- test/client_soap_test.rb
- test/client_xmlrpc_test.rb
- test/container_test.rb
- test/dispatcher_action_controller_soap_test.rb
- test/dispatcher_action_controller_xmlrpc_test.rb
- test/fixtures
- test/fixtures/db_definitions
- test/fixtures/db_definitions/mysql.sql
- test/fixtures/users.yml
- test/gencov
- test/invocation_test.rb
- test/run
- test/scaffolded_controller_test.rb
- test/struct_test.rb
- test/test_invoke_test.rb
- generators/web_service
- generators/web_service/templates
- generators/web_service/templates/api_definition.rb
- generators/web_service/templates/controller.rb
- generators/web_service/templates/functional_test.rb
- generators/web_service/USAGE
- generators/web_service/web_service_generator.rb
has_rdoc: true
homepage: http://www.rubyonrails.org
licenses: []
post_install_message:
rdoc_options: []
require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: "0"
version:
required_rubygems_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: "0"
version:
requirements:
- none
rubyforge_project: aws
rubygems_version: 1.3.5
signing_key:
specification_version: 2
summary: Web service support for Action Pack.
test_files: []

View file

@ -0,0 +1,320 @@
*2.1.0*
* Porting to Rails 2.1.0 [Kent Sibilev]
* Documentation for ActionWebService::API::Base. Closes #7275. [zackchandler]
* Allow action_web_service to handle various HTTP methods including GET. Closes #7011. [zackchandler]
* Ensure that DispatcherError is being thrown when a malformed request is received. [Kent Sibilev]
* Added support for decimal types. Closes #6676. [Kent Sibilev]
* Removed deprecated end_form_tag helper. [Kent Sibilev]
* Removed deprecated @request and @response usages. [Kent Sibilev]
* Removed deprecated end_form_tag helper. [Kent Sibilev]
* Removed deprecated @request and @response usages. [Kent Sibilev]
*1.2.6* (November 24th, 2007)
* Depend on Action Pack 1.13.6
* Depend on Active Record 1.15.6
*1.2.5* (October 12th, 2007)
* Depend on Action Pack 1.13.5
* Depend on Active Record 1.15.5
*1.2.4* (October 4th, 2007)
* Depend on Action Pack 1.13.4
* Depend on Active Record 1.15.4
*1.2.3* (March 12th, 2007)
* Depend on Action Pack 1.13.3
*1.2.2* (Feburary 4th, 2007)
* Depend on Action Pack 1.13.2
*1.2.1* (January 16th, 2007)
* Depend on Action Pack 1.13.1
*1.2.0* (January 16th, 2007)
* Removed invocation of deprecated before_action and around_action filter methods. Corresponding before_invocation and after_invocation methods should be used instead. #6275 [Kent Sibilev]
* Provide access to the underlying SOAP driver. #6212 [bmilekic, Kent Sibilev]
* ActionWebService WSDL generation ignores HTTP_X_FORWARDED_HOST [Paul Butcher <paul@paulbutcher.com>]
* Tighten rescue clauses. #5985 [james@grayproductions.net]
* Fixed XMLRPC multicall when one of the called methods returns a struct object. [Kent Sibilev]
* Fix invoke_layered since api_method didn't declare :expects. Closes #4720. [Kevin Ballard <kevin@sb.org>, Kent Sibilev]
*1.1.6* (August 10th, 2006)
* Rely on Action Pack 1.12.5
*1.1.5* (August 8th, 2006)
* Rely on Action Pack 1.12.4 and Active Record 1.14.4
*1.1.4* (June 29th, 2006)
* Rely on Action Pack 1.12.3
*1.1.3* (June 27th, 2006)
* Rely on Action Pack 1.12.2 and Active Record 1.14.3
*1.1.2* (April 9th, 2006)
* Rely on Active Record 1.14.2
*1.1.1* (April 6th, 2006)
* Do not convert driver options to strings (#4499)
*1.1.0* (March 27th, 2006)
* Make ActiveWebService::Struct type reloadable
* Fix scaffolding action when one of the members of a structural type has date or time type
* Remove extra index hash when generating scaffold html for parameters of structural type #4374 [joe@mjg2.com]
* Fix Scaffold Fails with Struct as a Parameter #4363 [joe@mjg2.com]
* Fix soap type registration of multidimensional arrays (#4232)
* Fix that marshaler couldn't handle ActiveRecord models defined in a different namespace (#2392).
* Fix that marshaler couldn't handle structs with members of ActiveRecord type (#1889).
* Fix that marshaler couldn't handle nil values for inner structs (#3576).
* Fix that changes to ActiveWebService::API::Base required restarting of the server (#2390).
* Fix scaffolding for signatures with :date, :time and :base64 types (#3321, #2769, #2078).
* Fix for incorrect casting of TrueClass/FalseClass instances (#2633, #3421).
* Fix for incompatibility problems with SOAP4R 1.5.5 (#2553) [Kent Sibilev]
*1.0.0* (December 13th, 2005)
* Become part of Rails 1.0
*0.9.4* (December 7th, 2005)
* Update from LGPL to MIT license as per Minero Aoki's permission. [Marcel Molina Jr.]
* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.]
* Fix that XML-RPC date/time values did not have well-defined behaviour (#2516, #2534). This fix has one caveat, in that we can't support pre-1970 dates from XML-RPC clients.
*0.9.3* (November 7th, 2005)
* Upgraded to Action Pack 1.11.0 and Active Record 1.13.0
*0.9.2* (October 26th, 2005)
* Upgraded to Action Pack 1.10.2 and Active Record 1.12.2
*0.9.1* (October 19th, 2005)
* Upgraded to Action Pack 1.10.1 and Active Record 1.12.1
*0.9.0* (October 16th, 2005)
* Fix invalid XML request generation bug in test_invoke [Ken Barker]
* Add XML-RPC 'system.multicall' support #1941 [jbonnar]
* Fix duplicate XSD entries for custom types shared across delegated/layered services #1729 [Tyler Kovacs]
* Allow multiple invocations in the same test method #1720 [dkhawk]
* Added ActionWebService::API::Base.soap_client and ActionWebService::API::Base.xmlrpc_client helper methods to create the internal clients for an API, useful for testing from ./script/console
* ActionWebService now always returns UTF-8 responses.
*0.8.1* (11 July, 2005)
* Fix scaffolding for Action Pack controller changes
*0.8.0* (6 July, 2005)
* Fix WSDL generation by aliasing #inherited instead of trying to overwrite it, or the WSDL action may end up not being defined in the controller
* Add ActionController::Base.wsdl_namespace option, to allow overriding of the namespace used in generated WSDL and SOAP messages. This is equivalent to the [WebService(Namespace = "Value")] attribute in .NET.
* Add workaround for Ruby 1.8.3's SOAP4R changing the return value of SOAP::Mapping::Registry#find_mapped_soap_class #1414 [Shugo Maeda]
* Fix moduled controller URLs in WSDL, and add unit test to verify the generated URL #1428
* Fix scaffolding template paths, it was broken on Win32
* Fix that functional testing of :layered controllers failed when using the SOAP protocol
* Allow invocation filters in :direct controllers as well, as they have access to more information regarding the web service request than ActionPack filters
* Add support for a :base64 signature type #1272 [Shugo Maeda]
* Fix that boolean fields were not rendered correctly in scaffolding
* Fix that scaffolding was not working for :delegated dispatching
* Add support for structured types as input parameters to scaffolding, this should let one test the blogging APIs using scaffolding as well
* Fix that generated WSDL was not using relative_url_root for base URI #1210 [Shugo Maeda]
* Use UTF-8 encoding by default for SOAP responses, but if an encoding is supplied by caller, use that for the response #1211 [Shugo Maeda, NAKAMURA Hiroshi]
* If the WSDL was retrieved over HTTPS, use HTTPS URLs in the WSDL too
* Fix that casting change in 0.7.0 would convert nil values to the default value for the type instead of leaving it as nil
*0.7.1* (20th April, 2005)
* Depend on Active Record 1.10.1 and Action Pack 1.8.1
*0.7.0* (19th April, 2005)
* When casting structured types, don't try to send obj.name= unless obj responds to it, causes casting to be less likely to fail for XML-RPC
* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser
* ActionWebService::API::Base#api_methods now returns a hash containing ActionWebService::API::Method objects instead of hashes. However, ActionWebService::API::Method defines a #[]() backwards compatibility method so any existing code utilizing this will still work.
* The :layered dispatching mode can now be used with SOAP as well, allowing you to support SOAP and XML-RPC clients for APIs like the metaWeblog API
* Remove ActiveRecordSoapMarshallable workaround, see #912 for details
* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC)
* Ensure return value is properly cast as well, fixes XML-RPC interoperability with Ecto and possibly other clients
* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC
* Perform casting of struct members even if the structure is already of the correct type, so that the type we specify for the struct member is always the type of the value seen by the API implementation
*0.6.2* (27th March, 2005)
* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939.
*0.6.1* (22th March, 2005)
* Fix that method response QNames mismatched with that declared in the WSDL, makes SOAP::WSDLDriverFactory work against AWS again
* Fix that @request.env was being modified, instead, dup the value gotten from env
* Fix XML-RPC example to use :layered mode, so it works again
* Support casting '0' or 0 into false, and '1' or 1 into true, when expecting a boolean value
* Fix that SOAP fault response fault code values were not QName's #804
*0.6.0* (7th March, 2005)
* Add action_controller/test_invoke, used for integrating AWS with the Rails testing infrastructure
* Allow passing through options to the SOAP RPC driver for the SOAP client
* Make the SOAP WS marshaler use #columns to decide which fields to marshal as well, avoids providing attributes brought in by associations
* Add <tt>ActionWebService::API::Base.allow_active_record_expects</tt> option, with a default of false. Setting this to true will allow specifying ActiveRecord::Base model classes in <tt>:expects</tt>. API writers should take care to validate the received ActiveRecord model objects when turning it on, and/or have an authentication mechanism in place to reduce the security risk.
* Improve error message reporting. Bugs in either AWS or the web service itself will send back a protocol-specific error report message if possible, otherwise, provide as much detail as possible.
* Removed type checking of received parameters, and perform casting for XML-RPC if possible, but fallback to the received parameters if casting fails, closes #677
* Refactored SOAP and XML-RPC marshaling and encoding into a small library devoted exclusively to protocol specifics, also cleaned up the SOAP marshaling approach, so that array and custom type marshaling should be a bit faster.
* Add namespaced XML-RPC method name support, closes #678
* Replace '::' with '..' in fully qualified type names for marshaling and WSDL. This improves interoperability with .NET, and closes #676.
*0.5.0* (24th February, 2005)
* lib/action_service/dispatcher*: replace "router" fragments with
one file for Action Controllers, moves dispatching work out of
the container
* lib/*,test/*,examples/*: rename project to
ActionWebService. prefix all generic "service" type names with web_.
update all using code as well as the RDoc.
* lib/action_service/router/wsdl.rb: ensure that #wsdl is
defined in the final container class, or the new ActionPack
filtering will exclude it
* lib/action_service/struct.rb,test/struct_test.rb: create a
default #initialize on inherit that accepts a Hash containing
the default member values
* lib/action_service/api/action_controller.rb: add support and
tests for #client_api in controller
* test/router_wsdl_test.rb: add tests to ensure declared
service names don't contain ':', as ':' causes interoperability
issues
* lib/*, test/*: rename "interface" concept to "api", and change all
related uses to reflect this change. update all uses of Inflector
to call the method on String instead.
* test/api_test.rb: add test to ensure API definition not
instantiatable
* lib/action_service/invocation.rb: change @invocation_params to
@method_params
* lib/*: update RDoc
* lib/action_service/struct.rb: update to support base types
* lib/action_service/support/signature.rb: support the notion of
"base types" in signatures, with well-known unambiguous names such as :int,
:bool, etc, which map to the correct Ruby class. accept the same names
used by ActiveRecord as well as longer versions of each, as aliases.
* examples/*: update for seperate API definition updates
* lib/action_service/*, test/*: extensive refactoring: define API methods in
a seperate class, and specify it wherever used with 'service_api'.
this makes writing a client API for accessing defined API methods
with ActionWebService really easy.
* lib/action_service/container.rb: fix a bug in default call
handling for direct dispatching, and add ActionController filter
support for direct dispatching.
* test/router_action_controller_test.rb: add tests to ensure
ActionController filters are actually called.
* test/protocol_soap_test.rb: add more tests for direct dispatching.
0.3.0
* First public release

View file

@ -0,0 +1,21 @@
Copyright (C) 2005 Leon Breedt
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,381 @@
= Action Web Service -- Serving APIs on rails
Action Web Service provides a way to publish interoperable web service APIs with
Rails without spending a lot of time delving into protocol details.
== Features
* SOAP RPC protocol support
* Dynamic WSDL generation for APIs
* XML-RPC protocol support
* Clients that use the same API definitions as the server for
easy interoperability with other Action Web Service based applications
* Type signature hints to improve interoperability with static languages
* Active Record model class support in signatures
== Defining your APIs
You specify the methods you want to make available as API methods in an
ActionWebService::API::Base derivative, and then specify this API
definition class wherever you want to use that API.
The implementation of the methods is done separately from the API
specification.
==== Method name inflection
Action Web Service will camelcase the method names according to Rails Inflector
rules for the API visible to public callers. What this means, for example,
is that the method names in generated WSDL will be camelcased, and callers will
have to supply the camelcased name in their requests for the request to
succeed.
If you do not desire this behaviour, you can turn it off with the
ActionWebService::API::Base +inflect_names+ option.
==== Inflection examples
:add => Add
:find_all => FindAll
==== Disabling inflection
class PersonAPI < ActionWebService::API::Base
inflect_names false
end
==== API definition example
class PersonAPI < ActionWebService::API::Base
api_method :add, :expects => [:string, :string, :bool], :returns => [:int]
api_method :remove, :expects => [:int], :returns => [:bool]
end
==== API usage example
class PersonController < ActionController::Base
web_service_api PersonAPI
def add
end
def remove
end
end
== Publishing your APIs
Action Web Service uses Action Pack to process protocol requests. There are two
modes of dispatching protocol requests, _Direct_, and _Delegated_.
=== Direct dispatching
This is the default mode. In this mode, public controller instance methods
implement the API methods, and parameters are passed through to the methods in
accordance with the API specification.
The return value of the method is sent back as the return value to the
caller.
In this mode, a special <tt>api</tt> action is generated in the target
controller to unwrap the protocol request, forward it on to the relevant method
and send back the wrapped return value. <em>This action must not be
overridden.</em>
==== Direct dispatching example
class PersonController < ApplicationController
web_service_api PersonAPI
def add
end
def remove
end
end
class PersonAPI < ActionWebService::API::Base
...
end
For this example, protocol requests for +Add+ and +Remove+ methods sent to
<tt>/person/api</tt> will be routed to the controller methods +add+ and +remove+.
=== Delegated dispatching
This mode can be turned on by setting the +web_service_dispatching_mode+ option
in a controller to <tt>:delegated</tt>.
In this mode, the controller contains one or more web service objects (objects
that implement an ActionWebService::API::Base definition). These web service
objects are each mapped onto one controller action only.
==== Delegated dispatching example
class ApiController < ApplicationController
web_service_dispatching_mode :delegated
web_service :person, PersonService.new
end
class PersonService < ActionWebService::Base
web_service_api PersonAPI
def add
end
def remove
end
end
class PersonAPI < ActionWebService::API::Base
...
end
For this example, all protocol requests for +PersonService+ are
sent to the <tt>/api/person</tt> action.
The <tt>/api/person</tt> action is generated when the +web_service+
method is called. <em>This action must not be overridden.</em>
Other controller actions (actions that aren't the target of a +web_service+ call)
are ignored for ActionWebService purposes, and can do normal action tasks.
=== Layered dispatching
This mode can be turned on by setting the +web_service_dispatching_mode+ option
in a controller to <tt>:layered</tt>.
This mode is similar to _delegated_ mode, in that multiple web service objects
can be attached to one controller, however, all protocol requests are sent to a
single endpoint.
Use this mode when you want to share code between XML-RPC and SOAP clients,
for APIs where the XML-RPC method names have prefixes. An example of such
a method name would be <tt>blogger.newPost</tt>.
==== Layered dispatching example
class ApiController < ApplicationController
web_service_dispatching_mode :layered
web_service :mt, MovableTypeService.new
web_service :blogger, BloggerService.new
web_service :metaWeblog, MetaWeblogService.new
end
class MovableTypeService < ActionWebService::Base
...
end
class BloggerService < ActionWebService::Base
...
end
class MetaWeblogService < ActionWebService::API::Base
...
end
For this example, an XML-RPC call for a method with a name like
<tt>mt.getCategories</tt> will be sent to the <tt>getCategories</tt>
method on the <tt>:mt</tt> service.
== Customizing WSDL generation
You can customize the names used for the SOAP bindings in the generated
WSDL by using the wsdl_service_name option in a controller:
class WsController < ApplicationController
wsdl_service_name 'MyApp'
end
You can also customize the namespace used in the generated WSDL for
custom types and message definition types:
class WsController < ApplicationController
wsdl_namespace 'http://my.company.com/app/wsapi'
end
The default namespace used is 'urn:ActionWebService', if you don't supply
one.
== ActionWebService and UTF-8
If you're going to be sending back strings containing non-ASCII UTF-8
characters using the <tt>:string</tt> data type, you need to make sure that
Ruby is using UTF-8 as the default encoding for its strings.
The default in Ruby is to use US-ASCII encoding for strings, which causes a string
validation check in the Ruby SOAP library to fail and your string to be sent
back as a Base-64 value, which may confuse clients that expected strings
because of the WSDL.
Two ways of setting the default string encoding are:
* Start Ruby using the <tt>-Ku</tt> command-line option to the Ruby executable
* Set the <tt>$KCODE</tt> flag in <tt>config/environment.rb</tt> to the
string <tt>'UTF8'</tt>
== Testing your APIs
=== Functional testing
You can perform testing of your APIs by creating a functional test for the
controller dispatching the API, and calling #invoke in the test case to
perform the invocation.
Example:
class PersonApiControllerTest < Test::Unit::TestCase
def setup
@controller = PersonController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_add
result = invoke :remove, 1
assert_equal true, result
end
end
This example invokes the API method <tt>test</tt>, defined on
the PersonController, and returns the result.
If you're not using SOAP (or you're having serialisation difficulties),
you can test XMLRPC like this:
class PersonApiControllerTest < Test::Unit::TestCase
def setup
@controller = PersonController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@protocol = :xmlrpc # can also be :soap, the default
end
def test_add
result = invoke :remove, 1 # no change here
assert_equal true, result
end
end
=== Scaffolding
You can also test your APIs with a web browser by attaching scaffolding
to the controller.
Example:
class PersonController
web_service_scaffold :invocation
end
This creates an action named <tt>invocation</tt> on the PersonController.
Navigating to this action lets you select the method to invoke, supply the parameters,
and view the result of the invocation.
== Using the client support
Action Web Service includes client classes that can use the same API
definition as the server. The advantage of this approach is that your client
will have the same support for Active Record and structured types as the
server, and can just use them directly, and rely on the marshaling to Do The
Right Thing.
*Note*: The client support is intended for communication between Ruby on Rails
applications that both use Action Web Service. It may work with other servers, but
that is not its intended use, and interoperability can't be guaranteed, especially
not for .NET web services.
Web services protocol specifications are complex, and Action Web Service client
support can only be guaranteed to work with a subset.
==== Factory created client example
class BlogManagerController < ApplicationController
web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger'
end
class SearchingController < ApplicationController
web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch'
end
See ActionWebService::API::ActionController::ClassMethods for more details.
==== Manually created client example
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 BloggerAPI < ActionWebService::API::Base
inflect_names false
api_method :getRecentPosts, :returns => [[Blog::Post]]
end
blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger")
posts = blog.getRecentPosts
See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details.
== Dependencies
Action Web Service requires that the Action Pack and Active Record are either
available to be required immediately or are accessible as GEMs.
It also requires a version of Ruby that includes SOAP support in the standard
library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this
is the version tested against.
== Download
The latest Action Web Service version can be downloaded from
http://rubyforge.org/projects/actionservice
== Installation
You can install Action Web Service with the following command.
% [sudo] ruby setup.rb
== License
Action Web Service is released under the MIT license.
== Support
The Ruby on Rails mailing list
Or, to contact the author, send mail to bitserf@gmail.com

View file

@ -0,0 +1,173 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
require 'fileutils'
require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
PKG_NAME = 'actionwebservice'
PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}"
RELEASE_NAME = "REL #{PKG_VERSION}"
RUBY_FORGE_PROJECT = "aws"
RUBY_FORGE_USER = "webster132"
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
t.test_files = Dir['test/*_test.rb']
t.verbose = true
}
SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
desc 'Build the MySQL test database'
task :build_database do
%x( mysqladmin -uroot create actionwebservice_unittest )
%x( mysql -uroot actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
end
# Generate the RDoc documentation
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Action Web Service -- Web services for Action Pack"
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('CHANGELOG')
rdoc.rdoc_files.include('lib/action_web_service.rb')
rdoc.rdoc_files.include('lib/action_web_service/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/api/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/client/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/container/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb')
rdoc.rdoc_files.include('lib/action_web_service/support/*.rb')
}
# Create compressed packages
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = PKG_NAME
s.summary = "Web service support for Action Pack."
s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack}
s.version = PKG_VERSION
s.author = "Leon Breedt, Kent Sibilev"
s.email = "bitserf@gmail.com, ksibilev@yahoo.com"
s.rubyforge_project = "aws"
s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD)
s.add_dependency('activerecord', '= 2.3.2' + PKG_BUILD)
s.has_rdoc = true
s.requirements << 'none'
s.require_path = 'lib'
s.autorequire = 'actionwebservice'
s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ]
s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) }
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) }
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) }
s.files = s.files + Dir.glob( "generators/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) }
end
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
p.need_tar = true
p.need_zip = true
end
# Publish beta gem
desc "Publish the API documentation"
task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
end
# Publish documentation
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload
end
def each_source_file(*args)
prefix, includes, excludes, open_file = args
prefix ||= File.dirname(__FILE__)
open_file = true if open_file.nil?
includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$]
excludes ||= %w[lib\/action_web_service\/vendor]
Find.find(prefix) do |file_name|
next if file_name =~ /\.svn/
file_name.gsub!(/^\.\//, '')
continue = false
includes.each do |inc|
if file_name.match(/#{inc}/)
continue = true
break
end
end
next unless continue
excludes.each do |exc|
if file_name.match(/#{exc}/)
continue = false
break
end
end
next unless continue
if open_file
File.open(file_name) do |f|
yield file_name, f
end
else
yield file_name
end
end
end
desc "Count lines of the AWS source code"
task :lines do
total_lines = total_loc = 0
puts "Per File:"
each_source_file do |file_name, f|
file_lines = file_loc = 0
while line = f.gets
file_lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
file_loc += 1
end
puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}"
total_lines += file_lines
total_loc += file_loc
end
puts "Total:"
puts " Lines #{total_lines}, LOC #{total_loc}"
end
desc "Publish the release files to RubyForge."
task :release => [ :package ] do
require 'rubyforge'
packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }
rubyforge = RubyForge.new
rubyforge.login
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
end

View file

@ -0,0 +1,32 @@
= Post-1.0
- Document/Literal SOAP support
- URL-based dispatching, URL identifies method
- Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching
in general. Support vanilla XML-format as a "Rails" protocol?
XML::Simple deserialization into params?
web_service_dispatching_mode :rest
def method1(params)
end
def method2(params)
end
/ws/method1
<xml>
/ws/method2
<yaml>
- Allow locking down a controller to only accept messages for a particular
protocol. This will allow us to generate fully conformant error messages
in cases where we currently fudge it if we don't know the protocol.
- Allow AWS user to participate in typecasting, so they can centralize
workarounds for buggy input in one place
= Refactoring
- Don't have clean way to go from SOAP Class object to the xsd:NAME type
string -- NaHi possibly looking at remedying this situation

View file

@ -0,0 +1,143 @@
= Google Service example
This example shows how one would implement an API like Google
Search that uses lots of structured types.
There are examples for "Direct" and "Delegated" dispatching
modes.
There is also an example for API definition file autoloading.
= Running the examples
1. Add the files to an Action Web Service enabled Rails project.
"Direct" example:
* Copy direct/search_controller.rb to "app/controllers"
in a Rails project.
* Copy direct/google_search_api.rb to "app/apis"
in a Rails project
"Delegated" example:
* Copy delegated/search_controller.rb to "app/controllers"
in a Rails project.
* Copy delegated/google_search_service.rb to "lib"
in a Rails project.
"Autoloading" example:
* Copy autoloading/google_search_api.rb to "app/apis" (create the directory
if it doesn't exist) in a Rails project.
* Copy autoloading/google_search_controller.rb "app/controllers"
in a Rails project.
2. Go to the WSDL url in a browser, and check that it looks correct.
"Direct" and "Delegated" examples:
http://url_to_project/search/wsdl
"Autoloading" example:
http://url_to_project/google_search/wsdl
You can compare it to Google's hand-coded WSDL at http://api.google.com/GoogleSearch.wsdl
and see how close (or not) the generated version is.
Note that I used GoogleSearch as the canonical "best practice"
interoperable example when implementing WSDL/SOAP support, which might
explain extreme similarities :)
3. Test that it works with .NET (Mono in this example):
$ wget WSDL_URL
$ mv wsdl GoogleSearch.wsdl
$ wsdl -out:GoogleSearch.cs GoogleSearch.wsdl
Add these lines to the GoogleSearchService class body (be mindful of the
wrapping):
public static void Main(string[] args)
{
GoogleSearchResult result;
GoogleSearchService service;
service = new GoogleSearchService();
result = service.doGoogleSearch("myApiKey", "my query", 10, 30, true, "restrict", false, "lr", "ie", "oe");
System.Console.WriteLine("documentFiltering: {0}", result.documentFiltering);
System.Console.WriteLine("searchComments: {0}", result.searchComments);
System.Console.WriteLine("estimatedTotalResultsCount: {0}", result.estimatedTotalResultsCount);
System.Console.WriteLine("estimateIsExact: {0}", result.estimateIsExact);
System.Console.WriteLine("resultElements:");
foreach (ResultElement element in result.resultElements) {
System.Console.WriteLine("\tsummary: {0}", element.summary);
System.Console.WriteLine("\tURL: {0}", element.URL);
System.Console.WriteLine("\tsnippet: {0}", element.snippet);
System.Console.WriteLine("\ttitle: {0}", element.title);
System.Console.WriteLine("\tcachedSize: {0}", element.cachedSize);
System.Console.WriteLine("\trelatedInformationPresent: {0}", element.relatedInformationPresent);
System.Console.WriteLine("\thostName: {0}", element.hostName);
System.Console.WriteLine("\tdirectoryCategory: {0}", element.directoryCategory.fullViewableName);
System.Console.WriteLine("\tdirectoryTitle: {0}", element.directoryTitle);
}
System.Console.WriteLine("searchQuery: {0}", result.searchQuery);
System.Console.WriteLine("startIndex: {0}", result.startIndex);
System.Console.WriteLine("endIndex: {0}", result.endIndex);
System.Console.WriteLine("searchTips: {0}", result.searchTips);
System.Console.WriteLine("directoryCategories:");
foreach (DirectoryCategory cat in result.directoryCategories) {
System.Console.WriteLine("\t{0} ({1})", cat.fullViewableName, cat.specialEncoding);
}
System.Console.WriteLine("searchTime: {0}", result.searchTime);
}
Now compile and run:
$ mcs -reference:System.Web.Services GoogleSearch.cs
$ mono GoogleSearch.exe
If you had the application running (on the same host you got
the WSDL from), you should see something like this:
documentFiltering: True
searchComments:
estimatedTotalResultsCount: 322000
estimateIsExact: False
resultElements:
summary: ONlamp.com: Rolling with Ruby on Rails
URL: http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html
snippet: Curt Hibbs shows off Ruby on Rails by building a simple ...
title: Teh Railz0r
cachedSize: Almost no lines of code!
relatedInformationPresent: True
hostName: rubyonrails.com
directoryCategory: Web Development
directoryTitle:
searchQuery: http://www.google.com/search?q=ruby+on+rails
startIndex: 10
endIndex: 40
searchTips: "on" is a very common word and was not included in your search [details]
directoryCategories:
Web Development (UTF-8)
Programming (US-ASCII)
searchTime: 1E-06
Also, if an API method throws an exception, it will be sent back to the
caller in the protocol's exception format, so they should get an exception
thrown on their side with a meaningful error message.
If you don't like this behaviour, you can do:
class MyController < ActionController::Base
web_service_exception_reporting false
end
4. Crack open a beer. Publishing APIs for working with the same model as
your Rails web app should be easy from now on :)

View file

@ -0,0 +1,50 @@
class DirectoryCategory < ActionWebService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionWebService::Struct
member :summary, :string
member :URL, :string
member :snippet, :string
member :title, :string
member :cachedSize, :string
member :relatedInformationPresent, :bool
member :hostName, :string
member :directoryCategory, DirectoryCategory
member :directoryTitle, :string
end
class GoogleSearchResult < ActionWebService::Struct
member :documentFiltering, :bool
member :searchComments, :string
member :estimatedTotalResultsCount, :int
member :estimateIsExact, :bool
member :resultElements, [ResultElement]
member :searchQuery, :string
member :startIndex, :int
member :endIndex, :int
member :searchTips, :string
member :directoryCategories, [DirectoryCategory]
member :searchTime, :float
end
class GoogleSearchAPI < ActionWebService::API::Base
inflect_names false
api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}]
api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}]
api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [
{:key=>:string},
{:q=>:string},
{:start=>:int},
{:maxResults=>:int},
{:filter=>:bool},
{:restrict=>:string},
{:safeSearch=>:bool},
{:lr=>:string},
{:ie=>:string},
{:oe=>:string}
]
end

View file

@ -0,0 +1,57 @@
class GoogleSearchController < ApplicationController
wsdl_service_name 'GoogleSearch'
def doGetCachedPage
"<html><body>i am a cached page. my key was %s, url was %s</body></html>" % [@params['key'], @params['url']]
end
def doSpellingSuggestion
"%s: Did you mean '%s'?" % [@params['key'], @params['phrase']]
end
def doGoogleSearch
resultElement = ResultElement.new
resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails"
resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html"
resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " +
"almost no Ruby experience. ... Rolling with Ruby on Rails. ..."
resultElement.title = "Teh Railz0r"
resultElement.cachedSize = "Almost no lines of code!"
resultElement.relatedInformationPresent = true
resultElement.hostName = "rubyonrails.com"
resultElement.directoryCategory = category("Web Development", "UTF-8")
result = GoogleSearchResult.new
result.documentFiltering = @params['filter']
result.searchComments = ""
result.estimatedTotalResultsCount = 322000
result.estimateIsExact = false
result.resultElements = [resultElement]
result.searchQuery = "http://www.google.com/search?q=ruby+on+rails"
result.startIndex = @params['start']
result.endIndex = @params['start'] + @params['maxResults']
result.searchTips = "\"on\" is a very common word and was not included in your search [details]"
result.searchTime = 0.000001
# For Mono, we have to clone objects if they're referenced by more than one place, otherwise
# the Ruby SOAP collapses them into one instance and uses references all over the
# place, confusing Mono.
#
# This has recently been fixed:
# http://bugzilla.ximian.com/show_bug.cgi?id=72265
result.directoryCategories = [
category("Web Development", "UTF-8"),
category("Programming", "US-ASCII"),
]
result
end
private
def category(name, encoding)
cat = DirectoryCategory.new
cat.fullViewableName = name.dup
cat.specialEncoding = encoding.dup
cat
end
end

View file

@ -0,0 +1,108 @@
class DirectoryCategory < ActionWebService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionWebService::Struct
member :summary, :string
member :URL, :string
member :snippet, :string
member :title, :string
member :cachedSize, :string
member :relatedInformationPresent, :bool
member :hostName, :string
member :directoryCategory, DirectoryCategory
member :directoryTitle, :string
end
class GoogleSearchResult < ActionWebService::Struct
member :documentFiltering, :bool
member :searchComments, :string
member :estimatedTotalResultsCount, :int
member :estimateIsExact, :bool
member :resultElements, [ResultElement]
member :searchQuery, :string
member :startIndex, :int
member :endIndex, :int
member :searchTips, :string
member :directoryCategories, [DirectoryCategory]
member :searchTime, :float
end
class GoogleSearchAPI < ActionWebService::API::Base
inflect_names false
api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}]
api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}]
api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [
{:key=>:string},
{:q=>:string},
{:start=>:int},
{:maxResults=>:int},
{:filter=>:bool},
{:restrict=>:string},
{:safeSearch=>:bool},
{:lr=>:string},
{:ie=>:string},
{:oe=>:string}
]
end
class GoogleSearchService < ActionWebService::Base
web_service_api GoogleSearchAPI
def doGetCachedPage(key, url)
"<html><body>i am a cached page</body></html>"
end
def doSpellingSuggestion(key, phrase)
"Did you mean 'teh'?"
end
def doGoogleSearch(key, q, start, maxResults, filter, restrict, safeSearch, lr, ie, oe)
resultElement = ResultElement.new
resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails"
resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html"
resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " +
"almost no Ruby experience. ... Rolling with Ruby on Rails. ..."
resultElement.title = "Teh Railz0r"
resultElement.cachedSize = "Almost no lines of code!"
resultElement.relatedInformationPresent = true
resultElement.hostName = "rubyonrails.com"
resultElement.directoryCategory = category("Web Development", "UTF-8")
result = GoogleSearchResult.new
result.documentFiltering = filter
result.searchComments = ""
result.estimatedTotalResultsCount = 322000
result.estimateIsExact = false
result.resultElements = [resultElement]
result.searchQuery = "http://www.google.com/search?q=ruby+on+rails"
result.startIndex = start
result.endIndex = start + maxResults
result.searchTips = "\"on\" is a very common word and was not included in your search [details]"
result.searchTime = 0.000001
# For Mono, we have to clone objects if they're referenced by more than one place, otherwise
# the Ruby SOAP collapses them into one instance and uses references all over the
# place, confusing Mono.
#
# This has recently been fixed:
# http://bugzilla.ximian.com/show_bug.cgi?id=72265
result.directoryCategories = [
category("Web Development", "UTF-8"),
category("Programming", "US-ASCII"),
]
result
end
private
def category(name, encoding)
cat = DirectoryCategory.new
cat.fullViewableName = name.dup
cat.specialEncoding = encoding.dup
cat
end
end

View file

@ -0,0 +1,7 @@
require 'google_search_service'
class SearchController < ApplicationController
wsdl_service_name 'GoogleSearch'
web_service_dispatching_mode :delegated
web_service :beta3, GoogleSearchService.new
end

View file

@ -0,0 +1,50 @@
class DirectoryCategory < ActionWebService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionWebService::Struct
member :summary, :string
member :URL, :string
member :snippet, :string
member :title, :string
member :cachedSize, :string
member :relatedInformationPresent, :bool
member :hostName, :string
member :directoryCategory, DirectoryCategory
member :directoryTitle, :string
end
class GoogleSearchResult < ActionWebService::Struct
member :documentFiltering, :bool
member :searchComments, :string
member :estimatedTotalResultsCount, :int
member :estimateIsExact, :bool
member :resultElements, [ResultElement]
member :searchQuery, :string
member :startIndex, :int
member :endIndex, :int
member :searchTips, :string
member :directoryCategories, [DirectoryCategory]
member :searchTime, :float
end
class GoogleSearchAPI < ActionWebService::API::Base
inflect_names false
api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}]
api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}]
api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [
{:key=>:string},
{:q=>:string},
{:start=>:int},
{:maxResults=>:int},
{:filter=>:bool},
{:restrict=>:string},
{:safeSearch=>:bool},
{:lr=>:string},
{:ie=>:string},
{:oe=>:string}
]
end

View file

@ -0,0 +1,58 @@
class SearchController < ApplicationController
web_service_api :google_search
wsdl_service_name 'GoogleSearch'
def doGetCachedPage
"<html><body>i am a cached page. my key was %s, url was %s</body></html>" % [@params['key'], @params['url']]
end
def doSpellingSuggestion
"%s: Did you mean '%s'?" % [@params['key'], @params['phrase']]
end
def doGoogleSearch
resultElement = ResultElement.new
resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails"
resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html"
resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " +
"almost no Ruby experience. ... Rolling with Ruby on Rails. ..."
resultElement.title = "Teh Railz0r"
resultElement.cachedSize = "Almost no lines of code!"
resultElement.relatedInformationPresent = true
resultElement.hostName = "rubyonrails.com"
resultElement.directoryCategory = category("Web Development", "UTF-8")
result = GoogleSearchResult.new
result.documentFiltering = @params['filter']
result.searchComments = ""
result.estimatedTotalResultsCount = 322000
result.estimateIsExact = false
result.resultElements = [resultElement]
result.searchQuery = "http://www.google.com/search?q=ruby+on+rails"
result.startIndex = @params['start']
result.endIndex = @params['start'] + @params['maxResults']
result.searchTips = "\"on\" is a very common word and was not included in your search [details]"
result.searchTime = 0.000001
# For Mono, we have to clone objects if they're referenced by more than one place, otherwise
# the Ruby SOAP collapses them into one instance and uses references all over the
# place, confusing Mono.
#
# This has recently been fixed:
# http://bugzilla.ximian.com/show_bug.cgi?id=72265
result.directoryCategories = [
category("Web Development", "UTF-8"),
category("Programming", "US-ASCII"),
]
result
end
private
def category(name, encoding)
cat = DirectoryCategory.new
cat.fullViewableName = name.dup
cat.specialEncoding = encoding.dup
cat
end
end

View file

@ -0,0 +1,17 @@
= metaWeblog example
This example shows how one might begin to go about adding metaWeblog
(http://www.xmlrpc.com/metaWeblogApi) API support to a Rails-based
blogging application.
The example APIs are more verbose than you may want to make them, for documentation
reasons.
= Running
1. Copy the "apis" directory and its files into "app" in a Rails project.
2. Copy the "controllers" directory and its files into "app" in a Rails project
3. Fire up a desktop blogging application (such as w.bloggar, MarsEdit, or BloGTK),
point it at http://localhost:3000/xmlrpc/api, and try creating or editing blog posts.

View file

@ -0,0 +1,60 @@
#
# see the blogger API spec at http://www.blogger.com/developers/api/1_docs/
# note that the method signatures are subtly different to metaWeblog, they
# are not identical. take care to ensure you handle the different semantics
# properly if you want to support blogger API too, to get maximum compatibility.
#
module Blog
class Blog < ActionWebService::Struct
member :url, :string
member :blogid, :string
member :blogName, :string
end
class User < ActionWebService::Struct
member :nickname, :string
member :userid, :string
member :url, :string
member :email, :string
member :lastname, :string
member :firstname, :string
end
end
#
# blogger
#
class BloggerAPI < ActionWebService::API::Base
inflect_names false
api_method :newPost, :returns => [:string], :expects => [
{:appkey=>:string},
{:blogid=>:string},
{:username=>:string},
{:password=>:string},
{:content=>:string},
{:publish=>:bool}
]
api_method :editPost, :returns => [:bool], :expects => [
{:appkey=>:string},
{:postid=>:string},
{:username=>:string},
{:password=>:string},
{:content=>:string},
{:publish=>:bool}
]
api_method :getUsersBlogs, :returns => [[Blog::Blog]], :expects => [
{:appkey=>:string},
{:username=>:string},
{:password=>:string}
]
api_method :getUserInfo, :returns => [Blog::User], :expects => [
{:appkey=>:string},
{:username=>:string},
{:password=>:string}
]
end

View file

@ -0,0 +1,34 @@
require 'blogger_api'
class BloggerService < ActionWebService::Base
web_service_api BloggerAPI
def initialize
@postid = 0
end
def newPost(key, id, user, pw, content, publish)
$stderr.puts "id=#{id} user=#{user} pw=#{pw}, content=#{content.inspect} [#{publish}]"
(@postid += 1).to_s
end
def editPost(key, post_id, user, pw, content, publish)
$stderr.puts "id=#{post_id} user=#{user} pw=#{pw} content=#{content.inspect} [#{publish}]"
true
end
def getUsersBlogs(key, user, pw)
$stderr.puts "getting blogs for #{user}"
blog = Blog::Blog.new(
:url =>'http://blog',
:blogid => 'myblog',
:blogName => 'My Blog'
)
[blog]
end
def getUserInfo(key, user, pw)
$stderr.puts "getting user info for #{user}"
Blog::User.new(:nickname => 'user', :email => 'user@test.com')
end
end

View file

@ -0,0 +1,67 @@
#
# here lie structures, cousins of those on http://www.xmlrpc.com/metaWeblog
# but they don't necessarily the real world reflect
# so if you do, find that your client complains:
# please tell, of problems you suffered through
#
module Blog
class Post < ActionWebService::Struct
member :title, :string
member :link, :string
member :description, :string
member :author, :string
member :category, :string
member :comments, :string
member :guid, :string
member :pubDate, :string
end
class Category < ActionWebService::Struct
member :description, :string
member :htmlUrl, :string
member :rssUrl, :string
end
end
#
# metaWeblog
#
class MetaWeblogAPI < ActionWebService::API::Base
inflect_names false
api_method :newPost, :returns => [:string], :expects => [
{:blogid=>:string},
{:username=>:string},
{:password=>:string},
{:struct=>Blog::Post},
{:publish=>:bool}
]
api_method :editPost, :returns => [:bool], :expects => [
{:postid=>:string},
{:username=>:string},
{:password=>:string},
{:struct=>Blog::Post},
{:publish=>:bool},
]
api_method :getPost, :returns => [Blog::Post], :expects => [
{:postid=>:string},
{:username=>:string},
{:password=>:string},
]
api_method :getCategories, :returns => [[Blog::Category]], :expects => [
{:blogid=>:string},
{:username=>:string},
{:password=>:string},
]
api_method :getRecentPosts, :returns => [[Blog::Post]], :expects => [
{:blogid=>:string},
{:username=>:string},
{:password=>:string},
{:numberOfPosts=>:int},
]
end

View file

@ -0,0 +1,48 @@
require 'meta_weblog_api'
class MetaWeblogService < ActionWebService::Base
web_service_api MetaWeblogAPI
def initialize
@postid = 0
end
def newPost(id, user, pw, struct, publish)
$stderr.puts "id=#{id} user=#{user} pw=#{pw}, struct=#{struct.inspect} [#{publish}]"
(@postid += 1).to_s
end
def editPost(post_id, user, pw, struct, publish)
$stderr.puts "id=#{post_id} user=#{user} pw=#{pw} struct=#{struct.inspect} [#{publish}]"
true
end
def getPost(post_id, user, pw)
$stderr.puts "get post #{post_id}"
Blog::Post.new(:title => 'hello world', :description => 'first post!')
end
def getCategories(id, user, pw)
$stderr.puts "categories for #{user}"
cat = Blog::Category.new(
:description => 'Tech',
:htmlUrl => 'http://blog/tech',
:rssUrl => 'http://blog/tech.rss')
[cat]
end
def getRecentPosts(id, user, pw, num)
$stderr.puts "recent #{num} posts for #{user} on blog #{id}"
post1 = Blog::Post.new(
:title => 'first post!',
:link => 'http://blog.xeraph.org/testOne.html',
:description => 'this is the first post'
)
post2 = Blog::Post.new(
:title => 'second post!',
:link => 'http://blog.xeraph.org/testTwo.html',
:description => 'this is the second post'
)
[post1, post2]
end
end

View file

@ -0,0 +1,16 @@
#
# example controller implementing both blogger and metaWeblog APIs
# in a way that should be compatible with clients supporting both/either.
#
# test by pointing your client at http://URL/xmlrpc/api
#
require 'meta_weblog_service'
require 'blogger_service'
class XmlrpcController < ApplicationController
web_service_dispatching_mode :layered
web_service :metaWeblog, MetaWeblogService.new
web_service :blogger, BloggerService.new
end

View file

@ -0,0 +1,28 @@
Description:
The web service generator creates the controller and API definition for
a web service.
The generator takes a web service name and a list of API methods as arguments.
The web service name may be given in CamelCase or under_score and should
contain no extra suffixes. To create a web service within a
module, specify the web service name as 'module/webservice'.
The generator creates a controller class in app/controllers, an API definition
in app/apis, and a functional test suite in test/functional.
Example:
./script/generate web_service User add edit list remove
User web service.
Controller: app/controllers/user_controller.rb
API: app/apis/user_api.rb
Test: test/functional/user_api_test.rb
Modules Example:
./script/generate web_service 'api/registration' register renew
Registration web service.
Controller: app/controllers/api/registration_controller.rb
API: app/apis/api/registration_api.rb
Test: test/functional/api/registration_api_test.rb

View file

@ -0,0 +1,5 @@
class <%= class_name %>Api < ActionWebService::API::Base
<% for method_name in args -%>
api_method :<%= method_name %>
<% end -%>
end

View file

@ -0,0 +1,8 @@
class <%= class_name %>Controller < ApplicationController
wsdl_service_name '<%= class_name %>'
<% for method_name in args -%>
def <%= method_name %>
end
<% end -%>
end

View file

@ -0,0 +1,19 @@
require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
require '<%= file_path %>_controller'
class <%= class_name %>Controller; def rescue_action(e) raise e end; end
class <%= class_name %>ControllerApiTest < Test::Unit::TestCase
def setup
@controller = <%= class_name %>Controller.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
<% for method_name in args -%>
def test_<%= method_name %>
result = invoke :<%= method_name %>
assert_equal nil, result
end
<% end -%>
end

View file

@ -0,0 +1,29 @@
class WebServiceGenerator < Rails::Generator::NamedBase
def manifest
record do |m|
# Check for class naming collisions.
m.class_collisions class_path, "#{class_name}Api", "#{class_name}Controller", "#{class_name}ApiTest"
# API and test directories.
m.directory File.join('app/services', class_path)
m.directory File.join('app/controllers', class_path)
m.directory File.join('test/functional', class_path)
# API definition, controller, and functional test.
m.template 'api_definition.rb',
File.join('app/services',
class_path,
"#{file_name}_api.rb")
m.template 'controller.rb',
File.join('app/controllers',
class_path,
"#{file_name}_controller.rb")
m.template 'functional_test.rb',
File.join('test/functional',
class_path,
"#{file_name}_api_test.rb")
end
end
end

View file

@ -0,0 +1,66 @@
#--
# Copyright (C) 2005 Leon Breedt
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
begin
require 'active_support'
require 'action_controller'
require 'active_record'
rescue LoadError
require 'rubygems'
gem 'activesupport', '>= 2.3.0'
gem 'actionpack', '>= 2.3.0'
gem 'activerecord', '>= 2.3.0'
end
$:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/")
require 'action_web_service/support/class_inheritable_options'
require 'action_web_service/support/signature_types'
require 'action_web_service/base'
require 'action_web_service/client'
require 'action_web_service/invocation'
require 'action_web_service/api'
require 'action_web_service/casting'
require 'action_web_service/struct'
require 'action_web_service/container'
require 'action_web_service/protocol'
require 'action_web_service/dispatcher'
require 'action_web_service/scaffolding'
ActionWebService::Base.class_eval do
include ActionWebService::Container::Direct
include ActionWebService::Invocation
end
ActionController::Base.class_eval do
include ActionWebService::Protocol::Discovery
include ActionWebService::Protocol::Soap
include ActionWebService::Protocol::XmlRpc
include ActionWebService::Container::Direct
include ActionWebService::Container::Delegated
include ActionWebService::Container::ActionController
include ActionWebService::Invocation
include ActionWebService::Dispatcher
include ActionWebService::Dispatcher::ActionController
include ActionWebService::Scaffolding
end

View file

@ -0,0 +1,297 @@
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
# Whether to transform the public API method names into camel-cased names
class_inheritable_option :inflect_names, true
# By default only HTTP POST requests are processed
class_inheritable_option :allowed_http_methods, [ :post ]
# 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
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# end
#
# ProjectsApi.has_api_method?('GetCount') #=> false
# ProjectsApi.has_api_method?(:getCount) #=> true
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
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# end
#
# ProjectsApi.has_api_method?(:getCount) #=> false
# ProjectsApi.has_api_method?('GetCount') #=> true
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
#
# ProjectsApi.public_api_method_name('GetCount') #=> "GetCount"
# ProjectsApi.public_api_method_name(:getCount) #=> "GetCount"
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
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# end
#
# ProjectsApi.api_method_name('GetCount') #=> :getCount
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.
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# api_method :getCompletedCount, :returns => [:int]
# end
#
# ProjectsApi.api_methods #=>
# {:getCount=>#<ActionWebService::API::Method:0x24379d8 ...>,
# :getCompletedCount=>#<ActionWebService::API::Method:0x2437794 ...>}
# ProjectsApi.api_methods[:getCount].public_name #=> "GetCount"
def api_methods
read_inheritable_attribute("api_methods") || {}
end
# The Method instance for the given public API method name, if any
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# api_method :getCompletedCount, :returns => [:int]
# end
#
# ProjectsApi.public_api_method_instance('GetCount') #=> <#<ActionWebService::API::Method:0x24379d8 ...>
# ProjectsApi.public_api_method_instance(:getCount) #=> nil
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
#
# class ProjectsApi < ActionWebService::API::Base
# api_method :getCount, :returns => [:int]
# api_method :getCompletedCount, :returns => [:int]
# end
#
# ProjectsApi.api_method_instance(:getCount) #=> <ActionWebService::API::Method:0x24379d8 ...>
# ProjectsApi.api_method_instance('GetCount') #=> <ActionWebService::API::Method:0x24379d8 ...>
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,38 @@
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
# 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,149 @@
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 :decimal
BigDecimal(value.to_s)
when :time
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
if value.kind_of?(Time)
value
elsif value.kind_of?(DateTime)
value.to_time
else
Time.parse(value.to_s)
end
when :date
value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash)
value.kind_of?(Date) ? value : Date.parse(value.to_s)
when :datetime
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s)
end
end
def cast_to_structured_type(value, signature_type) # :nodoc:
obj = nil
# if the canonical classes are the same or if the given value is of
# a type that is derived from the signature_type do not attempt to
# "cast" the value into the signature_type as it's already good to go
obj = (
canonical_type(value.class) == canonical_type(signature_type.type) or
derived_from?(signature_type.type, value.class)
) ? value : 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,113 @@
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
# provides access to the underlying soap driver
attr_reader :driver
# 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,93 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module ActionController # :nodoc:
def self.included(base) # :nodoc:
class << base
include ClassMethods
alias_method_chain :inherited, :api
alias_method_chain :web_service_api, :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,86 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module Delegated # :nodoc:
class ContainerError < ActionWebServiceError # :nodoc:
end
def self.included(base) # :nodoc:
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,69 @@
module ActionWebService # :nodoc:
module Container # :nodoc:
module Direct # :nodoc:
class ContainerError < ActionWebServiceError # :nodoc:
end
def self.included(base) # :nodoc:
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,207 @@
require 'benchmark'
module ActionWebService # :nodoc:
module Dispatcher # :nodoc:
class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc:
def initialize(*args)
super
set_backtrace(caller)
end
end
def self.included(base) # :nodoc:
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, nil]
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)
response_type = (api_method.returns ? api_method.returns[0] : nil)
return_value = api_method.cast_returns(return_value)
else
response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
end
responses << [return_value, response_type]
rescue Exception => e
responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil]
end
end
invocation = invocations[0]
invocation.protocol.encode_multicall_response(responses, 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,379 @@
require 'benchmark'
require 'builder/xmlmarkup'
module ActionWebService # :nodoc:
module Dispatcher # :nodoc:
module ActionController # :nodoc:
def self.included(base) # :nodoc:
class << base
include ClassMethods
alias_method_chain :inherited, :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
method = request.method.to_s.upcase
allowed_methods = self.class.web_service_api ? (self.class.web_service_api.allowed_http_methods || []) : [ :post ]
allowed_methods = allowed_methods.map{|m| m.to_s.upcase }
if !allowed_methods.include?(method)
render :text => "#{method} not supported", :status=>500
return
end
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 :status => 500, :text => "Internal protocol error: #{message}#{backtrace}"
end
end
def web_service_direct_invoke(invocation)
invocation.method_named_params.each do |name, value|
params[name] = value
end
web_service_direct_invoke_without_controller(invocation)
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 :status => 500, :text => 'POST not supported'
end
end
private
def base_uri
host = request.host_with_port
relative_url_root = ::ActionController::Base.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,202 @@
module ActionWebService # :nodoc:
module Invocation # :nodoc:
class InvocationError < ActionWebService::ActionWebServiceError # :nodoc:
end
def self.included(base) # :nodoc:
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.included(base)
base.class_eval do
alias_method_chain :perform_invocation, :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::Request # :nodoc:
def initialize(env = {})
@env = 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,242 @@
require 'soap/mapping'
# hack to improve the .Net interoperability
class SOAP::Mapping::Object
def each_pair
self.__xmlele.each { |n, v| yield n.name, v.to_s }
end
end
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)
@registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true})
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,122 @@
require 'xmlrpc/marshal'
require 'action_web_service/client/xmlrpc_client'
module XMLRPC # :nodoc:
class FaultException # :nodoc:
alias :message :faultString
end
class Create
def wrong_type(value)
if BigDecimal === value
[true, value.to_f]
else
false
end
end
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)
rescue
return nil
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 encode_multicall_response(responses, protocol_options={})
result = responses.map do |return_value, return_type|
if return_value && return_type
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
return_value = [return_value] unless return_value.nil?
end
return_value = false if return_value.nil?
return_value
end
raw_response = XMLRPC::Marshal.dump_response(result)
Response.new(raw_response, 'text/xml', result)
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,281 @@
require 'benchmark'
require 'pathname'
module ActionWebService
module Scaffolding # :nodoc:
class ScaffoldingError < ActionWebServiceError # :nodoc:
end
def self.included(base)
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.html.erb</tt>
# * <tt>action_name/parameters.html.erb</tt>
# * <tt>action_name/result.html.erb</tt>
# * <tt>action_name/layout.html.erb</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__ + 1
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)
self.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
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
end
def render_invocation_scaffold(action)
customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
default_template = scaffold_path(action)
begin
content = @template.render(:file => customized_template)
rescue ActionView::MissingTemplate
content = @template.render(:file => default_template)
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, :use_full_path => true
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".html.erb"
end
def reset_invocation_response
erase_render_results
response.instance_variable_set :@header, Rack::Utils::HeaderHash.new(::ActionController::Response::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,64 @@
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
# 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,226 @@
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 :decimal
:decimal
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 :decimal
BigDecimal
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') do -%>
<%= 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 -%>
<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})" if method && method.expects && 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 = 2
MINOR = 3
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
end

View file

@ -0,0 +1 @@
require 'action_web_service'

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,183 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'webrick'
require 'webrick/log'
require 'singleton'
module ClientTest
class Person < ActionWebService::Struct
member :firstnames, [:string]
member :lastname, :string
def ==(other)
firstnames == other.firstnames && lastname == other.lastname
end
end
class Inner < ActionWebService::Struct
member :name, :string
end
class Outer < ActionWebService::Struct
member :name, :string
member :inner, Inner
end
class User < ActiveRecord::Base
end
module Accounting
class User < ActiveRecord::Base
end
end
class WithModel < ActionWebService::Struct
member :user, User
member :users, [User]
end
class WithMultiDimArray < ActionWebService::Struct
member :pref, [[:string]]
end
class API < ActionWebService::API::Base
api_method :void
api_method :normal, :expects => [:int, :int], :returns => [:int]
api_method :array_return, :returns => [[Person]]
api_method :struct_pass, :expects => [[Person]], :returns => [:bool]
api_method :nil_struct_return, :returns => [Person]
api_method :inner_nil, :returns => [Outer]
api_method :client_container, :returns => [:int]
api_method :named_parameters, :expects => [{:key=>:string}, {:id=>:int}]
api_method :thrower
api_method :user_return, :returns => [User]
api_method :with_model_return, :returns => [WithModel]
api_method :scoped_model_return, :returns => [Accounting::User]
api_method :multi_dim_return, :returns => [WithMultiDimArray]
end
class NullLogOut
def <<(*args); end
end
class Container < ActionController::Base
web_service_api API
attr_accessor :value_void
attr_accessor :value_normal
attr_accessor :value_array_return
attr_accessor :value_struct_pass
attr_accessor :value_named_parameters
def initialize
@value_void = nil
@value_normal = nil
@value_array_return = nil
@value_struct_pass = nil
@value_named_parameters = nil
end
def void
@value_void = @method_params
end
def normal
@value_normal = @method_params
5
end
def array_return
person = Person.new
person.firstnames = ["one", "two"]
person.lastname = "last"
@value_array_return = [person]
end
def struct_pass
@value_struct_pass = @method_params
true
end
def nil_struct_return
nil
end
def inner_nil
Outer.new :name => 'outer', :inner => nil
end
def client_container
50
end
def named_parameters
@value_named_parameters = @method_params
end
def thrower
raise "Hi"
end
def user_return
User.find(1)
end
def with_model_return
WithModel.new :user => User.find(1), :users => User.find(:all)
end
def scoped_model_return
Accounting::User.find(1)
end
def multi_dim_return
WithMultiDimArray.new :pref => [%w{pref1 value1}, %w{pref2 value2}]
end
end
class AbstractClientLet < WEBrick::HTTPServlet::AbstractServlet
def initialize(controller)
@controller = controller
end
def get_instance(*args)
self
end
def require_path_info?
false
end
def do_GET(req, res)
raise WEBrick::HTTPStatus::MethodNotAllowed, "GET request not allowed."
end
def do_POST(req, res)
raise NotImplementedError
end
end
class AbstractServer
include ClientTest
include Singleton
attr :container
def initialize
@container = Container.new
@clientlet = create_clientlet(@container)
log = WEBrick::BasicLog.new(NullLogOut.new)
@server = WEBrick::HTTPServer.new(:Port => server_port, :Logger => log, :AccessLog => [])
@server.mount('/', @clientlet)
@thr = Thread.new { @server.start }
until @server.status == :Running; end
at_exit { @server.stop; @thr.join }
end
protected
def create_clientlet
raise NotImplementedError
end
def server_port
raise NotImplementedError
end
end
end

View file

@ -0,0 +1,548 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'stringio'
class ActionController::Base; def rescue_action(e) raise e end; end
module DispatcherTest
Utf8String = "One World Caf\303\251"
WsdlNamespace = 'http://rubyonrails.com/some/namespace'
class Node < ActiveRecord::Base
def initialize(*args)
super(*args)
@new_record = false
end
class << self
def name
"DispatcherTest::Node"
end
def columns(*args)
[
ActiveRecord::ConnectionAdapters::Column.new('id', 0, 'int'),
ActiveRecord::ConnectionAdapters::Column.new('name', nil, 'string'),
ActiveRecord::ConnectionAdapters::Column.new('description', nil, 'string'),
]
end
def connection
self
end
end
end
class Person < ActionWebService::Struct
member :id, :int
member :name, :string
def ==(other)
self.id == other.id && self.name == other.name
end
end
class API < ActionWebService::API::Base
api_method :add, :expects => [:int, :int], :returns => [:int]
api_method :interceptee
api_method :struct_return, :returns => [[Node]]
api_method :void
end
class DirectAPI < ActionWebService::API::Base
api_method :add, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int]
api_method :add2, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int]
api_method :before_filtered
api_method :after_filtered, :returns => [[:int]]
api_method :struct_return, :returns => [[Node]]
api_method :struct_pass, :expects => [{:person => Person}]
api_method :base_struct_return, :returns => [[Person]]
api_method :hash_struct_return, :returns => [[Person]]
api_method :thrower
api_method :void
api_method :test_utf8, :returns => [:string]
api_method :hex, :expects => [:base64], :returns => [:string]
api_method :unhex, :expects => [:string], :returns => [:base64]
api_method :time, :expects => [:time], :returns => [:time]
end
class VirtualAPI < ActionWebService::API::Base
default_api_method :fallback
end
class Service < ActionWebService::Base
web_service_api API
before_invocation :do_intercept, :only => [:interceptee]
attr :added
attr :intercepted
attr :void_called
def initialize
@void_called = false
end
def add(a, b)
@added = a + b
end
def interceptee
@intercepted = false
end
def struct_return
n1 = Node.new('id' => 1, 'name' => 'node1', 'description' => 'Node 1')
n2 = Node.new('id' => 2, 'name' => 'node2', 'description' => 'Node 2')
[n1, n2]
end
def void(*args)
@void_called = args
end
def do_intercept(name, args)
[false, "permission denied"]
end
end
class MTAPI < ActionWebService::API::Base
inflect_names false
api_method :getCategories, :returns => [[:string]]
api_method :bool, :returns => [:bool]
api_method :alwaysFail
api_method :person, :returns => [Person]
end
class BloggerAPI < ActionWebService::API::Base
inflect_names false
api_method :getCategories, :returns => [[:string]]
api_method :str, :expects => [:int], :returns => [:string]
api_method :alwaysFail
end
class MTService < ActionWebService::Base
web_service_api MTAPI
def getCategories
["mtCat1", "mtCat2"]
end
def bool
'y'
end
def alwaysFail
raise "MT AlwaysFail"
end
def person
Person.new('id' => 1, 'name' => 'person1')
end
end
class BloggerService < ActionWebService::Base
web_service_api BloggerAPI
def getCategories
["bloggerCat1", "bloggerCat2"]
end
def str(int)
unless int.is_a?(Integer)
raise "Not an integer!"
end
500 + int
end
def alwaysFail
raise "Blogger AlwaysFail"
end
end
class AbstractController < ActionController::Base
def generate_wsdl
self.request ||= ::ActionController::TestRequest.new
to_wsdl
end
end
class DelegatedController < AbstractController
web_service_dispatching_mode :delegated
wsdl_namespace WsdlNamespace
web_service(:test_service) { @service ||= Service.new; @service }
end
class LayeredController < AbstractController
web_service_dispatching_mode :layered
wsdl_namespace WsdlNamespace
web_service(:mt) { @mt_service ||= MTService.new; @mt_service }
web_service(:blogger) { @blogger_service ||= BloggerService.new; @blogger_service }
end
class DirectController < AbstractController
web_service_api DirectAPI
web_service_dispatching_mode :direct
wsdl_namespace WsdlNamespace
before_invocation :alwaysfail, :only => [:before_filtered]
after_invocation :alwaysok, :only => [:after_filtered]
attr :added
attr :added2
attr :before_filter_called
attr :before_filter_target_called
attr :after_filter_called
attr :after_filter_target_called
attr :void_called
attr :struct_pass_value
def initialize
@before_filter_called = false
@before_filter_target_called = false
@after_filter_called = false
@after_filter_target_called = false
@void_called = false
@struct_pass_value = false
end
def add
@added = params['a'] + params['b']
end
def add2(a, b)
@added2 = a + b
end
def before_filtered
@before_filter_target_called = true
end
def after_filtered
@after_filter_target_called = true
[5, 6, 7]
end
def thrower
raise "Hi, I'm an exception"
end
def struct_return
n1 = Node.new('id' => 1, 'name' => 'node1', 'description' => 'Node 1')
n2 = Node.new('id' => 2, 'name' => 'node2', 'description' => 'Node 2')
[n1, n2]
end
def struct_pass(person)
@struct_pass_value = person
end
def base_struct_return
p1 = Person.new('id' => 1, 'name' => 'person1')
p2 = Person.new('id' => 2, 'name' => 'person2')
[p1, p2]
end
def hash_struct_return
p1 = { :id => '1', 'name' => 'test' }
p2 = { 'id' => '2', :name => 'person2' }
[p1, p2]
end
def void
@void_called = @method_params
end
def test_utf8
Utf8String
end
def hex(s)
return s.unpack("H*")[0]
end
def unhex(s)
return [s].pack("H*")
end
def time(t)
t
end
protected
def alwaysfail(method_name, params)
@before_filter_called = true
false
end
def alwaysok(method_name, params, return_value)
@after_filter_called = true
end
end
class VirtualController < AbstractController
web_service_api VirtualAPI
wsdl_namespace WsdlNamespace
def fallback
"fallback!"
end
end
end
module DispatcherCommonTests
def test_direct_dispatching
assert_equal(70, do_method_call(@direct_controller, 'Add', 20, 50))
assert_equal(70, @direct_controller.added)
assert_equal(50, do_method_call(@direct_controller, 'Add2', 25, 25))
assert_equal(50, @direct_controller.added2)
assert(@direct_controller.void_called == false)
assert(do_method_call(@direct_controller, 'Void', 3, 4, 5).nil?)
assert(@direct_controller.void_called == [])
result = do_method_call(@direct_controller, 'BaseStructReturn')
assert(result[0].is_a?(DispatcherTest::Person))
assert(result[1].is_a?(DispatcherTest::Person))
assert_equal("cafe", do_method_call(@direct_controller, 'Hex', "\xca\xfe"))
assert_equal("\xca\xfe", do_method_call(@direct_controller, 'Unhex', "cafe"))
time = Time.gm(1998, "Feb", 02, 15, 12, 01)
assert_equal(time, do_method_call(@direct_controller, 'Time', time))
end
def test_direct_entrypoint
assert(@direct_controller.respond_to?(:api))
end
def test_virtual_dispatching
assert_equal("fallback!", do_method_call(@virtual_controller, 'VirtualOne'))
assert_equal("fallback!", do_method_call(@virtual_controller, 'VirtualTwo'))
end
def test_direct_filtering
assert_equal(false, @direct_controller.before_filter_called)
assert_equal(false, @direct_controller.before_filter_target_called)
do_method_call(@direct_controller, 'BeforeFiltered')
assert_equal(true, @direct_controller.before_filter_called)
assert_equal(false, @direct_controller.before_filter_target_called)
assert_equal(false, @direct_controller.after_filter_called)
assert_equal(false, @direct_controller.after_filter_target_called)
assert_equal([5, 6, 7], do_method_call(@direct_controller, 'AfterFiltered'))
assert_equal(true, @direct_controller.after_filter_called)
assert_equal(true, @direct_controller.after_filter_target_called)
end
def test_delegated_dispatching
assert_equal(130, do_method_call(@delegated_controller, 'Add', 50, 80))
service = @delegated_controller.web_service_object(:test_service)
assert_equal(130, service.added)
@delegated_controller.web_service_exception_reporting = true
assert(service.intercepted.nil?)
result = do_method_call(@delegated_controller, 'Interceptee')
assert(service.intercepted.nil?)
assert(is_exception?(result))
assert_match(/permission denied/, exception_message(result))
result = do_method_call(@delegated_controller, 'NonExistentMethod')
assert(is_exception?(result))
assert_match(/NonExistentMethod/, exception_message(result))
assert(service.void_called == false)
assert(do_method_call(@delegated_controller, 'Void', 3, 4, 5).nil?)
assert(service.void_called == [])
end
def test_garbage_request
[@direct_controller, @delegated_controller].each do |controller|
controller.class.web_service_exception_reporting = true
send_garbage_request = lambda do
service_name = service_name(controller)
request = protocol.encode_action_pack_request(service_name, 'broken, method, name!', 'broken request body', :request_class => ActionController::TestRequest)
response = ActionController::TestResponse.new
controller.process(request, response)
# puts response.body
assert(response.status =~ /^500/)
end
send_garbage_request.call
controller.class.web_service_exception_reporting = false
send_garbage_request.call
end
end
def test_exception_marshaling
@direct_controller.web_service_exception_reporting = true
result = do_method_call(@direct_controller, 'Thrower')
assert(is_exception?(result))
assert_equal("Hi, I'm an exception", exception_message(result))
@direct_controller.web_service_exception_reporting = false
result = do_method_call(@direct_controller, 'Thrower')
assert(exception_message(result) != "Hi, I'm an exception")
end
def test_ar_struct_return
[@direct_controller, @delegated_controller].each do |controller|
result = do_method_call(controller, 'StructReturn')
assert(result[0].is_a?(DispatcherTest::Node))
assert(result[1].is_a?(DispatcherTest::Node))
assert_equal('node1', result[0].name)
assert_equal('node2', result[1].name)
end
end
def test_casting
assert_equal 70, do_method_call(@direct_controller, 'Add', "50", "20")
assert_equal false, @direct_controller.struct_pass_value
person = DispatcherTest::Person.new(:id => 1, :name => 'test')
result = do_method_call(@direct_controller, 'StructPass', person)
assert(nil == result || true == result)
assert_equal person, @direct_controller.struct_pass_value
assert !person.equal?(@direct_controller.struct_pass_value)
result = do_method_call(@direct_controller, 'StructPass', {'id' => '1', 'name' => 'test'})
case
when soap?
assert_equal(person, @direct_controller.struct_pass_value)
assert !person.equal?(@direct_controller.struct_pass_value)
when xmlrpc?
assert_equal(person, @direct_controller.struct_pass_value)
assert !person.equal?(@direct_controller.struct_pass_value)
end
assert_equal person, do_method_call(@direct_controller, 'HashStructReturn')[0]
result = do_method_call(@direct_controller, 'StructPass', {'id' => '1', 'name' => 'test', 'nonexistent_attribute' => 'value'})
case
when soap?
assert_equal(person, @direct_controller.struct_pass_value)
assert !person.equal?(@direct_controller.struct_pass_value)
when xmlrpc?
assert_equal(person, @direct_controller.struct_pass_value)
assert !person.equal?(@direct_controller.struct_pass_value)
end
end
def test_logging
buf = ""
ActionController::Base.logger = Logger.new(StringIO.new(buf))
test_casting
test_garbage_request
test_exception_marshaling
ActionController::Base.logger = nil
assert_match /Web Service Response/, buf
assert_match /Web Service Request/, buf
end
def test_allowed_http_methods
webservice_api = @direct_controller.class.web_service_api
original_allowed_http_methods = webservice_api.allowed_http_methods
# check defaults
assert_equal false, http_method_allowed?(:get)
assert_equal false, http_method_allowed?(:head)
assert_equal false, http_method_allowed?(:put)
assert_equal false, http_method_allowed?(:delete)
assert_equal true, http_method_allowed?(:post)
# allow get and post
webservice_api.allowed_http_methods = [ :get, :post ]
assert_equal true, http_method_allowed?(:get)
assert_equal true, http_method_allowed?(:post)
# allow get only
webservice_api.allowed_http_methods = [ :get ]
assert_equal true, http_method_allowed?(:get)
assert_equal false, http_method_allowed?(:post)
# allow delete only
webservice_api.allowed_http_methods = [ 'DELETE' ]
assert_equal false, http_method_allowed?(:get)
assert_equal false, http_method_allowed?(:head)
assert_equal false, http_method_allowed?(:post)
assert_equal false, http_method_allowed?(:put)
assert_equal true, http_method_allowed?(:delete)
ensure
webservice_api.allowed_http_methods = original_allowed_http_methods
end
protected
def service_name(container)
raise NotImplementedError
end
def exception_message(obj)
raise NotImplementedError
end
def is_exception?(obj)
raise NotImplementedError
end
def protocol
@protocol
end
def soap?
protocol.is_a? ActionWebService::Protocol::Soap::SoapProtocol
end
def xmlrpc?
protocol.is_a? ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
end
def do_method_call(container, public_method_name, *params)
request_env = {}
mode = container.web_service_dispatching_mode
case mode
when :direct
service_name = service_name(container)
api = container.class.web_service_api
method = api.public_api_method_instance(public_method_name)
when :delegated
service_name = service_name(container)
api = container.web_service_object(service_name).class.web_service_api
method = api.public_api_method_instance(public_method_name)
when :layered
service_name = nil
real_method_name = nil
if public_method_name =~ /^([^\.]+)\.(.*)$/
service_name = $1
real_method_name = $2
end
if soap?
public_method_name = real_method_name
request_env['HTTP_SOAPACTION'] = "/soap/#{service_name}/#{real_method_name}"
end
api = container.web_service_object(service_name.to_sym).class.web_service_api rescue nil
method = api.public_api_method_instance(real_method_name) rescue nil
service_name = self.service_name(container)
end
protocol.register_api(api)
virtual = false
unless method
virtual = true
method ||= ActionWebService::API::Method.new(public_method_name.underscore.to_sym, public_method_name, nil, nil)
end
body = protocol.encode_request(public_method_name, params.dup, method.expects)
# puts body
ap_request = protocol.encode_action_pack_request(service_name, public_method_name, body, :request_class => ActionController::TestRequest)
ap_request.env.update(request_env)
ap_response = ActionController::TestResponse.new
container.process(ap_request, ap_response)
# puts ap_response.body
@response_body = ap_response.body
public_method_name, return_value = protocol.decode_response(ap_response.body)
unless is_exception?(return_value) || virtual
return_value = method.cast_returns(return_value)
end
if soap?
# http://dev.rubyonrails.com/changeset/920
assert_match(/Response$/, public_method_name) unless public_method_name == "fault"
end
return_value
end
def http_method_allowed?(method)
method = method.to_s.upcase
test_request = ActionController::TestRequest.new
test_request.action = 'api'
test_response = ActionController::TestResponse.new
test_request.env['REQUEST_METHOD'] = method
result = @direct_controller.process(test_request, test_response)
result.body =~ /(GET|POST|PUT|DELETE|TRACE|CONNECT) not supported/ ? false : true
end
end

View file

@ -0,0 +1,39 @@
$: << "#{File.dirname(__FILE__)}/../lib"
ENV["RAILS_ENV"] = "test"
require 'rubygems'
require 'test/unit'
require 'action_web_service'
require 'action_controller'
require 'action_controller/test_case'
require 'action_view'
require 'action_view/test_case'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
ActiveRecord::Base.logger = ActionController::Base.logger = Logger.new("debug.log")
begin
require 'activerecord'
require "active_record/test_case"
require "active_record/fixtures" unless Object.const_defined?(:Fixtures)
rescue LoadError => e
fail "\nFailed to load activerecord: #{e}"
end
ActiveRecord::Base.configurations = {
'mysql' => {
:adapter => "mysql",
:username => "root",
:encoding => "utf8",
:database => "actionwebservice_unittest"
}
}
ActiveRecord::Base.establish_connection 'mysql'
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/"
end

View file

@ -0,0 +1,102 @@
require File.dirname(__FILE__) + '/abstract_unit'
module APITest
class API < ActionWebService::API::Base
api_method :void
api_method :expects_and_returns, :expects_and_returns => [:string]
api_method :expects, :expects => [:int, :bool]
api_method :returns, :returns => [:int, [:string]]
api_method :named_signature, :expects => [{:appkey=>:int}, {:publish=>:bool}]
api_method :string_types, :expects => ['int', 'string', 'bool', 'base64']
api_method :class_types, :expects => [TrueClass, Bignum, String]
end
end
class TC_API < ActiveSupport::TestCase
API = APITest::API
def test_api_method_declaration
%w(
void
expects_and_returns
expects
returns
named_signature
string_types
class_types
).each do |name|
name = name.to_sym
public_name = API.public_api_method_name(name)
assert(API.has_api_method?(name))
assert(API.has_public_api_method?(public_name))
assert(API.api_method_name(public_name) == name)
assert(API.api_methods.has_key?(name))
end
end
def test_signature_canonicalization
assert_equal(nil, API.api_methods[:void].expects)
assert_equal(nil, API.api_methods[:void].returns)
assert_equal([String], API.api_methods[:expects_and_returns].expects.map{|x| x.type_class})
assert_equal([String], API.api_methods[:expects_and_returns].returns.map{|x| x.type_class})
assert_equal([Integer, TrueClass], API.api_methods[:expects].expects.map{|x| x.type_class})
assert_equal(nil, API.api_methods[:expects].returns)
assert_equal(nil, API.api_methods[:returns].expects)
assert_equal([Integer, [String]], API.api_methods[:returns].returns.map{|x| x.array?? [x.element_type.type_class] : x.type_class})
assert_equal([[:appkey, Integer], [:publish, TrueClass]], API.api_methods[:named_signature].expects.map{|x| [x.name, x.type_class]})
assert_equal(nil, API.api_methods[:named_signature].returns)
assert_equal([Integer, String, TrueClass, ActionWebService::Base64], API.api_methods[:string_types].expects.map{|x| x.type_class})
assert_equal(nil, API.api_methods[:string_types].returns)
assert_equal([TrueClass, Integer, String], API.api_methods[:class_types].expects.map{|x| x.type_class})
assert_equal(nil, API.api_methods[:class_types].returns)
end
def test_not_instantiable
assert_raises(NoMethodError) do
API.new
end
end
def test_api_errors
assert_raises(ActionWebService::ActionWebServiceError) do
klass = Class.new(ActionWebService::API::Base) do
api_method :test, :expects => [ActiveRecord::Base]
end
end
klass = Class.new(ActionWebService::API::Base) do
allow_active_record_expects true
api_method :test2, :expects => [ActiveRecord::Base]
end
assert_raises(ActionWebService::ActionWebServiceError) do
klass = Class.new(ActionWebService::API::Base) do
api_method :test, :invalid => [:int]
end
end
end
def test_parameter_names
method = API.api_methods[:named_signature]
assert_equal 0, method.expects_index_of(:appkey)
assert_equal 1, method.expects_index_of(:publish)
assert_equal 1, method.expects_index_of('publish')
assert_equal 0, method.expects_index_of('appkey')
assert_equal -1, method.expects_index_of('blah')
assert_equal -1, method.expects_index_of(:missing)
assert_equal -1, API.api_methods[:void].expects_index_of('test')
end
def test_parameter_hash
method = API.api_methods[:named_signature]
hash = method.expects_to_hash([5, false])
assert_equal({:appkey => 5, :publish => false}, hash)
end
def test_api_methods_compat
sig = API.api_methods[:named_signature][:expects]
assert_equal [{:appkey=>Integer}, {:publish=>TrueClass}], sig
end
def test_to_s
assert_equal 'void Expects(int param0, bool param1)', APITest::API.api_methods[:expects].to_s
end
end

View file

@ -0,0 +1,3 @@
class AutoLoadAPI < ActionWebService::API::Base
api_method :void
end

View file

@ -0,0 +1,42 @@
require File.dirname(__FILE__) + '/abstract_unit'
module BaseTest
class API < ActionWebService::API::Base
api_method :add, :expects => [:int, :int], :returns => [:int]
api_method :void
end
class PristineAPI < ActionWebService::API::Base
inflect_names false
api_method :add
api_method :under_score
end
class Service < ActionWebService::Base
web_service_api API
def add(a, b)
end
def void
end
end
class PristineService < ActionWebService::Base
web_service_api PristineAPI
def add
end
def under_score
end
end
end
class TC_Base < ActiveSupport::TestCase
def test_options
assert(BaseTest::PristineService.web_service_api.inflect_names == false)
assert(BaseTest::Service.web_service_api.inflect_names == true)
end
end

View file

@ -0,0 +1,94 @@
require File.dirname(__FILE__) + '/abstract_unit'
module CastingTest
class A < ActionWebService::Struct; end
class B < A; end
class API < ActionWebService::API::Base
api_method :int, :expects => [:int]
api_method :str, :expects => [:string]
api_method :base64, :expects => [:base64]
api_method :bool, :expects => [:bool]
api_method :float, :expects => [:float]
api_method :time, :expects => [:time]
api_method :datetime, :expects => [:datetime]
api_method :date, :expects => [:date]
api_method :int_array, :expects => [[:int]]
api_method :str_array, :expects => [[:string]]
api_method :bool_array, :expects => [[:bool]]
api_method :a, :expects => [A]
end
end
class TC_Casting < Test::Unit::TestCase
include CastingTest
def test_base_type_casting_valid
assert_equal 10000, cast_expects(:int, '10000')[0]
assert_equal '10000', cast_expects(:str, 10000)[0]
base64 = cast_expects(:base64, 10000)[0]
assert_equal '10000', base64
assert_instance_of ActionWebService::Base64, base64
[1, '1', 'true', 'y', 'yes'].each do |val|
assert_equal true, cast_expects(:bool, val)[0]
end
[0, '0', 'false', 'n', 'no'].each do |val|
assert_equal false, cast_expects(:bool, val)[0]
end
assert_equal 3.14159, cast_expects(:float, '3.14159')[0]
now = Time.at(Time.now.tv_sec)
casted = cast_expects(:time, now.to_s)[0]
assert_equal now, casted
now = DateTime.now
assert_equal now.to_s, cast_expects(:datetime, now.to_s)[0].to_s
today = Date.today
assert_equal today, cast_expects(:date, today.to_s)[0]
end
def test_base_type_casting_invalid
assert_raises ArgumentError do
cast_expects(:int, 'this is not a number')
end
assert_raises ActionWebService::Casting::CastingError do
# neither true or false ;)
cast_expects(:bool, 'i always lie')
end
assert_raises ArgumentError do
cast_expects(:float, 'not a float')
end
assert_raises ArgumentError do
cast_expects(:time, '111111111111111111111111111111111')
end
assert_raises ArgumentError do
cast_expects(:datetime, '-1')
end
assert_raises ArgumentError do
cast_expects(:date, '')
end
end
def test_array_type_casting
assert_equal [1, 2, 3213992, 4], cast_expects(:int_array, ['1', '2', '3213992', '4'])[0]
assert_equal ['one', 'two', '5.0', '200', nil, 'true'], cast_expects(:str_array, [:one, 'two', 5.0, 200, nil, true])[0]
assert_equal [true, nil, true, true, false], cast_expects(:bool_array, ['1', nil, 'y', true, 'false'])[0]
end
def test_array_type_casting_failure
assert_raises ActionWebService::Casting::CastingError do
cast_expects(:bool_array, ['false', 'blahblah'])
end
assert_raises ArgumentError do
cast_expects(:int_array, ['1', '2.021', '4'])
end
end
def test_structured_type_casting_with_polymorphism
assert cast_expects(:a, B.new)[0].is_a?(B)
end
private
def cast_expects(method_name, *args)
API.api_method_instance(method_name.to_sym).cast_expects([*args])
end
end

View file

@ -0,0 +1,155 @@
require File.dirname(__FILE__) + '/abstract_client'
module ClientSoapTest
PORT = 8998
class SoapClientLet < ClientTest::AbstractClientLet
def do_POST(req, res)
test_request = ActionController::TestRequest.new
test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1]
test_request.env['REQUEST_METHOD'] = "POST"
test_request.env['HTTP_CONTENTTYPE'] = 'text/xml'
test_request.env['HTTP_SOAPACTION'] = req.header['soapaction'][0]
test_request.env['RAW_POST_DATA'] = req.body
response = ActionController::TestResponse.new
@controller.process(test_request, response)
res.header['content-type'] = 'text/xml'
res.body = response.body
rescue Exception => e
$stderr.puts e.message
$stderr.puts e.backtrace.join("\n")
ensure
ActiveRecord::Base.clear_active_connections!
end
end
class ClientContainer < ActionController::Base
web_client_api :client, :soap, "http://localhost:#{PORT}/client/api", :api => ClientTest::API
web_client_api :invalid, :null, "", :api => true
def get_client
client
end
def get_invalid
invalid
end
end
class SoapServer < ClientTest::AbstractServer
def create_clientlet(controller)
SoapClientLet.new(controller)
end
def server_port
PORT
end
end
end
class TC_ClientSoap < ActiveSupport::TestCase
include ClientTest
include ClientSoapTest
fixtures :users
def setup
@server = SoapServer.instance
@container = @server.container
@client = ActionWebService::Client::Soap.new(API, "http://localhost:#{@server.server_port}/client/api")
end
def test_void
assert(@container.value_void.nil?)
@client.void
assert(!@container.value_void.nil?)
end
def test_normal
assert(@container.value_normal.nil?)
assert_equal(5, @client.normal(5, 6))
assert_equal([5, 6], @container.value_normal)
assert_equal(5, @client.normal("7", "8"))
assert_equal([7, 8], @container.value_normal)
assert_equal(5, @client.normal(true, false))
end
def test_array_return
assert(@container.value_array_return.nil?)
new_person = Person.new
new_person.firstnames = ["one", "two"]
new_person.lastname = "last"
assert_equal([new_person], @client.array_return)
assert_equal([new_person], @container.value_array_return)
end
def test_struct_pass
assert(@container.value_struct_pass.nil?)
new_person = Person.new
new_person.firstnames = ["one", "two"]
new_person.lastname = "last"
assert_equal(true, @client.struct_pass([new_person]))
assert_equal([[new_person]], @container.value_struct_pass)
end
def test_nil_struct_return
assert_nil @client.nil_struct_return
end
def test_inner_nil
outer = @client.inner_nil
assert_equal 'outer', outer.name
assert_nil outer.inner
end
def test_client_container
assert_equal(50, ClientContainer.new.get_client.client_container)
assert(ClientContainer.new.get_invalid.nil?)
end
def test_named_parameters
assert(@container.value_named_parameters.nil?)
assert(@client.named_parameters("key", 5).nil?)
assert_equal(["key", 5], @container.value_named_parameters)
end
def test_capitalized_method_name
@container.value_normal = nil
assert_equal(5, @client.Normal(5, 6))
assert_equal([5, 6], @container.value_normal)
@container.value_normal = nil
end
def test_model_return
user = @client.user_return
assert_equal 1, user.id
assert_equal 'Kent', user.name
assert user.active?
assert_kind_of Date, user.created_on
assert_equal Date.today, user.created_on
assert_equal BigDecimal('12.2'), user.balance
end
def test_with_model
with_model = @client.with_model_return
assert_equal 'Kent', with_model.user.name
assert_equal 2, with_model.users.size
with_model.users.each do |user|
assert_kind_of User, user
end
end
def test_scoped_model_return
scoped_model = @client.scoped_model_return
assert_kind_of Accounting::User, scoped_model
assert_equal 'Kent', scoped_model.name
end
def test_multi_dim_return
md_struct = @client.multi_dim_return
assert_kind_of Array, md_struct.pref
assert_equal 2, md_struct.pref.size
assert_kind_of Array, md_struct.pref[0]
end
end

View file

@ -0,0 +1,153 @@
require File.dirname(__FILE__) + '/abstract_client'
module ClientXmlRpcTest
PORT = 8999
class XmlRpcClientLet < ClientTest::AbstractClientLet
def do_POST(req, res)
test_request = ActionController::TestRequest.new
test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1]
test_request.env['REQUEST_METHOD'] = "POST"
test_request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
test_request.env['RAW_POST_DATA'] = req.body
response = ActionController::TestResponse.new
@controller.process(test_request, response)
res.header['content-type'] = 'text/xml'
res.body = response.body
rescue Exception => e
$stderr.puts e.message
$stderr.puts e.backtrace.join("\n")
ensure
ActiveRecord::Base.clear_active_connections!
end
end
class ClientContainer < ActionController::Base
web_client_api :client, :xmlrpc, "http://localhost:#{PORT}/client/api", :api => ClientTest::API
def get_client
client
end
end
class XmlRpcServer < ClientTest::AbstractServer
def create_clientlet(controller)
XmlRpcClientLet.new(controller)
end
def server_port
PORT
end
end
end
class TC_ClientXmlRpc < ActiveSupport::TestCase
include ClientTest
include ClientXmlRpcTest
fixtures :users
def setup
@server = XmlRpcServer.instance
@container = @server.container
@client = ActionWebService::Client::XmlRpc.new(API, "http://localhost:#{@server.server_port}/client/api")
end
def test_void
assert(@container.value_void.nil?)
@client.void
assert(!@container.value_void.nil?)
end
def test_normal
assert(@container.value_normal.nil?)
assert_equal(5, @client.normal(5, 6))
assert_equal([5, 6], @container.value_normal)
assert_equal(5, @client.normal("7", "8"))
assert_equal([7, 8], @container.value_normal)
assert_equal(5, @client.normal(true, false))
end
def test_array_return
assert(@container.value_array_return.nil?)
new_person = Person.new
new_person.firstnames = ["one", "two"]
new_person.lastname = "last"
assert_equal([new_person], @client.array_return)
assert_equal([new_person], @container.value_array_return)
end
def test_struct_pass
assert(@container.value_struct_pass.nil?)
new_person = Person.new
new_person.firstnames = ["one", "two"]
new_person.lastname = "last"
assert_equal(true, @client.struct_pass([new_person]))
assert_equal([[new_person]], @container.value_struct_pass)
end
def test_nil_struct_return
assert_equal false, @client.nil_struct_return
end
def test_inner_nil
outer = @client.inner_nil
assert_equal 'outer', outer.name
assert_nil outer.inner
end
def test_client_container
assert_equal(50, ClientContainer.new.get_client.client_container)
end
def test_named_parameters
assert(@container.value_named_parameters.nil?)
assert_equal(false, @client.named_parameters("xxx", 7))
assert_equal(["xxx", 7], @container.value_named_parameters)
end
def test_exception
assert_raises(ActionWebService::Client::ClientError) do
assert(@client.thrower)
end
end
def test_invalid_signature
assert_raises(ArgumentError) do
@client.normal
end
end
def test_model_return
user = @client.user_return
assert_equal 1, user.id
assert_equal 'Kent', user.name
assert user.active?
assert_kind_of Time, user.created_on
assert_equal Time.utc(Time.now.year, Time.now.month, Time.now.day), user.created_on
assert_equal BigDecimal('12.2'), user.balance
end
def test_with_model
with_model = @client.with_model_return
assert_equal 'Kent', with_model.user.name
assert_equal 2, with_model.users.size
with_model.users.each do |user|
assert_kind_of User, user
end
end
def test_scoped_model_return
scoped_model = @client.scoped_model_return
assert_kind_of Accounting::User, scoped_model
assert_equal 'Kent', scoped_model.name
end
def test_multi_dim_return
md_struct = @client.multi_dim_return
assert_kind_of Array, md_struct.pref
assert_equal 2, md_struct.pref.size
assert_kind_of Array, md_struct.pref[0]
end
end

View file

@ -0,0 +1,73 @@
require File.dirname(__FILE__) + '/abstract_unit'
module ContainerTest
$immediate_service = Object.new
$deferred_service = Object.new
class DelegateContainer < ActionController::Base
web_service_dispatching_mode :delegated
attr :flag
attr :previous_flag
def initialize
@previous_flag = nil
@flag = true
end
web_service :immediate_service, $immediate_service
web_service(:deferred_service) { @previous_flag = @flag; @flag = false; $deferred_service }
end
class DirectContainer < ActionController::Base
web_service_dispatching_mode :direct
end
class InvalidContainer
include ActionWebService::Container::Direct
end
end
class TC_Container < Test::Unit::TestCase
include ContainerTest
def setup
@delegate_container = DelegateContainer.new
@direct_container = DirectContainer.new
end
def test_registration
assert(DelegateContainer.has_web_service?(:immediate_service))
assert(DelegateContainer.has_web_service?(:deferred_service))
assert(!DelegateContainer.has_web_service?(:fake_service))
assert_raises(ActionWebService::Container::Delegated::ContainerError) do
DelegateContainer.web_service('invalid')
end
end
def test_service_object
assert_raises(ActionWebService::Container::Delegated::ContainerError) do
@delegate_container.web_service_object(:nonexistent)
end
assert(@delegate_container.flag == true)
assert(@delegate_container.web_service_object(:immediate_service) == $immediate_service)
assert(@delegate_container.previous_flag.nil?)
assert(@delegate_container.flag == true)
assert(@delegate_container.web_service_object(:deferred_service) == $deferred_service)
assert(@delegate_container.previous_flag == true)
assert(@delegate_container.flag == false)
end
def test_direct_container
assert(DirectContainer.web_service_dispatching_mode == :direct)
end
def test_validity
assert_raises(ActionWebService::Container::Direct::ContainerError) do
InvalidContainer.web_service_api :test
end
assert_raises(ActionWebService::Container::Direct::ContainerError) do
InvalidContainer.web_service_api 50.0
end
end
end

View file

@ -0,0 +1,138 @@
$:.unshift(File.dirname(__FILE__) + '/apis')
require File.dirname(__FILE__) + '/abstract_dispatcher'
require 'wsdl/parser'
class ActionController::Base
class << self
alias :inherited_without_name_error :inherited
def inherited(child)
begin
inherited_without_name_error(child)
rescue NameError => e
end
end
end
end
class AutoLoadController < ActionController::Base; end
class FailingAutoLoadController < ActionController::Base; end
class BrokenAutoLoadController < ActionController::Base; end
class TC_DispatcherActionControllerSoap < Test::Unit::TestCase
include DispatcherTest
include DispatcherCommonTests
def setup
@direct_controller = DirectController.new
@delegated_controller = DelegatedController.new
@virtual_controller = VirtualController.new
@layered_controller = LayeredController.new
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@direct_controller)
end
def test_wsdl_generation
ensure_valid_wsdl_generation DelegatedController.new, DispatcherTest::WsdlNamespace
ensure_valid_wsdl_generation DirectController.new, DispatcherTest::WsdlNamespace
end
def test_wsdl_action
delegated_types = ensure_valid_wsdl_action DelegatedController.new
delegated_names = delegated_types.map{|x| x.name.name}
assert(delegated_names.include?('DispatcherTest..NodeArray'))
assert(delegated_names.include?('DispatcherTest..Node'))
direct_types = ensure_valid_wsdl_action DirectController.new
direct_names = direct_types.map{|x| x.name.name}
assert(direct_names.include?('DispatcherTest..NodeArray'))
assert(direct_names.include?('DispatcherTest..Node'))
assert(direct_names.include?('IntegerArray'))
end
def test_autoloading
assert(!AutoLoadController.web_service_api.nil?)
assert(AutoLoadController.web_service_api.has_public_api_method?('Void'))
assert(FailingAutoLoadController.web_service_api.nil?)
assert_raises(MissingSourceFile) do
FailingAutoLoadController.require_web_service_api :blah
end
assert_raises(ArgumentError) do
FailingAutoLoadController.require_web_service_api 50.0
end
assert(BrokenAutoLoadController.web_service_api.nil?)
end
def test_layered_dispatching
mt_cats = do_method_call(@layered_controller, 'mt.getCategories')
assert_equal(["mtCat1", "mtCat2"], mt_cats)
blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories')
assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats)
end
def test_utf8
@direct_controller.web_service_exception_reporting = true
$KCODE = 'u'
assert_equal(Utf8String, do_method_call(@direct_controller, 'TestUtf8'))
retval = SOAP::Processor.unmarshal(@response_body).body.response
assert retval.is_a?(SOAP::SOAPString)
# If $KCODE is not set to UTF-8, any strings with non-ASCII UTF-8 data
# will be sent back as base64 by SOAP4R. By the time we get it here though,
# it will be decoded back into a string. So lets read the base64 value
# from the message body directly.
$KCODE = 'NONE'
do_method_call(@direct_controller, 'TestUtf8')
retval = SOAP::Processor.unmarshal(@response_body).body.response
assert retval.is_a?(SOAP::SOAPBase64)
assert_equal "T25lIFdvcmxkIENhZsOp", retval.data.to_s
end
protected
def exception_message(soap_fault_exception)
soap_fault_exception.detail.cause.message
end
def is_exception?(obj)
obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \
obj.detail.cause.is_a?(Exception)
end
def service_name(container)
container.is_a?(DelegatedController) ? 'test_service' : 'api'
end
def ensure_valid_wsdl_generation(controller, expected_namespace)
wsdl = controller.generate_wsdl
ensure_valid_wsdl(controller, wsdl, expected_namespace)
end
def ensure_valid_wsdl(controller, wsdl, expected_namespace)
definitions = WSDL::Parser.new.parse(wsdl)
assert(definitions.is_a?(WSDL::Definitions))
definitions.bindings.each do |binding|
assert(binding.name.name.index(':').nil?)
end
definitions.services.each do |service|
service.ports.each do |port|
assert(port.name.name.index(':').nil?)
end
end
types = definitions.collect_complextypes.map{|x| x.name}
types.each do |type|
assert(type.namespace == expected_namespace)
end
location = definitions.services[0].ports[0].soap_address.location
if controller.is_a?(DelegatedController)
assert_match %r{http://test.host/dispatcher_test/delegated/test_service$}, location
elsif controller.is_a?(DirectController)
assert_match %r{http://test.host/dispatcher_test/direct/api$}, location
end
definitions.collect_complextypes
end
def ensure_valid_wsdl_action(controller)
test_request = ActionController::TestRequest.new
test_request.action = 'wsdl'
test_response = ActionController::TestResponse.new
wsdl = controller.process(test_request, test_response).body
ensure_valid_wsdl(controller, wsdl, DispatcherTest::WsdlNamespace)
end
end

View file

@ -0,0 +1,59 @@
require File.dirname(__FILE__) + '/abstract_dispatcher'
class TC_DispatcherActionControllerXmlRpc < Test::Unit::TestCase
include DispatcherTest
include DispatcherCommonTests
def setup
@direct_controller = DirectController.new
@delegated_controller = DelegatedController.new
@layered_controller = LayeredController.new
@virtual_controller = VirtualController.new
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@direct_controller)
end
def test_layered_dispatching
mt_cats = do_method_call(@layered_controller, 'mt.getCategories')
assert_equal(["mtCat1", "mtCat2"], mt_cats)
blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories')
assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats)
end
def test_multicall
response = do_method_call(@layered_controller, 'system.multicall', [
{'methodName' => 'mt.getCategories'},
{'methodName' => 'blogger.getCategories'},
{'methodName' => 'mt.bool'},
{'methodName' => 'blogger.str', 'params' => ['2000']},
{'methodName' => 'mt.alwaysFail'},
{'methodName' => 'blogger.alwaysFail'},
{'methodName' => 'mt.blah'},
{'methodName' => 'blah.blah'},
{'methodName' => 'mt.person'}
])
assert_equal [
[["mtCat1", "mtCat2"]],
[["bloggerCat1", "bloggerCat2"]],
[true],
["2500"],
{"faultCode" => 3, "faultString" => "MT AlwaysFail"},
{"faultCode" => 3, "faultString" => "Blogger AlwaysFail"},
{"faultCode" => 4, "faultMessage" => "no such method 'blah' on API DispatcherTest::MTAPI"},
{"faultCode" => 4, "faultMessage" => "no such web service 'blah'"},
[{"name"=>"person1", "id"=>1}]
], response
end
protected
def exception_message(xmlrpc_fault_exception)
xmlrpc_fault_exception.faultString
end
def is_exception?(obj)
obj.is_a?(XMLRPC::FaultException)
end
def service_name(container)
container.is_a?(DelegatedController) ? 'test_service' : 'api'
end
end

View file

@ -0,0 +1,8 @@
CREATE TABLE `users` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(30) default NULL,
`active` tinyint(4) default NULL,
`balance` decimal(5, 2) default NULL,
`created_on` date default NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

View file

@ -0,0 +1,12 @@
user1:
id: 1
name: Kent
active: 1
balance: 12.2
created_on: <%= Date.today %>
user2:
id: 2
name: David
active: 1
balance: 16.4
created_on: <%= Date.today %>

View file

@ -0,0 +1,3 @@
#!/bin/sh
rcov -x '.*_test\.rb,rubygems,abstract_,/run,/apis' ./run

View file

@ -0,0 +1,185 @@
require File.dirname(__FILE__) + '/abstract_unit'
module InvocationTest
class API < ActionWebService::API::Base
api_method :add, :expects => [:int, :int], :returns => [:int]
api_method :transmogrify, :expects_and_returns => [:string]
api_method :fail_with_reason
api_method :fail_generic
api_method :no_before
api_method :no_after
api_method :only_one
api_method :only_two
end
class Interceptor
attr :args
def initialize
@args = nil
end
def intercept(*args)
@args = args
end
end
InterceptorClass = Interceptor.new
class Service < ActionController::Base
web_service_api API
before_invocation :intercept_before, :except => [:no_before]
after_invocation :intercept_after, :except => [:no_after]
prepend_after_invocation :intercept_after_first, :except => [:no_after]
prepend_before_invocation :intercept_only, :only => [:only_one, :only_two]
after_invocation(:only => [:only_one]) do |*args|
args[0].instance_variable_set('@block_invoked', args[1])
end
after_invocation InterceptorClass, :only => [:only_one]
attr_accessor :before_invoked
attr_accessor :after_invoked
attr_accessor :after_first_invoked
attr_accessor :only_invoked
attr_accessor :block_invoked
attr_accessor :invocation_result
def initialize
@before_invoked = nil
@after_invoked = nil
@after_first_invoked = nil
@only_invoked = nil
@invocation_result = nil
@block_invoked = nil
end
def add(a, b)
a + b
end
def transmogrify(str)
str.upcase
end
def fail_with_reason
end
def fail_generic
end
def no_before
5
end
def no_after
end
def only_one
end
def only_two
end
protected
def intercept_before(name, args)
@before_invoked = name
return [false, "permission denied"] if name == :fail_with_reason
return false if name == :fail_generic
end
def intercept_after(name, args, result)
@after_invoked = name
@invocation_result = result
end
def intercept_after_first(name, args, result)
@after_first_invoked = name
end
def intercept_only(name, args)
raise "Interception error" unless name == :only_one || name == :only_two
@only_invoked = name
end
end
end
class TC_Invocation < Test::Unit::TestCase
include ActionWebService::Invocation
def setup
@service = InvocationTest::Service.new
end
def test_invocation
assert(perform_invocation(:add, 5, 10) == 15)
assert(perform_invocation(:transmogrify, "hello") == "HELLO")
assert_raises(NoMethodError) do
perform_invocation(:nonexistent_method_xyzzy)
end
end
def test_interceptor_registration
assert(InvocationTest::Service.before_invocation_interceptors.length == 2)
assert(InvocationTest::Service.after_invocation_interceptors.length == 4)
assert_equal(:intercept_only, InvocationTest::Service.before_invocation_interceptors[0])
assert_equal(:intercept_after_first, InvocationTest::Service.after_invocation_interceptors[0])
end
def test_interception
assert(@service.before_invoked.nil?)
assert(@service.after_invoked.nil?)
assert(@service.only_invoked.nil?)
assert(@service.block_invoked.nil?)
assert(@service.invocation_result.nil?)
perform_invocation(:add, 20, 50)
assert(@service.before_invoked == :add)
assert(@service.after_invoked == :add)
assert(@service.invocation_result == 70)
end
def test_interception_canceling
reason = nil
perform_invocation(:fail_with_reason){|r| reason = r}
assert(@service.before_invoked == :fail_with_reason)
assert(@service.after_invoked.nil?)
assert(@service.invocation_result.nil?)
assert(reason == "permission denied")
reason = true
@service.before_invoked = @service.after_invoked = @service.invocation_result = nil
perform_invocation(:fail_generic){|r| reason = r}
assert(@service.before_invoked == :fail_generic)
assert(@service.after_invoked.nil?)
assert(@service.invocation_result.nil?)
assert(reason == true)
end
def test_interception_except_conditions
perform_invocation(:no_before)
assert(@service.before_invoked.nil?)
assert(@service.after_first_invoked == :no_before)
assert(@service.after_invoked == :no_before)
assert(@service.invocation_result == 5)
@service.before_invoked = @service.after_invoked = @service.invocation_result = nil
perform_invocation(:no_after)
assert(@service.before_invoked == :no_after)
assert(@service.after_invoked.nil?)
assert(@service.invocation_result.nil?)
end
def test_interception_only_conditions
assert(@service.only_invoked.nil?)
perform_invocation(:only_one)
assert(@service.only_invoked == :only_one)
assert(@service.block_invoked == :only_one)
assert(InvocationTest::InterceptorClass.args[1] == :only_one)
@service.only_invoked = nil
perform_invocation(:only_two)
assert(@service.only_invoked == :only_two)
end
private
def perform_invocation(method_name, *args, &block)
@service.perform_invocation(method_name, args, &block)
end
end

View file

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

View file

@ -0,0 +1,146 @@
require File.dirname(__FILE__) + '/abstract_unit'
ActionController::Routing::Routes.draw do |map|
map.connect '', :controller => 'scaffolded'
map.connect ':controller/:action/:id'
end
ActionController::Base.view_paths = [ '.' ]
class ScaffoldPerson < ActionWebService::Struct
member :id, :int
member :name, :string
member :birth, :date
def ==(other)
self.id == other.id && self.name == other.name
end
end
class ScaffoldedControllerTestAPI < ActionWebService::API::Base
api_method :hello, :expects => [{:integer=>:int}, :string], :returns => [:bool]
api_method :hello_struct_param, :expects => [{:person => ScaffoldPerson}], :returns => [:bool]
api_method :date_of_birth, :expects => [ScaffoldPerson], :returns => [:string]
api_method :bye, :returns => [[ScaffoldPerson]]
api_method :date_diff, :expects => [{:start_date => :date}, {:end_date => :date}], :returns => [:int]
api_method :time_diff, :expects => [{:start_time => :time}, {:end_time => :time}], :returns => [:int]
api_method :base64_upcase, :expects => [:base64], :returns => [:base64]
end
class ScaffoldedController < ActionController::Base
web_service_api ScaffoldedControllerTestAPI
web_service_scaffold :scaffold_invoke
def hello(int, string)
0
end
def hello_struct_param(person)
0
end
def date_of_birth(person)
person.birth.to_s
end
def bye
[ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")]
end
def rescue_action(e)
raise e
end
def date_diff(start_date, end_date)
end_date - start_date
end
def time_diff(start_time, end_time)
end_time - start_time
end
def base64_upcase(data)
data.upcase
end
end
class ScaffoldedControllerTest < ActionController::TestCase
# def setup
# @controller = ScaffoldedController.new
# @request = ActionController::TestRequest.new
# @response = ActionController::TestResponse.new
# end
def test_scaffold_invoke
get :scaffold_invoke
assert_template 'methods.html.erb'
end
def test_scaffold_invoke_method_params
get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Hello'
assert_template 'parameters.html.erb'
end
def test_scaffold_invoke_method_params_with_struct
get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'HelloStructParam'
assert_template 'parameters.html.erb'
assert_tag :tag => 'form'
assert_tag :tag => 'input', :attributes => {:name => "method_params[0][name]"}
end
def test_scaffold_invoke_submit_hello
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Hello', :method_params => {'0' => '5', '1' => 'hello world'}
assert_template 'result.html.erb'
assert_equal false, @controller.instance_eval{ @method_return_value }
end
def test_scaffold_invoke_submit_bye
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Bye'
assert_template 'result.html.erb'
persons = [ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")]
assert_equal persons, @controller.instance_eval{ @method_return_value }
end
def test_scaffold_date_params
get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'DateDiff'
(0..1).each do |param|
(1..3).each do |date_part|
assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"},
:children => {:greater_than => 1, :only => {:tag => 'option'}}
end
end
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateDiff',
:method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1'}, '1' => {'1' => '2006', '2' => '2', '3' => '2'}}
assert_equal 1, @controller.instance_eval{ @method_return_value }
end
def test_scaffold_struct_date_params
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateOfBirth',
:method_params => {'0' => {'birth' => {'1' => '2006', '2' => '2', '3' => '1'}, 'id' => '1', 'name' => 'person'}}
assert_equal '2006-02-01', @controller.instance_eval{ @method_return_value }
end
def test_scaffold_time_params
get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'TimeDiff'
(0..1).each do |param|
(1..6).each do |date_part|
assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"},
:children => {:greater_than => 1, :only => {:tag => 'option'}}
end
end
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'TimeDiff',
:method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1', '4' => '1', '5' => '1', '6' => '1'},
'1' => {'1' => '2006', '2' => '2', '3' => '2', '4' => '1', '5' => '1', '6' => '1'}}
assert_equal 86400, @controller.instance_eval{ @method_return_value }
end
def test_scaffold_base64
get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Base64Upcase'
assert_tag :tag => 'textarea', :attributes => {:name => 'method_params[0]'}
post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Base64Upcase', :method_params => {'0' => 'scaffold'}
assert_equal 'SCAFFOLD', @controller.instance_eval{ @method_return_value }
end
end

View file

@ -0,0 +1,52 @@
require File.dirname(__FILE__) + '/abstract_unit'
module StructTest
class Struct < ActionWebService::Struct
member :id, Integer
member :name, String
member :items, [String]
member :deleted, :bool
member :emails, [:string]
end
end
class TC_Struct < Test::Unit::TestCase
include StructTest
def setup
@struct = Struct.new(:id => 5,
:name => 'hello',
:items => ['one', 'two'],
:deleted => true,
:emails => ['test@test.com'])
end
def test_members
assert_equal(5, Struct.members.size)
assert_equal(Integer, Struct.members[:id].type_class)
assert_equal(String, Struct.members[:name].type_class)
assert_equal(String, Struct.members[:items].element_type.type_class)
assert_equal(TrueClass, Struct.members[:deleted].type_class)
assert_equal(String, Struct.members[:emails].element_type.type_class)
end
def test_initializer_and_lookup
assert_equal(5, @struct.id)
assert_equal('hello', @struct.name)
assert_equal(['one', 'two'], @struct.items)
assert_equal(true, @struct.deleted)
assert_equal(['test@test.com'], @struct.emails)
assert_equal(5, @struct['id'])
assert_equal('hello', @struct['name'])
assert_equal(['one', 'two'], @struct['items'])
assert_equal(true, @struct['deleted'])
assert_equal(['test@test.com'], @struct['emails'])
end
def test_each_pair
@struct.each_pair do |name, value|
assert_equal @struct.__send__(name), value
assert_equal @struct[name], value
end
end
end

View file

@ -0,0 +1,112 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'action_web_service/test_invoke'
class TestInvokeAPI < ActionWebService::API::Base
api_method :null
api_method :add, :expects => [:int, :int], :returns => [:int]
end
class TestInvokeService < ActionWebService::Base
web_service_api TestInvokeAPI
attr :invoked
def add(a, b)
@invoked = true
a + b
end
def null
end
end
class TestController < ActionController::Base
def rescue_action(e); raise e; end
end
class TestInvokeDirectController < TestController
web_service_api TestInvokeAPI
attr :invoked
def add
@invoked = true
@method_params[0] + @method_params[1]
end
def null
end
end
class TestInvokeDelegatedController < TestController
web_service_dispatching_mode :delegated
web_service :service, TestInvokeService.new
end
class TestInvokeLayeredController < TestController
web_service_dispatching_mode :layered
web_service(:one) { @service_one ||= TestInvokeService.new }
web_service(:two) { @service_two ||= TestInvokeService.new }
end
class TestInvokeTest < ActiveSupport::TestCase
def setup
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_direct_add
@controller = TestInvokeDirectController.new
assert_equal nil, @controller.invoked
result = invoke :add, 25, 25
assert_equal 50, result
assert_equal true, @controller.invoked
end
def test_delegated_add
@controller = TestInvokeDelegatedController.new
assert_equal nil, @controller.web_service_object(:service).invoked
result = invoke_delegated :service, :add, 100, 50
assert_equal 150, result
assert_equal true, @controller.web_service_object(:service).invoked
end
def test_layered_add
[:soap, :xmlrpc].each do |protocol|
@protocol = protocol
[:one, :two].each do |service|
@controller = TestInvokeLayeredController.new
assert_equal nil, @controller.web_service_object(service).invoked
result = invoke_layered service, :add, 200, -50
assert_equal 150, result
assert_equal true, @controller.web_service_object(service).invoked
end
end
end
def test_layered_fail_with_wrong_number_of_arguments
[:soap, :xmlrpc].each do |protocol|
@protocol = protocol
[:one, :two].each do |service|
@controller = TestInvokeLayeredController.new
assert_raise(ArgumentError) { invoke_layered service, :add, 1 }
end
end
end
def test_delegated_fail_with_wrong_number_of_arguments
@controller = TestInvokeDelegatedController.new
assert_raise(ArgumentError) { invoke_delegated :service, :add, 1 }
end
def test_direct_fail_with_wrong_number_of_arguments
@controller = TestInvokeDirectController.new
assert_raise(ArgumentError) { invoke :add, 1 }
end
def test_with_no_parameters_declared
@controller = TestInvokeDirectController.new
assert_nil invoke(:null)
end
end