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

Added Action Service to the repository

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@658 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson 2005-02-18 10:35:25 +00:00
parent e39bf10594
commit e7a2938029
52 changed files with 6326 additions and 0 deletions

41
actionservice/ChangeLog Normal file
View file

@ -0,0 +1,41 @@
UNRELEASED
* 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 ActionService 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

44
actionservice/HACKING Normal file
View file

@ -0,0 +1,44 @@
== Coding Style
Please try to follow Rails conventions and idioms.
== Concepts
* Service
A service has an associated API definition, and
implements the methods defined in the API definition
* Container
A container contains zero or more services
* API
An API definition defines a list of methods implemented by
a service
* Router
A router takes raw wire requests, decodes them, performs the invocation on
the service, and generates raw wire responses from the invocation result.
A router is mixed into a container class.
* Protocol
A protocol implementation implements the unmarshaling and marshaling of
raw wire requests and responses. Registers with router.
== Action Pack Integration
For Action Pack, the ActionController is both container and router, and also contains
the protocol implementations.
== Adding support for a new protocol
1. Add an ActionService::Protocol::YourProtocol module and any classes you need to
perform unmarshaling/marshaling of protocol requests. See the SOAP implementation
for an example of a complex mapping, and also see
ActionService::Protocol::AbstractProtocol for the methods you need to implement.
2. Add unit tests for your new protocol. Be sure to test using a Action Pack test request
duplicating how the real requests will arrive and verify that mapping to and from Ruby
types works correctly.

21
actionservice/MIT-LICENSE Normal file
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.

241
actionservice/README Normal file
View file

@ -0,0 +1,241 @@
= Action Service -- Serving APIs on rails
Action 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 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
ActionService::API::Base derivative, and then specify this API
definition class wherever you want to use that API.
The implementation of the methods is done seperately to the API
specification.
==== Method name inflection
Action 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
ActionService::API::Base +inflect_names+ option.
==== Inflection examples
:add => Add
:find_all => FindAll
==== Disabling inflection
class PersonAPI < ActionService::API::Base
inflect_names false
end
==== API definition example
class PersonAPI < ActionService::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
service_api PersonAPI
def add
end
def remove
end
end
== Publishing your APIs
Action 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, controller actions implement the API
methods, and parameters for incoming method calls will be placed in
<tt>@params</tt> (keyed by name), and <tt>@method_params</tt> (ordered list).
The return value of the action 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 action
and send back the wrapped return value. <em>This action must not be
overridden.</em>
==== Direct dispatching example
class PersonController < ApplicationController
service_api PersonAPI
def add
end
def remove
end
end
class PersonAPI < ActionService::API::Base
...
end
For this example, protocol requests for +Add+ and +Remove+ methods sent to
<tt>/person/api</tt> will be routed to the actions +add+ and +remove+.
=== Delegated dispatching
This mode can be turned on by setting the +service_dispatching_mode+ option
in a controller.
In this mode, the controller contains one or more service API objects (objects
that implement an ActionService::API::Base definition). These API
objects are each mapped onto one controller action only.
==== Delegated dispatching example
class ApiController < ApplicationController
service_dispatching_mode :delegated
service :person, PersonService.new
end
class PersonService < ActionService::Base
service_api PersonAPI
def add
end
def remove
end
end
class PersonAPI < ActionService::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 +service+
method is called. <em>This action must not be overridden.</em>
Other controller actions (actions that aren't the target of a +service+ call)
are ignored for ActionService purposes, and can do normal action tasks.
== Using the client support
Action 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 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 Service can only
be guaranteed to work with a subset.
If you have the need for clients for a complex service not running on Action
Service, it is recommended that you use +wsdl2ruby+ and generate the client
stub classes.
==== Factory created client example
class BlogManagerController < ApplicationController
client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger'
end
class SearchingController < ApplicationController
client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch'
end
See ActionService::API::ActionController::ClassMethods for more details.
==== Manually created client example
class PersonAPI < ActionService::API::Base
api_method :find_all, :returns => [[Person]]
end
soap_client = ActionService::Client::Soap.new(PersonAPI, "http://...")
persons = soap_client.find_all
class BloggerAPI < ActionService::API::Base
inflect_names false
api_method :getRecentPosts, :returns => [[Blog::Post]]
end
blog = ActionService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger")
posts = blog.getRecentPosts
See ActionService::Client::Soap and ActionService::Client::XmlRpc for more details.
== Dependencies
Action 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 Service version can be downloaded from
http://rubyforge.org/projects/actionservice
== Installation
You can install Action Service with the following command.
% [sudo] ruby setup.rb
== License
Action 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

135
actionservice/Rakefile Normal file
View file

@ -0,0 +1,135 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
require 'fileutils'
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
PKG_NAME = 'actionservice'
PKG_VERSION = '0.4.0' + PKG_BUILD
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/*_test.rb'
t.verbose = true
}
# Generate the RDoc documentation
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Action Service -- Web services for Action Pack"
rdoc.options << '--line-numbers --inline-source --main README --accessor class_inheritable_option=RW'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/action_service.rb')
rdoc.rdoc_files.include('lib/action_service/*.rb')
rdoc.rdoc_files.include('lib/action_service/api/*.rb')
rdoc.rdoc_files.include('lib/action_service/client/*.rb')
rdoc.rdoc_files.include('lib/action_service/protocol/*.rb')
rdoc.rdoc_files.include('lib/action_service/router/*.rb')
rdoc.rdoc_files.include('lib/action_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"
s.email = "bitserf@gmail.com"
s.rubyforge_project = "actionservice"
s.homepage = "http://rubyforge.org/projects/actionservice"
s.add_dependency('actionpack', '>= 1.4.0')
s.add_dependency('activerecord', '>= 1.6.0')
s.add_dependency('activesupport', '>= 0.9.0')
s.has_rdoc = true
s.requirements << 'none'
s.require_path = 'lib'
s.autorequire = 'action_service'
s.files = [ "Rakefile", "setup.rb", "README", "TODO", "HACKING", "ChangeLog", "MIT-LICENSE" ]
s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
p.need_tar = true
p.need_zip = true
end
desc "Publish API docs to RubyForge"
task :pdoc => [:rdoc] do
FileUtils.mkdir_p 'html'
FileUtils.mv 'doc', 'html/api'
Rake::RubyForgePublisher.new('actionservice', 'ljb').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_service\.rb$ lib\/action_service\/.*\.rb$]
excludes ||= %w[]
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 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

35
actionservice/TODO Normal file
View file

@ -0,0 +1,35 @@
= Post-0.4.0 Tasks
- relax type-checking for XML-RPC, and perform casts between base types if there
are mismatches (i.e. String received when Integer expected, or vice-versa)
- support XML-RPC's "handler." method namespacing. perhaps something like:
class BloggingServices < ActionService::LayeredService
def initialize(request)
@request = controller.request
end
service :mt {MTService.new(@request)}
service :blogger {BloggerService.new(@request)}
service :metaWeblog {MetaWeblogService.new(@request)}
end
class ApiController < ApplicationController
service_dispatching_mode :delegated
service :xmlrpc { BloggingServices.new(@request) }
end
= Low priority tasks
- add better type mapping tests for XML-RPC
- add tests for ActiveRecord support (with mock objects?)
= Refactoring
- Find an alternative way to map interesting types for SOAP (like ActiveRecord
model classes) that doesn't require creation of a sanitized copy object with data
copied from the real one. Ideally this would let us get rid of
ActionService::Struct altogether and provide a block that would yield the
attributes and values. "Filters" ? Not sure how to integrate with SOAP though.
- 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,151 @@
= 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
1. Ensure you have the 'actionservice' Gem installed. You can generate it using
this command:
$ rake package
2. Edit config/environment.rb, and add the following line after the rest of the
require_gem statements:
require_gem 'actionservice'
3. "Direct" example:
* Copy direct/search_controller.rb to "app/controllers"
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.
4. 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 :)
5. 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
service_exception_reporting false
end
6. 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 < ActionService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionService::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 < ActionService::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 < ActionService::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,110 @@
require 'action_service'
class DirectoryCategory < ActionService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionService::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 < ActionService::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 < ActionService::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 < ActionService::Base
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'
service_dispatching_mode :delegated
service :beta3, GoogleSearchService.new
end

View file

@ -0,0 +1,109 @@
class DirectoryCategory < ActionService::Struct
member :fullViewableName, :string
member :specialEncoding, :string
end
class ResultElement < ActionService::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 < ActionService::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 < ActionService::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 SearchController < ApplicationController
service_api GoogleSearchAPI
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,28 @@
= 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.
= Running
1. Ensure you have the 'actionservice' Gem installed. You can generate it using
this command:
$ rake package
2. Edit config/environment.rb, and add the following line after the rest of the
require_gem statements:
require_gem 'actionservice'
3. Copy blog_controller.rb to "app/controllers" in a Rails project.
4. Fire up a desktop blogging application (such as BloGTK on Linux),
point it at http://localhost:3000/blog/api, and try creating or
editing blog posts.

View file

@ -0,0 +1,121 @@
# structures as defined by the metaWeblog/blogger
# specifications.
module Blog
class Enclosure < ActionService::Struct
member :url, :string
member :length, :int
member :type, :string
end
class Source < ActionService::Struct
member :url, :string
member :name, :string
end
class Post < ActionService::Struct
member :title, :string
member :link, :string
member :description, :string
member :author, :string
member :category, :string
member :comments, :string
member :enclosure, Enclosure
member :guid, :string
member :pubDate, :string
member :source, Source
end
class Blog < ActionService::Struct
member :url, :string
member :blogid, :string
member :blogName, :string
end
end
# skeleton metaWeblog API
class MetaWeblogAPI < ActionService::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 :getUsersBlogs, :returns => [[Blog::Blog]], :expects => [
{:appkey=>:string},
{:username=>:string},
{:password=>:string},
]
api_method :getRecentPosts, :returns => [[Blog::Post]], :expects => [
{:blogid=>:string},
{:username=>:string},
{:password=>:string},
{:numberOfPosts=>:int},
]
end
class BlogController < ApplicationController
service_api MetaWeblogAPI
def initialize
@postid = 0
end
def newPost
$stderr.puts 'Creating post: username=%s password=%s struct=%s' % [
@params['username'],
@params['password'],
@params['struct'].inspect
]
(@postid += 1).to_s
end
def editPost
$stderr.puts 'Editing post: username=%s password=%s struct=%s' % [
@params['username'],
@params['password'],
@params['struct'].inspect
]
true
end
def getUsersBlogs
$stderr.puts "Returning user %s's blogs" % @params['username']
blog = Blog::Blog.new
blog.url = 'http://blog.xeraph.org'
blog.blogid = 'sttm'
blog.blogName = 'slave to the machine'
[blog]
end
def getRecentPosts
$stderr.puts "Returning recent posts (%d requested)" % @params['numberOfPosts']
post1 = Blog::Post.new
post1.title = 'first post!'
post1.link = 'http://blog.xeraph.org/testOne.html'
post1.description = 'this is the first post'
post2 = Blog::Post.new
post2.title = 'second post!'
post2.link = 'http://blog.xeraph.org/testTwo.html'
post2.description = 'this is the second post'
[post1, post2]
end
end

View file

@ -0,0 +1,60 @@
#--
# 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'
require_gem 'activesupport', '>= 0.9.0'
require_gem 'actionpack', '>= 1.4.0'
require_gem 'activerecord', '>= 1.6.0'
end
$:.unshift(File.dirname(__FILE__))
require 'action_service/base'
require 'action_service/client'
require 'action_service/invocation'
require 'action_service/api'
require 'action_service/struct'
require 'action_service/container'
require 'action_service/protocol'
require 'action_service/router'
ActionService::Base.class_eval do
include ActionService::API
include ActionService::Invocation
end
ActionController::Base.class_eval do
include ActionService::Container
include ActionService::Protocol::Registry
include ActionService::Protocol::Soap
include ActionService::Protocol::XmlRpc
include ActionService::API
include ActionService::API::ActionController
include ActionService::Router::ActionController
include ActionService::Router::Wsdl
end

View file

@ -0,0 +1,2 @@
require 'action_service/api/abstract'
require 'action_service/api/action_controller'

View file

@ -0,0 +1,198 @@
module ActionService # :nodoc:
module API # :nodoc:
class APIError < ActionService::ActionServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
end
module ClassMethods
# Attaches ActionService API +definition+ to the calling class.
#
# If +definition+ is not an ActionService::API::Base derivative class
# object, it may be a symbol or a string, in which case a file named
# <tt>definition_api.rb</tt> will be expected to exist in the load path,
# containing an API definition class named <tt>DefinitionAPI</tt> or
# <tt>DefinitionApi</tt>.
#
# Action Controllers can have a default associated API, removing the need
# to call this method if you follow the Action Service naming conventions.
#
# A controller with a class name of GoogleSearchController will
# implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the
# API definition class to be named <tt>GoogleSearchAPI</tt> or
# <tt>GoogleSearchApi</tt>.
#
# ==== Service class example
#
# class MyService < ActionService::Base
# service_api MyAPI
# end
#
# class MyAPI < ActionService::API::Base
# ...
# end
#
# ==== Controller class example
#
# class MyController < ActionController::Base
# service_api MyAPI
# end
#
# class MyAPI < ActionService::API::Base
# ...
# end
def service_api(definition=nil)
if definition.nil?
read_inheritable_attribute("service_api")
else
if definition.is_a?(Symbol)
raise(APIError, "symbols can only be used for #service_api inside of a controller")
end
unless definition.respond_to?(:ancestors) && definition.ancestors.include?(Base)
raise(APIError, "#{definition.to_s} is not a valid API definition")
end
write_inheritable_attribute("service_api", definition)
call_service_api_callbacks(self, definition)
end
end
def add_service_api_callback(&block) # :nodoc:
write_inheritable_array("service_api_callbacks", [block])
end
private
def call_service_api_callbacks(container_class, definition)
(read_inheritable_attribute("service_api_callbacks") || []).each do |block|
block.call(container_class, definition)
end
end
end
# A 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 service implementation classes like ActionService::Base
# and ActionController::Base derivatives with ClassMethods#service_api.
class Base
# Whether to transform API method names into camel-cased
# names
class_inheritable_option :inflect_names, true
# If present, the name of a method to call when the remote caller
# tried to call a nonexistent method. Semantically equivalent to
# +method_missing+.
class_inheritable_option :default_api_method
# Disallow instantiation
private_class_method :new, :allocate
class << self
include ActionService::Signature
# API methods have a +name+, which must be the Ruby method name to use when
# performing the invocation on the service object.
#
# The type signature hints for the method input parameters and return value
# can by specified in +options+.
#
# A signature hint is an array of one or more parameter type specifiers.
# A type specifier can be one of the following:
#
# * A symbol or string of representing one of the Action Service base types.
# See ActionService::Signature for a canonical list of the base types.
# * The Class object of the parameter type
# * A single-element Array containing one of the two preceding items. This
# will cause Action Service to treat the parameter at that position
# as an array containing only values of the given type.
# * A Hash containing as key the name of the parameter, and as value
# one of the three preceding items
#
# If no method input parameter or method return value hints are given,
# the method is assumed to take no parameters and 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 hint for the method input parameters
# [<tt>:returns</tt>] Signature hint for the method return value
# [<tt>:expects_and_returns</tt>] Signature hint for both input parameters and return value
def api_method(name, options={})
validate_options([:expects, :returns, :expects_and_returns], options.keys)
if options[:expects_and_returns]
expects = options[:expects_and_returns]
returns = options[:expects_and_returns]
else
expects = options[:expects]
returns = options[:returns]
end
expects = canonical_signature(expects) if expects
returns = canonical_signature(returns) if returns
if expects && Object.const_defined?('ActiveRecord')
expects.each do |param|
klass = signature_parameter_class(param)
klass = klass[0] if klass.is_a?(Array)
if klass.ancestors.include?(ActiveRecord::Base)
raise(ActionServiceError, "ActiveRecord model classes not allowed in :expects")
end
end
end
name = name.to_sym
public_name = public_api_method_name(name)
info = { :expects => expects, :returns => returns }
write_inheritable_hash("api_methods", name => info)
write_inheritable_hash("api_public_method_names", public_name => name)
end
# Whether the given method name is a service method on this API
def has_api_method?(name)
api_methods.has_key?(name)
end
# Whether the given public method name has a corresponding service method
# on this API
def has_public_api_method?(public_name)
api_public_method_names.has_key?(public_name)
end
# The corresponding public method name for the given service method name
def public_api_method_name(name)
if inflect_names
name.to_s.camelize
else
name.to_s
end
end
# The corresponding service method name for the given public method name
def api_method_name(public_name)
api_public_method_names[public_name]
end
# A Hash containing all service methods on this API, and their
# associated metadata.
def api_methods
read_inheritable_attribute("api_methods") || {}
end
private
def api_public_method_names
read_inheritable_attribute("api_public_method_names") || {}
end
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
unless unknown_option_keys.empty?
raise(ActionServiceError, "Unknown options: #{unknown_option_keys}")
end
end
end
end
end
end

View file

@ -0,0 +1,97 @@
module ActionService # :nodoc:
module API # :nodoc:
module ActionController # :nodoc:
def self.append_features(base) # :nodoc:
base.class_eval do
class << self
alias_method :inherited_without_api, :inherited
alias_method :service_api_without_require, :service_api
end
end
base.extend(ClassMethods)
end
module ClassMethods
# Creates a _protected_ factory method with the given
# +name+. This method will create a +protocol+ client connected
# to the given endpoint URL.
#
# ==== Example
#
# class MyController < ActionController::Base
# 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.
#
# The same rules as ActionService::API::Base#service_api are
# used to retrieve the API definition with the given +name+.
#
# +options+ is the set of protocol client specific options,
# see the 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, you can
# pass through the API definition class in +options+, using
# a key of <tt>:api</tt>
def client_api(name, protocol, endpoint_uri, options={})
unless method_defined?(name)
api_klass = options.delete(:api) || require_api(name)
class_eval do
define_method(name) do
probe_protocol_client(api_klass, protocol, endpoint_uri, options)
end
protected name
end
end
end
def service_api(definition=nil) # :nodoc:
return service_api_without_require if definition.nil?
case definition
when String, Symbol
klass = require_api(definition)
else
klass = definition
end
service_api_without_require(klass)
end
def require_api(name) # :nodoc:
case name
when String, Symbol
file_name = name.to_s.underscore + "_api"
class_name = file_name.camelize
class_names = [class_name, class_name.sub(/Api$/, 'API')]
begin
require_dependency(file_name)
rescue LoadError => load_error
requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
raise LoadError, requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}"
end
klass = nil
class_names.each do |name|
klass = name.constantize rescue nil
break unless klass.nil?
end
unless klass
raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found")
end
klass
else
raise(ArgumentError, "expected String or Symbol argument")
end
end
private
def inherited(child)
inherited_without_api(child)
child.service_api(child.controller_path)
rescue Exception => e
end
end
end
end
end

View file

@ -0,0 +1,41 @@
require 'action_service/support/class_inheritable_options'
require 'action_service/support/signature'
module ActionService # :nodoc:
class ActionServiceError < StandardError # :nodoc:
end
# An Action Service object implements a specified API.
#
# Used by controllers operating in _Delegated_ dispatching mode.
#
# ==== Example
#
# class PersonService < ActionService::Base
# service_api PersonAPI
#
# def find_person(criteria)
# Person.find_all [...]
# end
#
# def delete_person(id)
# Person.find_by_id(id).destroy
# end
# end
#
# class PersonAPI < ActionService::API::Base
# api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]]
# api_method :delete_person, :expects => [:int]
# end
#
# class SearchCriteria < ActionStruct::Base
# member :firstname, :string
# member :lastname, :string
# member :email, :string
# end
class Base
# Whether to report exceptions back to the caller in the protocol's exception
# format
class_inheritable_option :service_exception_reporting, true
end
end

View file

@ -0,0 +1,3 @@
require 'action_service/client/base'
require 'action_service/client/soap'
require 'action_service/client/xmlrpc'

View file

@ -0,0 +1,35 @@
module ActionService # :nodoc:
module Client # :nodoc:
class ClientError < StandardError # :nodoc:
end
class Base # :nodoc:
def initialize(api, endpoint_uri)
@api = api
@endpoint_uri = endpoint_uri
end
def method_missing(name, *args) # :nodoc:
call_name = method_name(name)
return super(name, *args) if call_name.nil?
perform_invocation(call_name, args)
end
protected
def perform_invocation(method_name, args) # :nodoc:
raise NotImplementedError, "use a protocol-specific client"
end
private
def method_name(name)
if @api.has_api_method?(name.to_sym)
name.to_s
elsif @api.has_public_api_method?(name.to_s)
@api.api_method_name(name.to_s).to_s
else
nil
end
end
end
end
end

View file

@ -0,0 +1,87 @@
require 'soap/rpc/driver'
require 'uri'
module ActionService # :nodoc:
module Client # :nodoc:
# Implements SOAP client support (using RPC encoding for the messages).
#
# ==== Example Usage
#
# class PersonAPI < ActionService::API::Base
# api_method :find_all, :returns => [[Person]]
# end
#
# soap_client = ActionService::Client::Soap.new(PersonAPI, "http://...")
# persons = soap_client.find_all
#
class Soap < Base
# Creates a new web service client using the SOAP RPC protocol.
#
# +api+ must be an ActionService::API::Base derivative, and
# +endpoint_uri+ must point at the relevant URL to which protocol requests
# will be sent with HTTP POST.
#
# Valid options:
# [<tt>:service_name</tt>] If the remote server has used a custom +wsdl_service_name+
# option, you must specify it here
def initialize(api, endpoint_uri, options={})
super(api, endpoint_uri)
@service_name = options[:service_name] || 'ActionService'
@namespace = "urn:#{@service_name}"
@mapper = ActionService::Protocol::Soap::SoapMapper.new(@namespace)
@protocol = ActionService::Protocol::Soap::SoapProtocol.new(@mapper)
@soap_action_base = options[:soap_action_base]
@soap_action_base ||= URI.parse(endpoint_uri).path
@driver = create_soap_rpc_driver(api, endpoint_uri)
end
protected
def perform_invocation(method_name, args)
@driver.send(method_name, *args)
end
def soap_action(method_name)
"#{@soap_action_base}/#{method_name}"
end
private
def create_soap_rpc_driver(api, endpoint_uri)
@mapper.map_api(api)
driver = SoapDriver.new(endpoint_uri, nil)
driver.mapping_registry = @mapper.registry
api.api_methods.each do |name, info|
public_name = api.public_api_method_name(name)
qname = XSD::QName.new(@namespace, public_name)
action = soap_action(public_name)
expects = info[:expects]
returns = info[:returns]
param_def = []
i = 1
if expects
expects.each do |klass|
param_name = klass.is_a?(Hash) ? klass.keys[0] : "param#{i}"
mapping = @mapper.lookup(klass)
param_def << ['in', param_name, mapping.registry_mapping]
i += 1
end
end
if returns
mapping = @mapper.lookup(returns[0])
param_def << ['retval', 'return', mapping.registry_mapping]
end
driver.add_method(qname, action, name.to_s, param_def)
end
driver
end
class SoapDriver < SOAP::RPC::Driver # :nodoc:
def add_method(qname, soapaction, name, param_def)
@proxy.add_rpc_method(qname, soapaction, name, param_def)
add_rpc_method_interface(name, param_def)
end
end
end
end
end

View file

@ -0,0 +1,76 @@
require 'uri'
require 'xmlrpc/client'
module ActionService # :nodoc:
module Client # :nodoc:
# Implements XML-RPC client support
#
# ==== Example Usage
#
# class BloggerAPI < ActionService::API::Base
# inflect_names false
# api_method :getRecentPosts, :returns => [[Blog::Post]]
# end
#
# blog = ActionService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger")
# posts = blog.getRecentPosts
class XmlRpc < Base
# Creates a new web service client using the XML-RPC protocol.
#
# +api+ must be an ActionService::API::Base derivative, and
# +endpoint_uri+ must point at the relevant URL to which protocol requests
# will be sent with HTTP POST.
#
# Valid options:
# [<tt>:handler_name</tt>] If the remote server defines its services inside special
# handler (the Blogger API uses a <tt>"blogger"</tt> handler name for example),
# provide it here, or your method calls will fail
def initialize(api, endpoint_uri, options={})
@api = api
@handler_name = options[:handler_name]
@client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout])
end
protected
def perform_invocation(method_name, args)
args = transform_outgoing_method_params(method_name, args)
ok, return_value = @client.call2(public_name(method_name), *args)
return transform_return_value(method_name, return_value) if ok
raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}")
end
def transform_outgoing_method_params(method_name, params)
info = @api.api_methods[method_name.to_sym]
signature = info[:expects]
signature_length = signature.nil?? 0 : signature.length
if signature_length != params.length
raise(ProtocolError, "API declares #{public_name(method_name)} to accept " +
"#{signature_length} parameters, but #{params.length} parameters " +
"were supplied")
end
if signature_length > 0
signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature)
(1..signature.size).each do |i|
i -= 1
params[i] = Protocol::XmlRpc::XmlRpcProtocol.ruby_to_xmlrpc(params[i], signature[i])
end
end
params
end
def transform_return_value(method_name, return_value)
info = @api.api_methods[method_name.to_sym]
return true unless signature = info[:returns]
signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature)
Protocol::XmlRpc::XmlRpcProtocol.xmlrpc_to_ruby(return_value, signature[0])
end
def public_name(method_name)
public_name = @api.public_api_method_name(method_name)
@handler_name ? "#{@handler_name}.#{public_name}" : public_name
end
end
end
end

View file

@ -0,0 +1,232 @@
module ActionService # :nodoc:
module Container # :nodoc:
class ContainerError < ActionService::ActionServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.class_inheritable_option(:service_dispatching_mode, :direct)
base.class_inheritable_option(:service_exception_reporting, true)
base.extend(ClassMethods)
base.send(:include, ActionService::Container::InstanceMethods)
end
module ClassMethods
# Declares a service that will provides access to the API of the given
# service +object+. +object+ must be an ActionService::Base derivative.
#
# 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 service object example
#
# class ApiController < ApplicationController
# service_dispatching_mode :delegated
#
# 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 service object example
#
# class ApiController < ApplicationController
# service_dispatching_mode :delegated
#
# service(:person) { PersonService.new(@request.env) }
# end
def 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("action_services", info)
call_service_definition_callbacks(self, name, info)
end
# Whether this service contains a service with the given +name+
def has_service?(name)
services.has_key?(name.to_sym)
end
def services # :nodoc:
read_inheritable_attribute("action_services") || {}
end
def add_service_definition_callback(&block) # :nodoc:
write_inheritable_array("service_definition_callbacks", [block])
end
private
def call_service_definition_callbacks(container_class, service_name, service_info)
(read_inheritable_attribute("service_definition_callbacks") || []).each do |block|
block.call(container_class, service_name, service_info)
end
end
end
module InstanceMethods # :nodoc:
def service_object(service_name)
info = self.class.services[service_name.to_sym]
unless info
raise(ContainerError, "no such service '#{service_name}'")
end
service = info[:block]
service ? instance_eval(&service) : info[:object]
end
private
def dispatch_service_request(protocol_request)
case service_dispatching_mode
when :direct
dispatch_direct_service_request(protocol_request)
when :delegated
dispatch_delegated_service_request(protocol_request)
else
raise(ContainerError, "unsupported dispatching mode '#{service_dispatching_mode}'")
end
end
def dispatch_direct_service_request(protocol_request)
public_method_name = protocol_request.public_method_name
api = self.class.service_api
method_name = api.api_method_name(public_method_name)
block = nil
expects = nil
if method_name
signature = api.api_methods[method_name]
expects = signature[:expects]
protocol_request.type = Protocol::CheckedMessage
protocol_request.signature = expects
protocol_request.return_signature = signature[:returns]
else
protocol_request.type = Protocol::UncheckedMessage
system_methods = self.class.read_inheritable_attribute('default_system_methods') || {}
protocol = protocol_request.protocol
block = system_methods[protocol.class]
unless block
method_name = api.default_api_method
unless method_name && respond_to?(method_name)
raise(ContainerError, "no such method ##{public_method_name}")
end
end
end
@method_params = protocol_request.unmarshal
@params ||= {}
if expects
(1..@method_params.size).each do |i|
i -= 1
if expects[i].is_a?(Hash)
@params[expects[i].keys.shift.to_s] = @method_params[i]
else
@params["param#{i}"] = @method_params[i]
end
end
end
if respond_to?(:before_action)
@params['action'] = method_name.to_s
return protocol_request.marshal(nil) if before_action == false
end
perform_invoke = lambda do
if block
block.call(public_method_name, self.class, *@method_params)
else
send(method_name)
end
end
try_default = true
result = nil
catch(:try_default) do
result = perform_invoke.call
try_default = false
end
if try_default
method_name = api.default_api_method
if method_name
protocol_request.type = Protocol::UncheckedMessage
else
raise(ContainerError, "no such method ##{public_method_name}")
end
result = perform_invoke.call
end
after_action if respond_to?(:after_action)
protocol_request.marshal(result)
end
def dispatch_delegated_service_request(protocol_request)
service_name = protocol_request.service_name
service = service_object(service_name)
api = service.class.service_api
public_method_name = protocol_request.public_method_name
method_name = api.api_method_name(public_method_name)
invocation = ActionService::Invocation::InvocationRequest.new(
ActionService::Invocation::ConcreteInvocation,
public_method_name,
method_name)
if method_name
protocol_request.type = Protocol::CheckedMessage
signature = api.api_methods[method_name]
protocol_request.signature = signature[:expects]
protocol_request.return_signature = signature[:returns]
invocation.params = protocol_request.unmarshal
else
protocol_request.type = Protocol::UncheckedMessage
invocation.type = ActionService::Invocation::VirtualInvocation
system_methods = self.class.read_inheritable_attribute('default_system_methods') || {}
protocol = protocol_request.protocol
block = system_methods[protocol.class]
if block
invocation.block = block
invocation.block_params << service.class
else
method_name = api.default_api_method
if method_name && service.respond_to?(method_name)
invocation.params = protocol_request.unmarshal
invocation.method_name = method_name.to_sym
else
raise(ContainerError, "no such method /#{service_name}##{public_method_name}")
end
end
end
canceled_reason = nil
canceled_block = lambda{|r| canceled_reason = r}
perform_invoke = lambda do
service.perform_invocation(invocation, &canceled_block)
end
try_default = true
result = nil
catch(:try_default) do
result = perform_invoke.call
try_default = false
end
if try_default
method_name = api.default_api_method
if method_name
protocol_request.type = Protocol::UncheckedMessage
invocation.params = protocol_request.unmarshal
invocation.method_name = method_name.to_sym
invocation.type = ActionService::Invocation::UnpublishedConcreteInvocation
else
raise(ContainerError, "no such method /#{service_name}##{public_method_name}")
end
result = perform_invoke.call
end
protocol_request.marshal(result)
end
end
end
end

View file

@ -0,0 +1,252 @@
module ActionService # :nodoc:
module Invocation # :nodoc:
ConcreteInvocation = :concrete
VirtualInvocation = :virtual
UnpublishedConcreteInvocation = :unpublished_concrete
class InvocationError < ActionService::ActionServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
base.send(:include, ActionService::Invocation::InstanceMethods)
end
# Invocation interceptors provide a means to execute custom code before
# and after method invocations on ActionService::Base objects.
#
# When running in _Direct_ dispatching mode, ActionController filters
# should be used for this functionality.
#
# The semantics of invocation interceptors are the same as ActionController
# filters, and accept the same parameters and options.
#
# A _before_ interceptor can also cancel execution by returning +false+,
# or returning a <tt>[false, "cancel reason"]</tt> array if it wishes to supply
# a reason for canceling the request.
#
# === Example
#
# class CustomService < ActionService::Base
# before_invocation :intercept_add, :only => [:add]
#
# def add(a, b)
# a + b
# end
#
# private
# def intercept_add
# return [false, "permission denied"] # cancel it
# end
# end
#
# Options:
# [<tt>:except</tt>] A list of methods for which the interceptor will NOT be called
# [<tt>:only</tt>] A list of methods for which the interceptor WILL be called
module ClassMethods
# Appends the given +interceptors+ to be called
# _before_ method invocation.
def append_before_invocation(*interceptors, &block)
conditions = extract_conditions!(interceptors)
interceptors << block if block_given?
add_interception_conditions(interceptors, conditions)
append_interceptors_to_chain("before", interceptors)
end
# Prepends the given +interceptors+ to be called
# _before_ method invocation.
def prepend_before_invocation(*interceptors, &block)
conditions = extract_conditions!(interceptors)
interceptors << block if block_given?
add_interception_conditions(interceptors, conditions)
prepend_interceptors_to_chain("before", interceptors)
end
alias :before_invocation :append_before_invocation
# Appends the given +interceptors+ to be called
# _after_ method invocation.
def append_after_invocation(*interceptors, &block)
conditions = extract_conditions!(interceptors)
interceptors << block if block_given?
add_interception_conditions(interceptors, conditions)
append_interceptors_to_chain("after", interceptors)
end
# Prepends the given +interceptors+ to be called
# _after_ method invocation.
def prepend_after_invocation(*interceptors, &block)
conditions = extract_conditions!(interceptors)
interceptors << block if block_given?
add_interception_conditions(interceptors, conditions)
prepend_interceptors_to_chain("after", interceptors)
end
alias :after_invocation :append_after_invocation
def before_invocation_interceptors # :nodoc:
read_inheritable_attribute("before_invocation_interceptors")
end
def after_invocation_interceptors # :nodoc:
read_inheritable_attribute("after_invocation_interceptors")
end
def included_intercepted_methods # :nodoc:
read_inheritable_attribute("included_intercepted_methods") || {}
end
def excluded_intercepted_methods # :nodoc:
read_inheritable_attribute("excluded_intercepted_methods") || {}
end
private
def append_interceptors_to_chain(condition, interceptors)
write_inheritable_array("#{condition}_invocation_interceptors", interceptors)
end
def prepend_interceptors_to_chain(condition, interceptors)
interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors")
write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors)
end
def extract_conditions!(interceptors)
return nil unless interceptors.last.is_a? Hash
interceptors.pop
end
def add_interception_conditions(interceptors, conditions)
return unless conditions
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included
write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded
end
def condition_hash(interceptors, *methods)
interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})}
end
end
module InstanceMethods # :nodoc:
def self.append_features(base)
super
base.class_eval do
alias_method :perform_invocation_without_interception, :perform_invocation
alias_method :perform_invocation, :perform_invocation_with_interception
end
end
def perform_invocation_with_interception(invocation, &block)
return if before_invocation(invocation.method_name, invocation.params, &block) == false
result = perform_invocation_without_interception(invocation)
after_invocation(invocation.method_name, invocation.params, result)
result
end
def perform_invocation(invocation)
if invocation.concrete?
unless self.respond_to?(invocation.method_name) && \
self.class.service_api.has_api_method?(invocation.method_name)
raise InvocationError, "no such service method '#{invocation.method_name}'"
end
end
params = invocation.params
if invocation.concrete? || invocation.unpublished_concrete?
self.send(invocation.method_name, *params)
else
if invocation.block
params = invocation.block_params + params
invocation.block.call(invocation.public_method_name, *params)
else
self.send(invocation.method_name, *params)
end
end
end
def before_invocation(name, args, &block)
call_interceptors(self.class.before_invocation_interceptors, [name, args], &block)
end
def after_invocation(name, args, result)
call_interceptors(self.class.after_invocation_interceptors, [name, args, result])
end
private
def call_interceptors(interceptors, interceptor_args, &block)
if interceptors and not interceptors.empty?
interceptors.each do |interceptor|
next if method_exempted?(interceptor, interceptor_args[0].to_s)
result = case
when interceptor.is_a?(Symbol)
self.send(interceptor, *interceptor_args)
when interceptor_block?(interceptor)
interceptor.call(self, *interceptor_args)
when interceptor_class?(interceptor)
interceptor.intercept(self, *interceptor_args)
else
raise(
InvocationError,
"Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method"
)
end
reason = nil
if result.is_a?(Array)
reason = result[1] if result[1]
result = result[0]
end
if result == false
block.call(reason) if block && reason
return false
end
end
end
end
def interceptor_block?(interceptor)
interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1)
end
def interceptor_class?(interceptor)
interceptor.respond_to?("intercept")
end
def method_exempted?(interceptor, method_name)
case
when self.class.included_intercepted_methods[interceptor]
!self.class.included_intercepted_methods[interceptor].include?(method_name)
when self.class.excluded_intercepted_methods[interceptor]
self.class.excluded_intercepted_methods[interceptor].include?(method_name)
end
end
end
class InvocationRequest # :nodoc:
attr_accessor :type
attr :public_method_name
attr_accessor :method_name
attr_accessor :params
attr_accessor :block
attr :block_params
def initialize(type, public_method_name, method_name, params=nil)
@type = type
@public_method_name = public_method_name
@method_name = method_name
@params = params || []
@block = nil
@block_params = []
end
def concrete?
@type == ConcreteInvocation ? true : false
end
def unpublished_concrete?
@type == UnpublishedConcreteInvocation ? true : false
end
end
end
end

View file

@ -0,0 +1,4 @@
require 'action_service/protocol/abstract'
require 'action_service/protocol/registry'
require 'action_service/protocol/soap'
require 'action_service/protocol/xmlrpc'

View file

@ -0,0 +1,128 @@
module ActionService # :nodoc:
module Protocol # :nodoc:
CheckedMessage = :checked
UncheckedMessage = :unchecked
class ProtocolError < ActionService::ActionServiceError # :nodoc:
end
class AbstractProtocol # :nodoc:
attr :container_class
def initialize(container_class)
@container_class = container_class
end
def unmarshal_request(protocol_request)
raise NotImplementedError
end
def marshal_response(protocol_request, return_value)
raise NotImplementedError
end
def marshal_exception(exception)
raise NotImplementedError
end
def self.create_protocol_request(container_class, action_pack_request)
nil
end
def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
nil
end
end
class AbstractProtocolMessage # :nodoc:
attr_accessor :signature
attr_accessor :return_signature
attr_accessor :type
attr :options
def initialize(options={})
@signature = @return_signature = nil
@options = options
@type = @options[:type] || CheckedMessage
end
def signature=(value)
return if value.nil?
@signature = []
value.each do |klass|
if klass.is_a?(Hash)
@signature << klass.values.shift
else
@signature << klass
end
end
@signature
end
def checked?
@type == CheckedMessage
end
def check_parameter_types(values, signature)
return unless checked? && signature
unless signature.length == values.length
raise(ProtocolError, "Signature and parameter lengths mismatch")
end
(1..signature.length).each do |i|
check_compatibility(signature[i-1], values[i-1].class)
end
end
def check_compatibility(expected_class, received_class)
return if \
(expected_class == TrueClass or expected_class == FalseClass) and \
(received_class == TrueClass or received_class == FalseClass)
unless received_class.ancestors.include?(expected_class) or \
expected_class.ancestors.include?(received_class)
raise(ProtocolError, "value of type #{received_class.name} is not " +
"compatible with expected type #{expected_class.name}")
end
end
end
class ProtocolRequest < AbstractProtocolMessage # :nodoc:
attr :protocol
attr :raw_body
attr_accessor :service_name
attr_accessor :public_method_name
attr_accessor :content_type
def initialize(protocol, raw_body, service_name, public_method_name, content_type, options={})
super(options)
@protocol = protocol
@raw_body = raw_body
@service_name = service_name
@public_method_name = public_method_name
@content_type = content_type
end
def unmarshal
@protocol.unmarshal_request(self)
end
def marshal(return_value)
@protocol.marshal_response(self, return_value)
end
end
class ProtocolResponse < AbstractProtocolMessage # :nodoc:
attr :protocol
attr :raw_body
attr_accessor :content_type
def initialize(protocol, raw_body, content_type, options={})
super(options)
@protocol = protocol
@raw_body = raw_body
@content_type = content_type
end
end
end
end

View file

@ -0,0 +1,55 @@
module ActionService # :nodoc:
module Protocol # :nodoc:
HeaderAndBody = :header_and_body
BodyOnly = :body_only
module Registry # :nodoc:
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
base.send(:include, ActionService::Protocol::Registry::InstanceMethods)
end
module ClassMethods # :nodoc:
def register_protocol(type, klass) # :nodoc:
case type
when HeaderAndBody
write_inheritable_array("header_and_body_protocols", [klass])
when BodyOnly
write_inheritable_array("body_only_protocols", [klass])
else
raise(ProtocolError, "unknown protocol type #{type}")
end
end
end
module InstanceMethods # :nodoc:
private
def probe_request_protocol(action_pack_request)
(header_and_body_protocols + body_only_protocols).each do |protocol|
protocol_request = protocol.create_protocol_request(self.class, action_pack_request)
return protocol_request if protocol_request
end
raise(ProtocolError, "unsupported request message format")
end
def probe_protocol_client(api, protocol_name, endpoint_uri, options)
(header_and_body_protocols + body_only_protocols).each do |protocol|
protocol_client = protocol.create_protocol_client(api, protocol_name, endpoint_uri, options)
return protocol_client if protocol_client
end
raise(ProtocolError, "unsupported client protocol :#{protocol_name}")
end
def header_and_body_protocols
self.class.read_inheritable_attribute("header_and_body_protocols") || []
end
def body_only_protocols
self.class.read_inheritable_attribute("body_only_protocols") || []
end
end
end
end
end

View file

@ -0,0 +1,484 @@
require 'soap/processor'
require 'soap/mapping'
require 'soap/rpc/element'
require 'xsd/datatypes'
require 'xsd/ns'
require 'singleton'
module ActionService # :nodoc:
module Protocol # :nodoc:
module Soap # :nodoc:
class ProtocolError < ActionService::ActionServiceError # :nodoc:
end
def self.append_features(base) # :nodoc:
super
base.register_protocol(HeaderAndBody, SoapProtocol)
base.extend(ClassMethods)
base.wsdl_service_name('ActionService')
end
module ClassMethods
# Specifies the WSDL service name to use when generating WSDL. Highly
# recommended that you set this value, or code generators may generate
# classes with very generic names.
#
# === Example
# class MyController < ActionController::Base
# wsdl_service_name 'MyService'
# end
def wsdl_service_name(name)
write_inheritable_attribute("soap_mapper", SoapMapper.new("urn:#{name}"))
end
def soap_mapper # :nodoc:
read_inheritable_attribute("soap_mapper")
end
end
class SoapProtocol < AbstractProtocol # :nodoc:
attr :mapper
def initialize(mapper)
@mapper = mapper
end
def self.create_protocol_request(container_class, action_pack_request)
soap_action = extract_soap_action(action_pack_request)
return nil unless soap_action
service_name = action_pack_request.parameters['action']
public_method_name = soap_action.gsub(/^[\/]+/, '').split(/[\/]+/)[-1]
content_type = action_pack_request.env['HTTP_CONTENT_TYPE']
content_type ||= 'text/xml'
protocol = SoapProtocol.new(container_class.soap_mapper)
ProtocolRequest.new(protocol,
action_pack_request.raw_post,
service_name.to_sym,
public_method_name,
content_type)
end
def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
return nil unless protocol_name.to_s.downcase.to_sym == :soap
ActionService::Client::Soap.new(api, endpoint_uri, options)
end
def unmarshal_request(protocol_request)
unmarshal = lambda do
envelope = SOAP::Processor.unmarshal(protocol_request.raw_body)
request = envelope.body.request
values = request.collect{|k, v| request[k]}
soap_to_ruby_array(values)
end
signature = protocol_request.signature
if signature
map_signature_types(signature)
values = unmarshal.call
signature = signature.map{|x|mapper.lookup(x).ruby_klass}
protocol_request.check_parameter_types(values, signature)
values
else
if protocol_request.checked?
[]
else
unmarshal.call
end
end
end
def marshal_response(protocol_request, return_value)
marshal = lambda do |signature|
mapping = mapper.lookup(signature[0])
return_value = fixup_array_types(mapping, return_value)
signature = signature.map{|x|mapper.lookup(x).ruby_klass}
protocol_request.check_parameter_types([return_value], signature)
param_def = [['retval', 'return', mapping.registry_mapping]]
[param_def, ruby_to_soap(return_value)]
end
signature = protocol_request.return_signature
param_def = nil
if signature
param_def, return_value = marshal.call(signature)
else
if protocol_request.checked?
param_def, return_value = nil, nil
else
param_def, return_value = marshal.call([return_value.class])
end
end
qname = XSD::QName.new(mapper.custom_namespace,
protocol_request.public_method_name)
response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def)
response.retval = return_value unless return_value.nil?
ProtocolResponse.new(self, create_response(response), 'text/xml')
end
def marshal_exception(exc)
ProtocolResponse.new(self, create_exception_response(exc), 'text/xml')
end
private
def self.extract_soap_action(request)
return nil unless request.method == :post
content_type = request.env['HTTP_CONTENT_TYPE'] || 'text/xml'
return nil unless content_type
soap_action = request.env['HTTP_SOAPACTION']
return nil unless soap_action
soap_action.gsub!(/^"/, '')
soap_action.gsub!(/"$/, '')
soap_action.strip!
return nil if soap_action.empty?
soap_action
end
def fixup_array_types(mapping, obj)
mapping.each_attribute do |name, type, attr_mapping|
if attr_mapping.custom_type?
attr_obj = obj.send(name)
new_obj = fixup_array_types(attr_mapping, attr_obj)
obj.send("#{name}=", new_obj) unless new_obj.equal?(attr_obj)
end
end
if mapping.is_a?(SoapArrayMapping)
obj = mapping.ruby_klass.new(obj)
# man, this is going to be slow for big arrays :(
(1..obj.size).each do |i|
i -= 1
obj[i] = fixup_array_types(mapping.element_mapping, obj[i])
end
else
if !mapping.generated_klass.nil? && mapping.generated_klass.respond_to?(:members)
# have to map the publically visible structure of the class
new_obj = mapping.generated_klass.new
mapping.generated_klass.members.each do |name, klass|
new_obj.send("#{name}=", obj.send(name))
end
obj = new_obj
end
end
obj
end
def map_signature_types(types)
types.collect{|type| mapper.map(type)}
end
def create_response(body)
header = SOAP::SOAPHeader.new
body = SOAP::SOAPBody.new(body)
envelope = SOAP::SOAPEnvelope.new(header, body)
SOAP::Processor.marshal(envelope)
end
def create_exception_response(exc)
detail = SOAP::Mapping::SOAPException.new(exc)
body = SOAP::SOAPFault.new(
SOAP::SOAPString.new('Server'),
SOAP::SOAPString.new(exc.to_s),
SOAP::SOAPString.new(self.class.name),
SOAP::Mapping.obj2soap(detail))
create_response(body)
end
def ruby_to_soap(obj)
SOAP::Mapping.obj2soap(obj, mapper.registry)
end
def soap_to_ruby(obj)
SOAP::Mapping.soap2obj(obj, mapper.registry)
end
def soap_to_ruby_array(array)
array.map{|x| soap_to_ruby(x)}
end
end
class SoapMapper # :nodoc:
attr :registry
attr :custom_namespace
attr :custom_types
def initialize(custom_namespace)
@custom_namespace = custom_namespace
@registry = SOAP::Mapping::Registry.new
@klass2map = {}
@custom_types = {}
@ar2klass = {}
end
def lookup(klass)
lookup_klass = klass.is_a?(Array) ? klass[0] : klass
generated_klass = nil
unless lookup_klass.respond_to?(:ancestors)
raise(ProtocolError, "expected parameter type definition to be a Class")
end
if lookup_klass.ancestors.include?(ActiveRecord::Base)
generated_klass = @ar2klass.has_key?(klass) ? @ar2klass[klass] : nil
klass = generated_klass if generated_klass
end
return @klass2map[klass] if @klass2map.has_key?(klass)
custom_type = false
ruby_klass = select_class(lookup_klass)
generated_klass = @ar2klass[lookup_klass] if @ar2klass.has_key?(lookup_klass)
type_name = ruby_klass.name
# Array signatures generate a double-mapping and require generation
# of an Array subclass to represent the mapping in the SOAP
# registry
array_klass = nil
if klass.is_a?(Array)
array_klass = Class.new(Array) do
module_eval <<-END
def self.name
"#{type_name}Array"
end
END
end
end
mapping = @registry.find_mapped_soap_class(ruby_klass) rescue nil
unless mapping
# Custom structured type, generate a mapping
info = { :type => XSD::QName.new(@custom_namespace, type_name) }
@registry.add(ruby_klass,
SOAP::SOAPStruct,
SOAP::Mapping::Registry::TypedStructFactory,
info)
mapping = ensure_mapped(ruby_klass)
custom_type = true
end
array_mapping = nil
if array_klass
# Typed array always requires a custom type. The info of the array
# is the info of its element type (in mapping[2]), falling back
# to SOAP base types.
info = mapping[2]
info ||= {}
info[:type] ||= soap_base_type_qname(mapping[0])
@registry.add(array_klass,
SOAP::SOAPArray,
SOAP::Mapping::Registry::TypedArrayFactory,
info)
array_mapping = ensure_mapped(array_klass)
end
if array_mapping
@klass2map[ruby_klass] = SoapMapping.new(self,
type_name,
ruby_klass,
generated_klass,
mapping[0],
mapping,
custom_type)
@klass2map[klass] = SoapArrayMapping.new(self,
type_name,
array_klass,
array_mapping[0],
array_mapping,
@klass2map[ruby_klass])
@custom_types[klass] = @klass2map[klass]
@custom_types[ruby_klass] = @klass2map[ruby_klass] if custom_type
else
@klass2map[klass] = SoapMapping.new(self,
type_name,
ruby_klass,
generated_klass,
mapping[0],
mapping,
custom_type)
@custom_types[klass] = @klass2map[klass] if custom_type
end
@klass2map[klass]
end
alias :map :lookup
def map_container_services(container, &block)
dispatching_mode = container.service_dispatching_mode
services = nil
case dispatching_mode
when :direct
api = container.class.service_api
if container.respond_to?(:controller_class_name)
service_name = container.controller_class_name.sub(/Controller$/, '').underscore
else
service_name = container.class.name.demodulize.underscore
end
services = { service_name => api }
when :delegated
services = {}
container.class.services.each do |service_name, service_info|
begin
object = container.service_object(service_name)
rescue Exception => e
raise(ProtocolError, "failed to retrieve service object for mapping: #{e.message}")
end
services[service_name] = object.class.service_api
end
end
services.each do |service_name, api|
if api.nil?
raise(ProtocolError, "no service API set while in :#{dispatching_mode} mode")
end
map_api(api) do |api_methods|
yield service_name, api, api_methods if block_given?
end
end
end
def map_api(api, &block)
lookup_proc = lambda do |klass|
mapping = lookup(klass)
custom_mapping = nil
if mapping.respond_to?(:element_mapping)
custom_mapping = mapping.element_mapping
else
custom_mapping = mapping
end
if custom_mapping && custom_mapping.custom_type?
# What gives? This is required so that structure types
# referenced only by structures (and not signatures) still
# have a custom type mapping in the registry (needed for WSDL
# generation).
custom_mapping.each_attribute{}
end
mapping
end
api_methods = block.nil?? nil : {}
api.api_methods.each do |method_name, method_info|
expects = method_info[:expects]
expects_signature = nil
if expects
expects_signature = block ? [] : nil
expects.each do |klass|
lookup_klass = nil
if klass.is_a?(Hash)
lookup_klass = lookup_proc.call(klass.values[0])
expects_signature << {klass.keys[0]=>lookup_klass} if block
else
lookup_klass = lookup_proc.call(klass)
expects_signature << lookup_klass if block
end
end
end
returns = method_info[:returns]
returns_signature = returns ? returns.map{|klass| lookup_proc.call(klass)} : nil
if block
api_methods[method_name] = {
:expects => expects_signature,
:returns => returns_signature
}
end
end
yield api_methods if block
end
private
def select_class(klass)
return Integer if klass == Fixnum
if klass.ancestors.include?(ActiveRecord::Base)
new_klass = Class.new(ActionService::Struct)
new_klass.class_eval <<-EOS
def self.name
"#{klass.name}"
end
EOS
klass.columns.each do |column|
next if column.klass.nil?
new_klass.send(:member, column.name.to_sym, column.klass)
end
@ar2klass[klass] = new_klass
return new_klass
end
klass
end
def ensure_mapped(klass)
mapping = @registry.find_mapped_soap_class(klass) rescue nil
raise(ProtocolError, "failed to register #{klass.name}") unless mapping
mapping
end
def soap_base_type_qname(base_type)
xsd_type = base_type.ancestors.find{|c| c.const_defined? 'Type'}
xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type
end
end
class SoapMapping # :nodoc:
attr :ruby_klass
attr :generated_klass
attr :soap_klass
attr :registry_mapping
def initialize(mapper, type_name, ruby_klass, generated_klass, soap_klass, registry_mapping,
custom_type=false)
@mapper = mapper
@type_name = type_name
@ruby_klass = ruby_klass
@generated_klass = generated_klass
@soap_klass = soap_klass
@registry_mapping = registry_mapping
@custom_type = custom_type
end
def type_name
@type_name
end
def custom_type?
@custom_type
end
def qualified_type_name
name = type_name
if custom_type?
"typens:#{name}"
else
xsd_type_for(@soap_klass)
end
end
def each_attribute(&block)
if @ruby_klass.respond_to?(:members)
@ruby_klass.members.each do |name, klass|
name = name.to_s
mapping = @mapper.lookup(klass)
yield name, mapping.qualified_type_name, mapping
end
end
end
def is_xsd_type?(klass)
klass.ancestors.include?(XSD::NSDBase)
end
def xsd_type_for(klass)
ns = XSD::NS.new
ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag)
xsd_klass = klass.ancestors.find{|c| c.const_defined?('Type')}
return ns.name(XSD::AnyTypeName) unless xsd_klass
ns.name(xsd_klass.const_get('Type'))
end
end
class SoapArrayMapping < SoapMapping # :nodoc:
attr :element_mapping
def initialize(mapper, type_name, ruby_klass, soap_klass, registry_mapping, element_mapping)
super(mapper, type_name, ruby_klass, nil, soap_klass, registry_mapping, true)
@element_mapping = element_mapping
end
def type_name
super + "Array"
end
def each_attribute(&block); end
end
end
end
end

View file

@ -0,0 +1,187 @@
require 'xmlrpc/parser'
require 'xmlrpc/create'
require 'xmlrpc/config'
require 'xmlrpc/utils'
require 'singleton'
module XMLRPC # :nodoc:
class XmlRpcHelper # :nodoc:
include Singleton
include ParserWriterChooseMixin
def parse_method_call(message)
parser().parseMethodCall(message)
end
def create_method_response(successful, return_value)
create().methodResponse(successful, return_value)
end
end
end
module ActionService # :nodoc:
module Protocol # :nodoc:
module XmlRpc # :nodoc:
def self.append_features(base) # :nodoc:
super
base.register_protocol(BodyOnly, XmlRpcProtocol)
end
class XmlRpcProtocol < AbstractProtocol # :nodoc:
public
def self.create_protocol_request(container_class, action_pack_request)
helper = XMLRPC::XmlRpcHelper.instance
service_name = action_pack_request.parameters['action']
methodname, params = helper.parse_method_call(action_pack_request.raw_post)
methodname.gsub!(/^[^\.]+\./, '') unless methodname =~ /^system\./ # XXX
protocol = XmlRpcProtocol.new(container_class)
content_type = action_pack_request.env['HTTP_CONTENT_TYPE']
content_type ||= 'text/xml'
request = ProtocolRequest.new(protocol,
action_pack_request.raw_post,
service_name.to_sym,
methodname,
content_type,
:xmlrpc_values => params)
request
rescue
nil
end
def self.create_protocol_client(api, protocol_name, endpoint_uri, options)
return nil unless protocol_name.to_s.downcase.to_sym == :xmlrpc
ActionService::Client::XmlRpc.new(api, endpoint_uri, options)
end
def initialize(container_class)
super(container_class)
container_class.write_inheritable_hash('default_system_methods', XmlRpcProtocol => method(:xmlrpc_default_system_handler))
end
def unmarshal_request(protocol_request)
values = protocol_request.options[:xmlrpc_values]
signature = protocol_request.signature
if signature
values = self.class.transform_incoming_method_params(self.class.transform_array_types(signature), values)
protocol_request.check_parameter_types(values, check_array_types(signature))
values
else
protocol_request.checked? ? [] : values
end
end
def marshal_response(protocol_request, return_value)
helper = XMLRPC::XmlRpcHelper.instance
signature = protocol_request.return_signature
if signature
protocol_request.check_parameter_types([return_value], check_array_types(signature))
return_value = self.class.transform_return_value(self.class.transform_array_types(signature), return_value)
raw_response = helper.create_method_response(true, return_value)
else
# XML-RPC doesn't have the concept of a void method, nor does it
# support a nil return value, so return true if we would have returned
# nil
if protocol_request.checked?
raw_response = helper.create_method_response(true, true)
else
return_value = true if return_value.nil?
raw_response = helper.create_method_response(true, return_value)
end
end
ProtocolResponse.new(self, raw_response, 'text/xml')
end
def marshal_exception(exception)
helper = XMLRPC::XmlRpcHelper.instance
exception = XMLRPC::FaultException.new(1, exception.message)
raw_response = helper.create_method_response(false, exception)
ProtocolResponse.new(self, raw_response, 'text/xml')
end
class << self
def transform_incoming_method_params(signature, params)
(1..signature.size).each do |i|
i -= 1
params[i] = xmlrpc_to_ruby(params[i], signature[i])
end
params
end
def transform_return_value(signature, return_value)
ruby_to_xmlrpc(return_value, signature[0])
end
def ruby_to_xmlrpc(param, param_class)
if param_class.is_a?(XmlRpcArray)
param.map{|p| ruby_to_xmlrpc(p, param_class.klass)}
elsif param_class.ancestors.include?(ActiveRecord::Base)
param.instance_variable_get('@attributes')
elsif param_class.ancestors.include?(ActionService::Struct)
struct = {}
param_class.members.each do |name, klass|
value = param.send(name)
next if value.nil?
struct[name.to_s] = value
end
struct
else
param
end
end
def xmlrpc_to_ruby(param, param_class)
if param_class.is_a?(XmlRpcArray)
param.map{|p| xmlrpc_to_ruby(p, param_class.klass)}
elsif param_class.ancestors.include?(ActiveRecord::Base)
raise(ProtocolError, "incoming ActiveRecord::Base types are not allowed")
elsif param_class.ancestors.include?(ActionService::Struct)
unless param.is_a?(Hash)
raise(ProtocolError, "expected parameter to be a Hash")
end
new_param = param_class.new
param_class.members.each do |name, klass|
new_param.send('%s=' % name.to_s, param[name.to_s])
end
new_param
else
param
end
end
def transform_array_types(signature)
signature.map{|x| x.is_a?(Array) ? XmlRpcArray.new(x[0]) : x}
end
end
private
def xmlrpc_default_system_handler(name, service_class, *args)
case name
when 'system.listMethods'
methods = []
api = service_class.service_api
api.api_methods.each do |name, info|
methods << api.public_api_method_name(name)
end
methods.sort
else
throw :try_default
end
end
def check_array_types(signature)
signature.map{|x| x.is_a?(Array) ? Array : x}
end
class XmlRpcArray
attr :klass
def initialize(klass)
@klass = klass
end
end
end
end
end
end

View file

@ -0,0 +1,2 @@
require 'action_service/router/action_controller'
require 'action_service/router/wsdl'

View file

@ -0,0 +1,97 @@
module ActionService # :nodoc:
module Router # :nodoc:
module ActionController # :nodoc:
def self.append_features(base) # :nodoc:
base.add_service_api_callback do |container_class, api|
if container_class.service_dispatching_mode == :direct && !container_class.method_defined?(:api)
container_class.class_eval <<-EOS
def api
process_action_service_request
end
EOS
end
end
base.add_service_definition_callback do |klass, name, info|
if klass.service_dispatching_mode == :delegated
klass.class_eval <<-EOS
def #{name}
process_action_service_request
end
EOS
end
end
base.send(:include, ActionService::Router::ActionController::InstanceMethods)
end
module InstanceMethods # :nodoc:
private
def process_action_service_request
protocol_request = nil
begin
begin
protocol_request = probe_request_protocol(self.request)
rescue Exception => e
logger.error "Invalid request: #{e.message}"
logger.error self.request.raw_post
raise
end
if protocol_request
log_request(protocol_request)
protocol_response = dispatch_service_request(protocol_request)
log_response(protocol_response)
response_options = {
:type => protocol_response.content_type,
:disposition => 'inline'
}
send_data(protocol_response.raw_body, response_options)
else
logger.fatal "Invalid Action Service service or method requested"
render_text 'Internal protocol error', "500 Invalid service/method"
end
rescue Exception => e
log_error e unless logger.nil?
exc_response = nil
case service_dispatching_mode
when :direct
if self.class.service_exception_reporting
exc_response = protocol_request.protocol.marshal_exception(e)
end
when :delegated
service_object = service_object(protocol_request.service_name) rescue nil
if service_object && service_object.class.service_exception_reporting
exc_response = protocol_request.protocol.marshal_exception(e) rescue nil
end
end
if exc_response
response_options = {
:type => exc_response.content_type,
:disposition => 'inline'
}
log_response exc_response
send_data(exc_response.raw_body, response_options)
else
render_text 'Internal protocol error', "500 #{e.message}"
end
end
end
def log_request(protocol_request)
unless logger.nil?
service_name = protocol_request.service_name
method_name = protocol_request.public_method_name
logger.info "\nProcessing Action Service Request: #{service_name}##{method_name}"
logger.info "Raw Request Body:"
logger.info protocol_request.raw_body
end
end
def log_response(protocol_response)
unless logger.nil?
logger.info "\nRaw Response Body:"
logger.info protocol_response.raw_body
end
end
end
end
end
end

View file

@ -0,0 +1,210 @@
module ActionService # :nodoc:
module Router # :nodoc:
module Wsdl # :nodoc:
def self.append_features(base) # :nodoc:
base.class_eval do
class << self
alias_method :inherited_without_wsdl, :inherited
end
end
base.extend(ClassMethods)
end
module ClassMethods
def inherited(child)
inherited_without_wsdl(child)
child.send(:include, ActionService::Router::Wsdl::InstanceMethods)
end
end
module InstanceMethods # :nodoc:
XsdNs = 'http://www.w3.org/2001/XMLSchema'
WsdlNs = 'http://schemas.xmlsoap.org/wsdl/'
SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/'
SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/'
SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http'
def wsdl
case @request.method
when :get
begin
host_name = @request.env['HTTP_HOST']||@request.env['SERVER_NAME']
uri = "http://#{host_name}/#{controller_name}/"
soap_action_base = "/#{controller_name}"
xml = to_wsdl(self, uri, soap_action_base)
send_data(xml, :type => 'text/xml', :disposition => 'inline')
rescue Exception => e
log_error e unless logger.nil?
render_text('', "500 #{e.message}")
end
when :post
render_text('', "500 POST not supported")
end
end
private
def to_wsdl(container, uri, soap_action_base)
wsdl = ""
service_dispatching_mode = container.service_dispatching_mode
mapper = container.class.soap_mapper
namespace = mapper.custom_namespace
wsdl_service_name = namespace.split(/:/)[1]
services = {}
mapper.map_container_services(container) do |name, api, api_methods|
services[name] = [api, api_methods]
end
custom_types = mapper.custom_types
xm = Builder::XmlMarkup.new(:target => wsdl, :indent => 2)
xm.instruct!
xm.definitions('name' => wsdl_service_name,
'targetNamespace' => namespace,
'xmlns:typens' => namespace,
'xmlns:xsd' => XsdNs,
'xmlns:soap' => SoapNs,
'xmlns:soapenc' => SoapEncodingNs,
'xmlns:wsdl' => WsdlNs,
'xmlns' => WsdlNs) do
# Custom type XSD generation
if custom_types.size > 0
xm.types do
xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do
custom_types.each do |klass, mapping|
case
when mapping.is_a?(ActionService::Protocol::Soap::SoapArrayMapping)
xm.xsd(:complexType, 'name' => mapping.type_name) do
xm.xsd(:complexContent) do
xm.xsd(:restriction, 'base' => 'soapenc:Array') do
xm.xsd(:attribute, 'ref' => 'soapenc:arrayType',
'wsdl:arrayType' => mapping.element_mapping.qualified_type_name + '[]')
end
end
end
when mapping.is_a?(ActionService::Protocol::Soap::SoapMapping)
xm.xsd(:complexType, 'name' => mapping.type_name) do
xm.xsd(:all) do
mapping.each_attribute do |name, type_name|
xm.xsd(:element, 'name' => name, 'type' => type_name)
end
end
end
else
raise(WsdlError, "unsupported mapping type #{mapping.class.name}")
end
end
end
end
end
services.each do |service_name, service_values|
service_api, api_methods = service_values
# Parameter list message definitions
api_methods.each do |method_name, method_signature|
gen = lambda do |msg_name, direction|
xm.message('name' => msg_name) do
sym = nil
if direction == :out
if method_signature[:returns]
xm.part('name' => 'return', 'type' => method_signature[:returns][0].qualified_type_name)
end
else
mapping_list = method_signature[:expects]
i = 1
mapping_list.each do |mapping|
if mapping.is_a?(Hash)
param_name = mapping.keys.shift
mapping = mapping.values.shift
else
param_name = "param#{i}"
end
xm.part('name' => param_name, 'type' => mapping.qualified_type_name)
i += 1
end if mapping_list
end
end
end
public_name = service_api.public_api_method_name(method_name)
gen.call(public_name, :in)
gen.call("#{public_name}Response", :out)
end
# Declare the port
port_name = port_name_for(wsdl_service_name, service_name)
xm.portType('name' => port_name) do
api_methods.each do |method_name, method_signature|
public_name = service_api.public_api_method_name(method_name)
xm.operation('name' => public_name) do
xm.input('message' => "typens:#{public_name}")
xm.output('message' => "typens:#{public_name}Response")
end
end
end
# Bind the port to SOAP
binding_name = binding_name_for(wsdl_service_name, service_name)
xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do
xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport)
api_methods.each do |method_name, method_signature|
public_name = service_api.public_api_method_name(method_name)
xm.operation('name' => public_name) do
case service_dispatching_mode
when :direct
soap_action = soap_action_base + "/api/" + public_name
when :delegated
soap_action = soap_action_base \
+ "/" + service_name.to_s \
+ "/" + public_name
end
xm.soap(:operation, 'soapAction' => soap_action)
xm.input do
xm.soap(:body,
'use' => 'encoded',
'namespace' => namespace,
'encodingStyle' => SoapEncodingNs)
end
xm.output do
xm.soap(:body,
'use' => 'encoded',
'namespace' => namespace,
'encodingStyle' => SoapEncodingNs)
end
end
end
end
end
# Define the service
xm.service('name' => "#{wsdl_service_name}Service") do
services.each do |service_name, service_values|
port_name = port_name_for(wsdl_service_name, service_name)
binding_name = binding_name_for(wsdl_service_name, service_name)
case service_dispatching_mode
when :direct
binding_target = 'api'
when :delegated
binding_target = service_name.to_s
end
xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do
xm.soap(:address, 'location' => "#{uri}#{binding_target}")
end
end
end
end
end
def port_name_for(wsdl_service_name, service_name)
"#{wsdl_service_name}#{service_name.to_s.camelize}Port"
end
def binding_name_for(wsdl_service_name, service_name)
"#{wsdl_service_name}#{service_name.to_s.camelize}Binding"
end
end
end
end
end

View file

@ -0,0 +1,56 @@
module ActionService
# To send structured types across the wire, derive from ActionService::Struct,
# and use +member+ to declare structure members.
#
# ActionService::Struct should be used in method signatures when you want to accept or return
# structured types that have no Active Record model class representations, or you don't
# want to expose your entire Active Record model to remote callers.
#
# === Example
#
# class Person < ActionService::Struct
# member :id, :int
# member :firstnames, [:string]
# member :lastname, :string
# member :email, :string
# end
#
# Active Record model classes are already implicitly supported for method
# return signatures. A structure containing its columns as members will be
# automatically generated if its present in a signature.
#
# The structure
class Struct
# If a Hash is given as argument to an ActionService::Struct constructor,
# containing as key the member name, and its associated initial value
def initialize(values={})
if values.is_a?(Hash)
values.map{|k,v| send('%s=' % k.to_s, v)}
end
end
# The member with the given name
def [](name)
send(name.to_s)
end
class << self
include ActionService::Signature
# Creates a structure member accessible using +name+. Generates
# accessor methods for reading and writing the member value.
def member(name, type)
write_inheritable_hash("struct_members", name => signature_parameter_class(type))
class_eval <<-END
def #{name}; @#{name}; end
def #{name}=(value); @#{name} = value; end
END
end
def members # :nodoc:
read_inheritable_attribute("struct_members") || {}
end
end
end
end

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,100 @@
module ActionService # :nodoc:
# Action Service parameter type specifiers may contain symbols or strings
# instead of Class objects, for a limited set of base types.
#
# This provides an unambiguous way to specify that a given parameter
# contains an integer or boolean value, for example.
#
# The allowed set of symbol/string aliases:
#
# [<tt>:int</tt>] any integer value
# [<tt>:float</tt>] any floating point value
# [<tt>:string</tt>] any string value
# [<tt>:bool</tt>] any boolean value
# [<tt>:time</tt>] any value containing both date and time
# [<tt>:date</tt>] any value containing only a date
module Signature
class SignatureError < StandardError # :nodoc:
end
private
def canonical_signature(params)
return nil if params.nil?
params.map do |param|
klass = signature_parameter_class(param)
if param.is_a?(Hash)
param[param.keys[0]] = klass
param
else
klass
end
end
end
def signature_parameter_class(param)
param = param.is_a?(Hash) ? param.values[0] : param
is_array = param.is_a?(Array)
param = is_array ? param[0] : param
param = param.is_a?(String) ? param.to_sym : param
param = param.is_a?(Symbol) ? signature_ruby_class(param) : param
is_array ? [param] : param
end
def canonical_signature_base_type(base_type)
base_type = base_type.to_sym
case base_type
when :int, :integer, :fixnum, :bignum
:int
when :string, :base64
:string
when :bool, :boolean
:bool
when :float, :double
:float
when :time, :datetime, :timestamp
:time
when :date
:date
else
raise(SignatureError, ":#{base_type} is not an ActionService base type")
end
end
def signature_ruby_class(base_type)
case canonical_signature_base_type(base_type)
when :int
Integer
when :string
String
when :bool
TrueClass
when :float
Float
when :time
Time
when :date
Date
end
end
def signature_base_type(ruby_class)
case ruby_class
when Bignum, Integer, Fixnum
:int
when String
:string
when TrueClass, FalseClass
:bool
when Float, Numeric, Precision
:float
when Time, DateTime
:time
when Date
:date
else
raise(SignatureError, "#{ruby_class.name} is not an ActionService base type")
end
end
end
end

1360
actionservice/setup.rb Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,124 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'webrick'
require 'webrick/log'
require 'singleton'
module ClientTest
class Person < ActionService::Struct
member :firstnames, [:string]
member :lastname, :string
def ==(other)
firstnames == other.firstnames && lastname == other.lastname
end
end
class API < ActionService::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 :client_container, :returns => [:int]
end
class NullLogOut
def <<(*args); end
end
class Container < ActionController::Base
service_api API
attr :value_void
attr :value_normal
attr :value_array_return
attr :value_struct_pass
def initialize
@session = @assigns = {}
@value_void = nil
@value_normal = nil
@value_array_return = nil
@value_struct_pass = 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 client_container
50
end
def protocol_request(request)
probe_request_protocol(request)
end
def dispatch_request(protocol_request)
dispatch_service_request(protocol_request)
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 => log)
@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,58 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'soap/rpc/element'
class SoapTestError < StandardError
end
class AbstractSoapTest < Test::Unit::TestCase
def default_test
end
protected
def service_name
raise NotImplementedError
end
def do_soap_call(public_method_name, *args)
mapper = @container.class.soap_mapper
param_def = []
i = 1
args.each do |arg|
mapping = mapper.lookup(arg.class)
param_def << ["in", "param#{i}", mapping.registry_mapping]
i += 1
end
qname = XSD::QName.new('urn:ActionService', public_method_name)
request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def)
soap_args = []
i = 1
args.each do |arg|
soap_args << ["param#{i}", SOAP::Mapping.obj2soap(arg)]
i += 1
end
request.set_param(soap_args)
header = SOAP::SOAPHeader.new
body = SOAP::SOAPBody.new(request)
envelope = SOAP::SOAPEnvelope.new(header, body)
raw_request = SOAP::Processor.marshal(envelope)
test_request = ActionController::TestRequest.new
test_request.request_parameters['action'] = service_name
test_request.env['REQUEST_METHOD'] = "POST"
test_request.env['HTTP_CONTENTTYPE'] = 'text/xml'
test_request.env['HTTP_SOAPACTION'] = "/soap/#{service_name}/#{public_method_name}"
test_request.env['RAW_POST_DATA'] = raw_request
test_response = ActionController::TestResponse.new
response = yield test_request, test_response
raw_body = response.respond_to?(:body) ? response.body : response.raw_body
envelope = SOAP::Processor.unmarshal(raw_body)
if envelope
if envelope.body.response
SOAP::Mapping.soap2obj(envelope.body.response)
else
nil
end
else
raise(SoapTestError, "empty/invalid body from server")
end
end
end

View file

@ -0,0 +1,9 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
require 'action_service'
require 'action_controller'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = true

View file

@ -0,0 +1,52 @@
require File.dirname(__FILE__) + '/abstract_unit'
module APITest
class API < ActionService::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']
api_method :class_types, :expects => [TrueClass, Bignum, String]
end
end
class TC_API < Test::Unit::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({:expects=>nil, :returns=>nil}, API.api_methods[:void])
assert_equal({:expects=>[String], :returns=>[String]}, API.api_methods[:expects_and_returns])
assert_equal({:expects=>[Integer, TrueClass], :returns=>nil}, API.api_methods[:expects])
assert_equal({:expects=>nil, :returns=>[Integer, [String]]}, API.api_methods[:returns])
assert_equal({:expects=>[{:appkey=>Integer}, {:publish=>TrueClass}], :returns=>nil}, API.api_methods[:named_signature])
assert_equal({:expects=>[Integer, String, TrueClass], :returns=>nil}, API.api_methods[:string_types])
assert_equal({:expects=>[TrueClass, Bignum, String], :returns=>nil}, API.api_methods[:class_types])
end
def test_not_instantiable
assert_raises(NoMethodError) do
API.new
end
end
end

View file

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

View file

@ -0,0 +1,87 @@
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
protocol_request = @controller.protocol_request(test_request)
response = @controller.dispatch_request(protocol_request)
res.header['content-type'] = 'text/xml'
res.body = response.raw_body
rescue Exception => e
$stderr.puts e.message
$stderr.puts e.backtrace.join("\n")
end
end
class ClientContainer < ActionController::Base
client_api :client, :soap, "http://localhost:#{PORT}/client/api", :api => ClientTest::API
def get_client
client
end
end
class SoapServer < ClientTest::AbstractServer
def create_clientlet(controller)
SoapClientLet.new(controller)
end
def server_port
PORT
end
end
end
class TC_ClientSoap < Test::Unit::TestCase
include ClientTest
include ClientSoapTest
def setup
@server = SoapServer.instance
@container = @server.container
@client = ActionService::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)
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_client_container
assert_equal(50, ClientContainer.new.get_client.client_container)
end
end

View file

@ -0,0 +1,86 @@
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_CONTENTTYPE'] = 'text/xml'
test_request.env['RAW_POST_DATA'] = req.body
protocol_request = @controller.protocol_request(test_request)
response = @controller.dispatch_request(protocol_request)
res.header['content-type'] = 'text/xml'
res.body = response.raw_body
rescue Exception => e
$stderr.puts e.message
$stderr.puts e.backtrace.join("\n")
end
end
class ClientContainer < ActionController::Base
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 < Test::Unit::TestCase
include ClientTest
include ClientXmlRpcTest
def setup
@server = XmlRpcServer.instance
@container = @server.container
@client = ActionService::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)
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_client_container
assert_equal(50, ClientContainer.new.get_client.client_container)
end
end

View file

@ -0,0 +1,53 @@
require File.dirname(__FILE__) + '/abstract_unit'
module ContainerTest
$immediate_service = Object.new
$deferred_service = Object.new
class DelegateContainer < ActionController::Base
service_dispatching_mode :delegated
attr :flag
attr :previous_flag
def initialize
@previous_flag = nil
@flag = true
end
service :immediate_service, $immediate_service
service(:deferred_service) { @previous_flag = @flag; @flag = false; $deferred_service }
end
class DirectContainer < ActionController::Base
service_dispatching_mode :direct
end
end
class TC_Container < Test::Unit::TestCase
def setup
@delegate_container = ContainerTest::DelegateContainer.new
@direct_container = ContainerTest::DirectContainer.new
end
def test_registration
assert(ContainerTest::DelegateContainer.has_service?(:immediate_service))
assert(ContainerTest::DelegateContainer.has_service?(:deferred_service))
assert(!ContainerTest::DelegateContainer.has_service?(:fake_service))
end
def test_service_object
assert(@delegate_container.flag == true)
assert(@delegate_container.service_object(:immediate_service) == $immediate_service)
assert(@delegate_container.previous_flag.nil?)
assert(@delegate_container.flag == true)
assert(@delegate_container.service_object(:deferred_service) == $deferred_service)
assert(@delegate_container.previous_flag == true)
assert(@delegate_container.flag == false)
end
def test_direct_container
assert(ContainerTest::DirectContainer.service_dispatching_mode == :direct)
end
end

View file

@ -0,0 +1,158 @@
require File.dirname(__FILE__) + '/abstract_unit'
module InvocationTest
class API < ActionService::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 Service < ActionService::Base
service_api API
before_invocation :intercept_before, :except => [:no_before]
after_invocation :intercept_after, :except => [:no_after]
before_invocation :intercept_only, :only => [:only_one, :only_two]
attr_accessor :before_invoked
attr_accessor :after_invoked
attr_accessor :only_invoked
attr_accessor :invocation_result
def initialize
@before_invoked = nil
@after_invoked = nil
@only_invoked = nil
@invocation_result = 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
def not_public
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_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 ActionService::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(InvocationError) do
perform_invocation(:not_public)
end
assert_raises(InvocationError) 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 == 1)
end
def test_interception
assert(@service.before_invoked.nil? && @service.after_invoked.nil? && @service.only_invoked.nil? && @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_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)
@service.only_invoked = nil
perform_invocation(:only_two)
assert(@service.only_invoked == :only_two)
end
private
def perform_invocation(method_name, *args, &block)
public_method_name = @service.class.service_api.public_api_method_name(method_name)
args ||= []
request = InvocationRequest.new(ConcreteInvocation, public_method_name, method_name, args)
@service.perform_invocation(request, &block)
end
end

View file

@ -0,0 +1,53 @@
require File.dirname(__FILE__) + '/abstract_unit'
module Foo
include ActionService::Protocol
def self.append_features(base)
super
base.register_protocol(BodyOnly, FooMinimalProtocol)
base.register_protocol(HeaderAndBody, FooMinimalProtocolTwo)
base.register_protocol(HeaderAndBody, FooMinimalProtocolTwo)
base.register_protocol(HeaderAndBody, FooFullProtocol)
end
class FooFullProtocol < AbstractProtocol
def self.create_protocol_request(klass, request)
protocol = FooFullProtocol.new klass
ActionService::Protocol::ProtocolRequest.new(protocol, '', '', '', '')
end
end
class FooMinimalProtocol < AbstractProtocol
def self.create_protocol_request(klass, request)
protocol = FooMinimalProtocol.new klass
ActionService::Protocol::ProtocolRequest.new(protocol, '', '', '', '')
end
end
class FooMinimalProtocolTwo < AbstractProtocol
end
end
class ProtocolRegistry
include ActionService::Protocol::Registry
include Foo
def all_protocols
header_and_body_protocols + body_only_protocols
end
def protocol_request
probe_request_protocol(nil)
end
end
class TC_ProtocolRegistry < Test::Unit::TestCase
def test_registration
registry = ProtocolRegistry.new
assert(registry.all_protocols.length == 4)
assert(registry.protocol_request.protocol.is_a?(Foo::FooFullProtocol))
end
end

View file

@ -0,0 +1,226 @@
require File.dirname(__FILE__) + '/abstract_soap'
module ProtocolSoapTest
class Person < ActionService::Struct
member :id, Integer
member :names, [String]
member :lastname, String
member :deleted, TrueClass
def ==(other)
id == other.id && names == other.names && lastname == other.lastname && deleted == other.deleted
end
end
class API < ActionService::API::Base
api_method :argument_passing, :expects => [{:int=>:int}, {:string=>:string}, {:array=>[:int]}], :returns => [:bool]
api_method :array_returner, :returns => [[:int]]
api_method :nil_returner
api_method :struct_array_returner, :returns => [[Person]]
api_method :exception_thrower
default_api_method :default
end
class Service < ActionService::Base
service_api API
attr :int
attr :string
attr :array
attr :values
attr :person
attr :default_args
def initialize
@int = 20
@string = "wrong string value"
@default_args = nil
end
def argument_passing(int, string, array)
@int = int
@string = string
@array = array
true
end
def array_returner
@values = [1, 2, 3]
end
def nil_returner
nil
end
def struct_array_returner
@person = Person.new
@person.id = 5
@person.names = ["one", "two"]
@person.lastname = "test"
@person.deleted = false
[@person]
end
def exception_thrower
raise "Hi, I'm a SOAP error"
end
def default(*args)
@default_args = args
nil
end
end
class AbstractContainer
include ActionService::API
include ActionService::Container
include ActionService::Protocol::Registry
include ActionService::Protocol::Soap
wsdl_service_name 'Test'
def protocol_request(request)
probe_request_protocol(request)
end
def dispatch_request(protocol_request)
dispatch_service_request(protocol_request)
end
end
class DelegatedContainer < AbstractContainer
service_dispatching_mode :delegated
service :protocol_soap_service, Service.new
end
class DirectContainer < AbstractContainer
service_api API
service_dispatching_mode :direct
attr :int
attr :string
attr :array
attr :values
attr :person
attr :default_args
def initialize
@int = 20
@string = "wrong string value"
@default_args = nil
end
def argument_passing
@int = @params['int']
@string = @params['string']
@array = @params['array']
true
end
def array_returner
@values = [1, 2, 3]
end
def nil_returner
nil
end
def struct_array_returner
@person = Person.new
@person.id = 5
@person.names = ["one", "two"]
@person.lastname = "test"
@person.deleted = false
[@person]
end
def exception_thrower
raise "Hi, I'm a SOAP error"
end
def default
@default_args = @method_params
nil
end
end
end
class TC_ProtocolSoap < AbstractSoapTest
def setup
@delegated_container = ProtocolSoapTest::DelegatedContainer.new
@direct_container = ProtocolSoapTest::DirectContainer.new
end
def test_argument_passing
in_all_containers do
assert(do_soap_call('ArgumentPassing', 5, "test string", [true, false]) == true)
assert(service.int == 5)
assert(service.string == "test string")
assert(service.array == [true, false])
end
end
def test_array_returner
in_all_containers do
assert(do_soap_call('ArrayReturner') == [1, 2, 3])
assert(service.values == [1, 2, 3])
end
end
def test_nil_returner
in_all_containers do
assert(do_soap_call('NilReturner') == nil)
end
end
def test_struct_array_returner
in_all_containers do
assert(do_soap_call('StructArrayReturner') == [service.person])
end
end
def test_exception_thrower
in_all_containers do
assert_raises(RuntimeError) do
do_soap_call('ExceptionThrower')
end
end
end
def test_default_api_method
in_all_containers do
assert(do_soap_call('NonExistentMethodName', 50, false).nil?)
assert(service.default_args == [50, false])
end
end
def test_service_name_setting
in_all_containers do
assert(ProtocolSoapTest::DelegatedContainer.soap_mapper.custom_namespace == 'urn:Test')
end
end
protected
def service_name
@container == @direct_container ? 'api' : 'protocol_soap_service'
end
def service
@container == @direct_container ? @container : @container.service_object(:protocol_soap_service)
end
def in_all_containers(&block)
[@direct_container].each do |container|
@container = container
block.call
end
end
def do_soap_call(public_method_name, *args)
super(public_method_name, *args) do |test_request, test_response|
protocol_request = @container.protocol_request(test_request)
@container.dispatch_request(protocol_request)
end
end
end

View file

@ -0,0 +1,157 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'xmlrpc/parser'
require 'xmlrpc/create'
require 'xmlrpc/config'
module XMLRPC
class XmlRpcTestHelper
include ParserWriterChooseMixin
def create_request(methodName, *args)
create().methodCall(methodName, *args)
end
def parse_response(response)
parser().parseMethodResponse(response)
end
end
end
module ProtocolXmlRpcTest
class Person < ActionService::Struct
member :firstname, String
member :lastname, String
member :active, TrueClass
end
class API < ActionService::API::Base
api_method :add, :expects => [Integer, Integer], :returns => [Integer]
api_method :hash_returner, :returns => [Hash]
api_method :array_returner, :returns => [[Integer]]
api_method :something_hash, :expects => [Hash]
api_method :struct_array_returner, :returns => [[Person]]
default_api_method :default
end
class Service < ActionService::Base
service_api API
attr :result
attr :hashvalue
attr :default_args
def initialize
@result = nil
@hashvalue = nil
@default_args = nil
end
def add(a, b)
@result = a + b
end
def something_hash(hash)
@hashvalue = hash
end
def array_returner
[1, 2, 3]
end
def hash_returner
{'name' => 1, 'value' => 2}
end
def struct_array_returner
person = Person.new
person.firstname = "John"
person.lastname = "Doe"
person.active = true
[person]
end
def default(*args)
@default_args = args
nil
end
end
$service = Service.new
class Container
include ActionService::Container
include ActionService::Protocol::Registry
include ActionService::Protocol::Soap
include ActionService::Protocol::XmlRpc
def protocol_request(request)
probe_request_protocol(request)
end
def dispatch_request(protocol_request)
dispatch_service_request(protocol_request)
end
service :xmlrpc, $service
service_dispatching_mode :delegated
end
end
class TC_ProtocolXmlRpc < Test::Unit::TestCase
def setup
@helper = XMLRPC::XmlRpcTestHelper.new
@container = ProtocolXmlRpcTest::Container.new
end
def test_xmlrpc_request_dispatching
retval = do_xmlrpc_call('Add', 50, 30)
assert(retval == [true, 80])
end
def test_array_returning
retval = do_xmlrpc_call('ArrayReturner')
assert(retval == [true, [1, 2, 3]])
end
def test_hash_returning
retval = do_xmlrpc_call('HashReturner')
assert(retval == [true, {'name' => 1, 'value' => 2}])
end
def test_struct_array_returning
retval = do_xmlrpc_call('StructArrayReturner')
assert(retval == [true, [{"firstname"=>"John", "lastname"=>"Doe", "active"=>true}]])
end
def test_hash_parameter
retval = do_xmlrpc_call('SomethingHash', {'name' => 1, 'value' => 2})
assert(retval == [true, true])
assert($service.hashvalue == {'name' => 1, 'value' => 2})
end
def test_default_api_method
retval = do_xmlrpc_call('SomeNonexistentMethod', 'test', [1, 2], {'name'=>'value'})
assert(retval == [true, true])
assert($service.default_args == ['test', [1, 2], {'name'=>'value'}])
end
def test_xmlrpc_introspection
retval = do_xmlrpc_call('system.listMethods', 'test', [1, 2], {'name'=>'value'})
assert(retval == [true, ["Add", "ArrayReturner", "HashReturner", "SomethingHash", "StructArrayReturner"]])
end
private
def do_xmlrpc_call(public_method_name, *args)
service_name = 'xmlrpc'
raw_request = @helper.create_request(public_method_name, *args)
test_request = ActionController::TestRequest.new
test_request.request_parameters['action'] = service_name
test_request.env['REQUEST_METHOD'] = "POST"
test_request.env['HTTP_CONTENTTYPE'] = 'text/xml'
test_request.env['RAW_POST_DATA'] = raw_request
protocol_request = @container.protocol_request(test_request)
response = @container.dispatch_request(protocol_request)
@helper.parse_response(response.raw_body)
end
end

View file

@ -0,0 +1,139 @@
require File.dirname(__FILE__) + '/abstract_soap'
require 'wsdl/parser'
module RouterActionControllerTest
class API < ActionService::API::Base
api_method :add, :expects => [:int, :int], :returns => [:int]
end
class Service < ActionService::Base
service_api API
attr :added
def add(a, b)
@added = a + b
end
end
class DelegatedController < ActionController::Base
service_dispatching_mode :delegated
service(:test_service) { @service ||= Service.new; @service }
end
class DirectAPI < ActionService::API::Base
api_method :add, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int]
api_method :before_filtered
api_method :after_filtered, :returns => [:int]
api_method :thrower
end
class DirectController < ActionController::Base
service_api DirectAPI
service_dispatching_mode :direct
before_filter :alwaysfail, :only => [:before_filtered]
after_filter :alwaysok, :only => [:after_filtered]
attr :added
attr :before_filter_called
attr :before_filter_target_called
attr :after_filter_called
attr :after_filter_target_called
def initialize
@before_filter_called = false
@before_filter_target_called = false
@after_filter_called = false
@after_filter_target_called = false
end
def add
@added = @params['a'] + @params['b']
end
def before_filtered
@before_filter_target_called = true
end
def after_filtered
@after_filter_target_called = true
5
end
def thrower
raise "Hi, I'm a SOAP exception"
end
protected
def alwaysfail
@before_filter_called = true
false
end
def alwaysok
@after_filter_called = true
end
end
end
class TC_RouterActionController < AbstractSoapTest
def test_direct_routing
@container = RouterActionControllerTest::DirectController.new
assert(do_soap_call('Add', 20, 50) == 70)
assert(@container.added == 70)
end
def test_direct_entrypoint
@container = RouterActionControllerTest::DirectController.new
assert(@container.respond_to?(:api))
end
def test_direct_filtering
@container = RouterActionControllerTest::DirectController.new
assert(@container.before_filter_called == false)
assert(@container.before_filter_target_called == false)
assert(do_soap_call('BeforeFiltered').nil?)
assert(@container.before_filter_called == true)
assert(@container.before_filter_target_called == false)
assert(@container.after_filter_called == false)
assert(@container.after_filter_target_called == false)
assert(do_soap_call('AfterFiltered') == 5)
assert(@container.after_filter_called == true)
assert(@container.after_filter_target_called == true)
end
def test_delegated_routing
@container = RouterActionControllerTest::DelegatedController.new
assert(do_soap_call('Add', 50, 80) == 130)
assert(service.added == 130)
end
def test_exception_marshaling
@container = RouterActionControllerTest::DirectController.new
result = do_soap_call('Thrower')
exception = result.detail
assert(exception.cause.is_a?(RuntimeError))
assert_equal("Hi, I'm a SOAP exception", exception.cause.message)
@container.service_exception_reporting = false
assert_raises(SoapTestError) do
do_soap_call('Thrower')
end
end
protected
def service_name
@container.is_a?(RouterActionControllerTest::DelegatedController) ? 'test_service' : 'api'
end
def service
@container.service_object(:test_service)
end
def do_soap_call(public_method_name, *args)
super(public_method_name, *args) do |test_request, test_response|
response = @container.process(test_request, test_response)
end
end
end

View file

@ -0,0 +1,100 @@
require File.dirname(__FILE__) + '/abstract_unit'
require 'wsdl/parser'
module RouterWsdlTest
class Person < ActionService::Struct
member :id, Integer
member :names, [String]
member :lastname, String
member :deleted, TrueClass
end
class API < ActionService::API::Base
api_method :add, :expects => [{:a=>:int}, {:b=>:int}], :returns => [:int]
api_method :find_people, :returns => [[Person]]
api_method :nil_returner
end
class Service < ActionService::Base
service_api API
def add(a, b)
a + b
end
def find_people
[]
end
def nil_returner
end
end
class AbstractController < ActionController::Base
def generate_wsdl(container, uri, soap_action_base)
to_wsdl(container, uri, soap_action_base)
end
end
class DirectController < AbstractController
service_api API
def add
end
def find_people
end
def nil_returner
end
end
class DelegatedController < AbstractController
service_dispatching_mode :delegated
service(:test_service) { Service.new }
end
end
class TC_RouterWsdl < Test::Unit::TestCase
include RouterWsdlTest
def test_wsdl_generation
ensure_valid_generation DelegatedController.new
ensure_valid_generation DirectController.new
end
def
def test_wsdl_action
ensure_valid_wsdl_action DelegatedController.new
ensure_valid_wsdl_action DirectController.new
end
protected
def ensure_valid_generation(controller)
wsdl = controller.generate_wsdl(controller, 'http://localhost:3000/test/', '/test')
ensure_valid_wsdl(wsdl)
end
def ensure_valid_wsdl(wsdl)
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
end
def ensure_valid_wsdl_action(controller)
test_request = ActionController::TestRequest.new({ 'action' => 'wsdl' })
test_request.env['REQUEST_METHOD'] = 'GET'
test_request.env['HTTP_HOST'] = 'localhost:3000'
test_response = ActionController::TestResponse.new
wsdl = controller.process(test_request, test_response).body
ensure_valid_wsdl(wsdl)
end
end

View file

@ -0,0 +1,40 @@
require File.dirname(__FILE__) + '/abstract_unit'
module StructTest
class Struct < ActionService::Struct
member :id, Integer
member :name, String
member :items, [String]
member :deleted, :bool
member :emails, [:string]
end
end
class TC_Struct < Test::Unit::TestCase
def test_members
assert_equal(5, StructTest::Struct.members.size)
assert_equal(Integer, StructTest::Struct.members[:id])
assert_equal(String, StructTest::Struct.members[:name])
assert_equal([String], StructTest::Struct.members[:items])
assert_equal(TrueClass, StructTest::Struct.members[:deleted])
assert_equal([String], StructTest::Struct.members[:emails])
end
def test_initializer_and_lookup
s = StructTest::Struct.new(:id => 5,
:name => 'hello',
:items => ['one', 'two'],
:deleted => true,
:emails => ['test@test.com'])
assert_equal(5, s.id)
assert_equal('hello', s.name)
assert_equal(['one', 'two'], s.items)
assert_equal(true, s.deleted)
assert_equal(['test@test.com'], s.emails)
assert_equal(5, s['id'])
assert_equal('hello', s['name'])
assert_equal(['one', 'two'], s['items'])
assert_equal(true, s['deleted'])
assert_equal(['test@test.com'], s['emails'])
end
end