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:
parent
e39bf10594
commit
e7a2938029
52 changed files with 6326 additions and 0 deletions
41
actionservice/ChangeLog
Normal file
41
actionservice/ChangeLog
Normal 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
44
actionservice/HACKING
Normal 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
21
actionservice/MIT-LICENSE
Normal 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
241
actionservice/README
Normal 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
135
actionservice/Rakefile
Normal 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
35
actionservice/TODO
Normal 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
|
151
actionservice/examples/googlesearch/README
Normal file
151
actionservice/examples/googlesearch/README
Normal 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 :)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
require 'google_search_service'
|
||||
|
||||
class SearchController < ApplicationController
|
||||
wsdl_service_name 'GoogleSearch'
|
||||
service_dispatching_mode :delegated
|
||||
service :beta3, GoogleSearchService.new
|
||||
end
|
109
actionservice/examples/googlesearch/direct/search_controller.rb
Normal file
109
actionservice/examples/googlesearch/direct/search_controller.rb
Normal 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
|
28
actionservice/examples/metaWeblog/README
Normal file
28
actionservice/examples/metaWeblog/README
Normal 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.
|
121
actionservice/examples/metaWeblog/blog_controller.rb
Normal file
121
actionservice/examples/metaWeblog/blog_controller.rb
Normal 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
|
60
actionservice/lib/action_service.rb
Normal file
60
actionservice/lib/action_service.rb
Normal 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
|
2
actionservice/lib/action_service/api.rb
Normal file
2
actionservice/lib/action_service/api.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
require 'action_service/api/abstract'
|
||||
require 'action_service/api/action_controller'
|
198
actionservice/lib/action_service/api/abstract.rb
Normal file
198
actionservice/lib/action_service/api/abstract.rb
Normal 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
|
97
actionservice/lib/action_service/api/action_controller.rb
Normal file
97
actionservice/lib/action_service/api/action_controller.rb
Normal 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
|
41
actionservice/lib/action_service/base.rb
Normal file
41
actionservice/lib/action_service/base.rb
Normal 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
|
3
actionservice/lib/action_service/client.rb
Normal file
3
actionservice/lib/action_service/client.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
require 'action_service/client/base'
|
||||
require 'action_service/client/soap'
|
||||
require 'action_service/client/xmlrpc'
|
35
actionservice/lib/action_service/client/base.rb
Normal file
35
actionservice/lib/action_service/client/base.rb
Normal 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
|
87
actionservice/lib/action_service/client/soap.rb
Normal file
87
actionservice/lib/action_service/client/soap.rb
Normal 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
|
76
actionservice/lib/action_service/client/xmlrpc.rb
Normal file
76
actionservice/lib/action_service/client/xmlrpc.rb
Normal 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
|
232
actionservice/lib/action_service/container.rb
Normal file
232
actionservice/lib/action_service/container.rb
Normal 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
|
252
actionservice/lib/action_service/invocation.rb
Normal file
252
actionservice/lib/action_service/invocation.rb
Normal 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
|
4
actionservice/lib/action_service/protocol.rb
Normal file
4
actionservice/lib/action_service/protocol.rb
Normal 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'
|
128
actionservice/lib/action_service/protocol/abstract.rb
Normal file
128
actionservice/lib/action_service/protocol/abstract.rb
Normal 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
|
55
actionservice/lib/action_service/protocol/registry.rb
Normal file
55
actionservice/lib/action_service/protocol/registry.rb
Normal 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
|
484
actionservice/lib/action_service/protocol/soap.rb
Normal file
484
actionservice/lib/action_service/protocol/soap.rb
Normal 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
|
187
actionservice/lib/action_service/protocol/xmlrpc.rb
Normal file
187
actionservice/lib/action_service/protocol/xmlrpc.rb
Normal 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
|
2
actionservice/lib/action_service/router.rb
Normal file
2
actionservice/lib/action_service/router.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
require 'action_service/router/action_controller'
|
||||
require 'action_service/router/wsdl'
|
97
actionservice/lib/action_service/router/action_controller.rb
Normal file
97
actionservice/lib/action_service/router/action_controller.rb
Normal 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
|
210
actionservice/lib/action_service/router/wsdl.rb
Normal file
210
actionservice/lib/action_service/router/wsdl.rb
Normal 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
|
56
actionservice/lib/action_service/struct.rb
Normal file
56
actionservice/lib/action_service/struct.rb
Normal 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
|
|
@ -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
|
100
actionservice/lib/action_service/support/signature.rb
Normal file
100
actionservice/lib/action_service/support/signature.rb
Normal 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
1360
actionservice/setup.rb
Normal file
File diff suppressed because it is too large
Load diff
124
actionservice/test/abstract_client.rb
Normal file
124
actionservice/test/abstract_client.rb
Normal 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
|
58
actionservice/test/abstract_soap.rb
Normal file
58
actionservice/test/abstract_soap.rb
Normal 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
|
9
actionservice/test/abstract_unit.rb
Normal file
9
actionservice/test/abstract_unit.rb
Normal 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
|
52
actionservice/test/api_test.rb
Normal file
52
actionservice/test/api_test.rb
Normal 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
|
42
actionservice/test/base_test.rb
Normal file
42
actionservice/test/base_test.rb
Normal 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
|
87
actionservice/test/client_soap_test.rb
Normal file
87
actionservice/test/client_soap_test.rb
Normal 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
|
86
actionservice/test/client_xmlrpc_test.rb
Normal file
86
actionservice/test/client_xmlrpc_test.rb
Normal 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
|
53
actionservice/test/container_test.rb
Normal file
53
actionservice/test/container_test.rb
Normal 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
|
158
actionservice/test/invocation_test.rb
Normal file
158
actionservice/test/invocation_test.rb
Normal 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
|
53
actionservice/test/protocol_registry_test.rb
Normal file
53
actionservice/test/protocol_registry_test.rb
Normal 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
|
226
actionservice/test/protocol_soap_test.rb
Normal file
226
actionservice/test/protocol_soap_test.rb
Normal 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
|
157
actionservice/test/protocol_xmlrpc_test.rb
Normal file
157
actionservice/test/protocol_xmlrpc_test.rb
Normal 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
|
139
actionservice/test/router_action_controller_test.rb
Normal file
139
actionservice/test/router_action_controller_test.rb
Normal 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
|
100
actionservice/test/router_wsdl_test.rb
Normal file
100
actionservice/test/router_wsdl_test.rb
Normal 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
|
40
actionservice/test/struct_test.rb
Normal file
40
actionservice/test/struct_test.rb
Normal 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
|
Loading…
Reference in a new issue