From e7a29380292902eae4799b2658507b3cfffb9cec Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Feb 2005 10:35:25 +0000 Subject: [PATCH] Added Action Service to the repository git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@658 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionservice/ChangeLog | 41 + actionservice/HACKING | 44 + actionservice/MIT-LICENSE | 21 + actionservice/README | 241 +++ actionservice/Rakefile | 135 ++ actionservice/TODO | 35 + actionservice/examples/googlesearch/README | 151 ++ .../autoloading/google_search_api.rb | 50 + .../autoloading/google_search_controller.rb | 57 + .../delegated/google_search_service.rb | 110 ++ .../delegated/search_controller.rb | 7 + .../googlesearch/direct/search_controller.rb | 109 ++ actionservice/examples/metaWeblog/README | 28 + .../examples/metaWeblog/blog_controller.rb | 121 ++ actionservice/lib/action_service.rb | 60 + actionservice/lib/action_service/api.rb | 2 + .../lib/action_service/api/abstract.rb | 198 +++ .../action_service/api/action_controller.rb | 97 ++ actionservice/lib/action_service/base.rb | 41 + actionservice/lib/action_service/client.rb | 3 + .../lib/action_service/client/base.rb | 35 + .../lib/action_service/client/soap.rb | 87 ++ .../lib/action_service/client/xmlrpc.rb | 76 + actionservice/lib/action_service/container.rb | 232 +++ .../lib/action_service/invocation.rb | 252 +++ actionservice/lib/action_service/protocol.rb | 4 + .../lib/action_service/protocol/abstract.rb | 128 ++ .../lib/action_service/protocol/registry.rb | 55 + .../lib/action_service/protocol/soap.rb | 484 ++++++ .../lib/action_service/protocol/xmlrpc.rb | 187 +++ actionservice/lib/action_service/router.rb | 2 + .../router/action_controller.rb | 97 ++ .../lib/action_service/router/wsdl.rb | 210 +++ actionservice/lib/action_service/struct.rb | 56 + .../support/class_inheritable_options.rb | 26 + .../lib/action_service/support/signature.rb | 100 ++ actionservice/setup.rb | 1360 +++++++++++++++++ actionservice/test/abstract_client.rb | 124 ++ actionservice/test/abstract_soap.rb | 58 + actionservice/test/abstract_unit.rb | 9 + actionservice/test/api_test.rb | 52 + actionservice/test/base_test.rb | 42 + actionservice/test/client_soap_test.rb | 87 ++ actionservice/test/client_xmlrpc_test.rb | 86 ++ actionservice/test/container_test.rb | 53 + actionservice/test/invocation_test.rb | 158 ++ actionservice/test/protocol_registry_test.rb | 53 + actionservice/test/protocol_soap_test.rb | 226 +++ actionservice/test/protocol_xmlrpc_test.rb | 157 ++ .../test/router_action_controller_test.rb | 139 ++ actionservice/test/router_wsdl_test.rb | 100 ++ actionservice/test/struct_test.rb | 40 + 52 files changed, 6326 insertions(+) create mode 100644 actionservice/ChangeLog create mode 100644 actionservice/HACKING create mode 100644 actionservice/MIT-LICENSE create mode 100644 actionservice/README create mode 100644 actionservice/Rakefile create mode 100644 actionservice/TODO create mode 100644 actionservice/examples/googlesearch/README create mode 100644 actionservice/examples/googlesearch/autoloading/google_search_api.rb create mode 100644 actionservice/examples/googlesearch/autoloading/google_search_controller.rb create mode 100644 actionservice/examples/googlesearch/delegated/google_search_service.rb create mode 100644 actionservice/examples/googlesearch/delegated/search_controller.rb create mode 100644 actionservice/examples/googlesearch/direct/search_controller.rb create mode 100644 actionservice/examples/metaWeblog/README create mode 100644 actionservice/examples/metaWeblog/blog_controller.rb create mode 100644 actionservice/lib/action_service.rb create mode 100644 actionservice/lib/action_service/api.rb create mode 100644 actionservice/lib/action_service/api/abstract.rb create mode 100644 actionservice/lib/action_service/api/action_controller.rb create mode 100644 actionservice/lib/action_service/base.rb create mode 100644 actionservice/lib/action_service/client.rb create mode 100644 actionservice/lib/action_service/client/base.rb create mode 100644 actionservice/lib/action_service/client/soap.rb create mode 100644 actionservice/lib/action_service/client/xmlrpc.rb create mode 100644 actionservice/lib/action_service/container.rb create mode 100644 actionservice/lib/action_service/invocation.rb create mode 100644 actionservice/lib/action_service/protocol.rb create mode 100644 actionservice/lib/action_service/protocol/abstract.rb create mode 100644 actionservice/lib/action_service/protocol/registry.rb create mode 100644 actionservice/lib/action_service/protocol/soap.rb create mode 100644 actionservice/lib/action_service/protocol/xmlrpc.rb create mode 100644 actionservice/lib/action_service/router.rb create mode 100644 actionservice/lib/action_service/router/action_controller.rb create mode 100644 actionservice/lib/action_service/router/wsdl.rb create mode 100644 actionservice/lib/action_service/struct.rb create mode 100644 actionservice/lib/action_service/support/class_inheritable_options.rb create mode 100644 actionservice/lib/action_service/support/signature.rb create mode 100644 actionservice/setup.rb create mode 100644 actionservice/test/abstract_client.rb create mode 100644 actionservice/test/abstract_soap.rb create mode 100644 actionservice/test/abstract_unit.rb create mode 100644 actionservice/test/api_test.rb create mode 100644 actionservice/test/base_test.rb create mode 100644 actionservice/test/client_soap_test.rb create mode 100644 actionservice/test/client_xmlrpc_test.rb create mode 100644 actionservice/test/container_test.rb create mode 100644 actionservice/test/invocation_test.rb create mode 100644 actionservice/test/protocol_registry_test.rb create mode 100644 actionservice/test/protocol_soap_test.rb create mode 100644 actionservice/test/protocol_xmlrpc_test.rb create mode 100644 actionservice/test/router_action_controller_test.rb create mode 100644 actionservice/test/router_wsdl_test.rb create mode 100644 actionservice/test/struct_test.rb diff --git a/actionservice/ChangeLog b/actionservice/ChangeLog new file mode 100644 index 0000000000..e226a0a14c --- /dev/null +++ b/actionservice/ChangeLog @@ -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 diff --git a/actionservice/HACKING b/actionservice/HACKING new file mode 100644 index 0000000000..9c0cde6313 --- /dev/null +++ b/actionservice/HACKING @@ -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. diff --git a/actionservice/MIT-LICENSE b/actionservice/MIT-LICENSE new file mode 100644 index 0000000000..528941e849 --- /dev/null +++ b/actionservice/MIT-LICENSE @@ -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. + diff --git a/actionservice/README b/actionservice/README new file mode 100644 index 0000000000..75d60abd46 --- /dev/null +++ b/actionservice/README @@ -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 +@params (keyed by name), and @method_params (ordered list). + +The return value of the action is sent back as the return value to the +caller. + +In this mode, a special api 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. This action must not be +overridden. + +==== 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 +/person/api 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 /api/person action. + +The /api/person action is generated when the +service+ +method is called. This action must not be overridden. + +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 + diff --git a/actionservice/Rakefile b/actionservice/Rakefile new file mode 100644 index 0000000000..9bdd25be2a --- /dev/null +++ b/actionservice/Rakefile @@ -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 diff --git a/actionservice/TODO b/actionservice/TODO new file mode 100644 index 0000000000..7a78bd2654 --- /dev/null +++ b/actionservice/TODO @@ -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 diff --git a/actionservice/examples/googlesearch/README b/actionservice/examples/googlesearch/README new file mode 100644 index 0000000000..20ecbc3a76 --- /dev/null +++ b/actionservice/examples/googlesearch/README @@ -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 :) diff --git a/actionservice/examples/googlesearch/autoloading/google_search_api.rb b/actionservice/examples/googlesearch/autoloading/google_search_api.rb new file mode 100644 index 0000000000..e7e33a1105 --- /dev/null +++ b/actionservice/examples/googlesearch/autoloading/google_search_api.rb @@ -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 diff --git a/actionservice/examples/googlesearch/autoloading/google_search_controller.rb b/actionservice/examples/googlesearch/autoloading/google_search_controller.rb new file mode 100644 index 0000000000..c62e869df5 --- /dev/null +++ b/actionservice/examples/googlesearch/autoloading/google_search_controller.rb @@ -0,0 +1,57 @@ +class GoogleSearchController < ApplicationController + wsdl_service_name 'GoogleSearch' + + def doGetCachedPage + "i am a cached page. my key was %s, url was %s" % [@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 diff --git a/actionservice/examples/googlesearch/delegated/google_search_service.rb b/actionservice/examples/googlesearch/delegated/google_search_service.rb new file mode 100644 index 0000000000..84faf220ae --- /dev/null +++ b/actionservice/examples/googlesearch/delegated/google_search_service.rb @@ -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) + "i am a cached page" + 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 diff --git a/actionservice/examples/googlesearch/delegated/search_controller.rb b/actionservice/examples/googlesearch/delegated/search_controller.rb new file mode 100644 index 0000000000..58c8321ef2 --- /dev/null +++ b/actionservice/examples/googlesearch/delegated/search_controller.rb @@ -0,0 +1,7 @@ +require 'google_search_service' + +class SearchController < ApplicationController + wsdl_service_name 'GoogleSearch' + service_dispatching_mode :delegated + service :beta3, GoogleSearchService.new +end diff --git a/actionservice/examples/googlesearch/direct/search_controller.rb b/actionservice/examples/googlesearch/direct/search_controller.rb new file mode 100644 index 0000000000..e4e1d7a0eb --- /dev/null +++ b/actionservice/examples/googlesearch/direct/search_controller.rb @@ -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 + "i am a cached page. my key was %s, url was %s" % [@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 diff --git a/actionservice/examples/metaWeblog/README b/actionservice/examples/metaWeblog/README new file mode 100644 index 0000000000..f8a56d7018 --- /dev/null +++ b/actionservice/examples/metaWeblog/README @@ -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. diff --git a/actionservice/examples/metaWeblog/blog_controller.rb b/actionservice/examples/metaWeblog/blog_controller.rb new file mode 100644 index 0000000000..b23ccb04c6 --- /dev/null +++ b/actionservice/examples/metaWeblog/blog_controller.rb @@ -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 diff --git a/actionservice/lib/action_service.rb b/actionservice/lib/action_service.rb new file mode 100644 index 0000000000..005e829e7b --- /dev/null +++ b/actionservice/lib/action_service.rb @@ -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 diff --git a/actionservice/lib/action_service/api.rb b/actionservice/lib/action_service/api.rb new file mode 100644 index 0000000000..61f36fff56 --- /dev/null +++ b/actionservice/lib/action_service/api.rb @@ -0,0 +1,2 @@ +require 'action_service/api/abstract' +require 'action_service/api/action_controller' diff --git a/actionservice/lib/action_service/api/abstract.rb b/actionservice/lib/action_service/api/abstract.rb new file mode 100644 index 0000000000..33ed603bfe --- /dev/null +++ b/actionservice/lib/action_service/api/abstract.rb @@ -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 + # definition_api.rb will be expected to exist in the load path, + # containing an API definition class named DefinitionAPI or + # DefinitionApi. + # + # 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 app/apis/google_search_api.rb, and expect the + # API definition class to be named GoogleSearchAPI or + # GoogleSearchApi. + # + # ==== 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: + # [:expects] Signature hint for the method input parameters + # [:returns] Signature hint for the method return value + # [:expects_and_returns] 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 diff --git a/actionservice/lib/action_service/api/action_controller.rb b/actionservice/lib/action_service/api/action_controller.rb new file mode 100644 index 0000000000..7ea0a0d3bd --- /dev/null +++ b/actionservice/lib/action_service/api/action_controller.rb @@ -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 blogger 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 :api + 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 diff --git a/actionservice/lib/action_service/base.rb b/actionservice/lib/action_service/base.rb new file mode 100644 index 0000000000..c3a1747106 --- /dev/null +++ b/actionservice/lib/action_service/base.rb @@ -0,0 +1,41 @@ +require 'action_service/support/class_inheritable_options' +require 'action_service/support/signature' + +module ActionService # :nodoc: + class ActionServiceError < StandardError # :nodoc: + end + + # An Action Service object implements a specified API. + # + # Used by controllers operating in _Delegated_ dispatching mode. + # + # ==== Example + # + # class PersonService < ActionService::Base + # 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 diff --git a/actionservice/lib/action_service/client.rb b/actionservice/lib/action_service/client.rb new file mode 100644 index 0000000000..ce91529f20 --- /dev/null +++ b/actionservice/lib/action_service/client.rb @@ -0,0 +1,3 @@ +require 'action_service/client/base' +require 'action_service/client/soap' +require 'action_service/client/xmlrpc' diff --git a/actionservice/lib/action_service/client/base.rb b/actionservice/lib/action_service/client/base.rb new file mode 100644 index 0000000000..955887a4d8 --- /dev/null +++ b/actionservice/lib/action_service/client/base.rb @@ -0,0 +1,35 @@ +module ActionService # :nodoc: + module Client # :nodoc: + class ClientError < StandardError # :nodoc: + end + + class Base # :nodoc: + def initialize(api, endpoint_uri) + @api = api + @endpoint_uri = endpoint_uri + end + + def method_missing(name, *args) # :nodoc: + call_name = method_name(name) + return super(name, *args) if call_name.nil? + perform_invocation(call_name, args) + end + + protected + def perform_invocation(method_name, args) # :nodoc: + raise NotImplementedError, "use a protocol-specific client" + end + + private + def method_name(name) + if @api.has_api_method?(name.to_sym) + name.to_s + elsif @api.has_public_api_method?(name.to_s) + @api.api_method_name(name.to_s).to_s + else + nil + end + end + end + end +end diff --git a/actionservice/lib/action_service/client/soap.rb b/actionservice/lib/action_service/client/soap.rb new file mode 100644 index 0000000000..c617f36589 --- /dev/null +++ b/actionservice/lib/action_service/client/soap.rb @@ -0,0 +1,87 @@ +require 'soap/rpc/driver' +require 'uri' + +module ActionService # :nodoc: + module Client # :nodoc: + + # Implements SOAP client support (using RPC encoding for the messages). + # + # ==== Example Usage + # + # class PersonAPI < ActionService::API::Base + # api_method :find_all, :returns => [[Person]] + # end + # + # soap_client = ActionService::Client::Soap.new(PersonAPI, "http://...") + # persons = soap_client.find_all + # + class Soap < Base + + # Creates a new web service client using the SOAP RPC protocol. + # + # +api+ must be an ActionService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:service_name] If the remote server has used a custom +wsdl_service_name+ + # option, you must specify it here + def initialize(api, endpoint_uri, options={}) + super(api, endpoint_uri) + @service_name = options[:service_name] || 'ActionService' + @namespace = "urn:#{@service_name}" + @mapper = ActionService::Protocol::Soap::SoapMapper.new(@namespace) + @protocol = ActionService::Protocol::Soap::SoapProtocol.new(@mapper) + @soap_action_base = options[:soap_action_base] + @soap_action_base ||= URI.parse(endpoint_uri).path + @driver = create_soap_rpc_driver(api, endpoint_uri) + end + + protected + def perform_invocation(method_name, args) + @driver.send(method_name, *args) + end + + def soap_action(method_name) + "#{@soap_action_base}/#{method_name}" + end + + private + def create_soap_rpc_driver(api, endpoint_uri) + @mapper.map_api(api) + driver = SoapDriver.new(endpoint_uri, nil) + driver.mapping_registry = @mapper.registry + api.api_methods.each do |name, info| + public_name = api.public_api_method_name(name) + qname = XSD::QName.new(@namespace, public_name) + action = soap_action(public_name) + expects = info[:expects] + returns = info[:returns] + param_def = [] + i = 1 + if expects + expects.each do |klass| + param_name = klass.is_a?(Hash) ? klass.keys[0] : "param#{i}" + mapping = @mapper.lookup(klass) + param_def << ['in', param_name, mapping.registry_mapping] + i += 1 + end + end + if returns + mapping = @mapper.lookup(returns[0]) + param_def << ['retval', 'return', mapping.registry_mapping] + end + driver.add_method(qname, action, name.to_s, param_def) + end + driver + end + + class SoapDriver < SOAP::RPC::Driver # :nodoc: + def add_method(qname, soapaction, name, param_def) + @proxy.add_rpc_method(qname, soapaction, name, param_def) + add_rpc_method_interface(name, param_def) + end + end + end + end +end diff --git a/actionservice/lib/action_service/client/xmlrpc.rb b/actionservice/lib/action_service/client/xmlrpc.rb new file mode 100644 index 0000000000..d0d007f871 --- /dev/null +++ b/actionservice/lib/action_service/client/xmlrpc.rb @@ -0,0 +1,76 @@ +require 'uri' +require 'xmlrpc/client' + +module ActionService # :nodoc: + module Client # :nodoc: + + # Implements XML-RPC client support + # + # ==== Example Usage + # + # class BloggerAPI < ActionService::API::Base + # inflect_names false + # api_method :getRecentPosts, :returns => [[Blog::Post]] + # end + # + # blog = ActionService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger") + # posts = blog.getRecentPosts + class XmlRpc < Base + + # Creates a new web service client using the XML-RPC protocol. + # + # +api+ must be an ActionService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:handler_name] If the remote server defines its services inside special + # handler (the Blogger API uses a "blogger" handler name for example), + # provide it here, or your method calls will fail + def initialize(api, endpoint_uri, options={}) + @api = api + @handler_name = options[:handler_name] + @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout]) + end + + protected + def perform_invocation(method_name, args) + args = transform_outgoing_method_params(method_name, args) + ok, return_value = @client.call2(public_name(method_name), *args) + return transform_return_value(method_name, return_value) if ok + raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}") + end + + def transform_outgoing_method_params(method_name, params) + info = @api.api_methods[method_name.to_sym] + signature = info[:expects] + signature_length = signature.nil?? 0 : signature.length + if signature_length != params.length + raise(ProtocolError, "API declares #{public_name(method_name)} to accept " + + "#{signature_length} parameters, but #{params.length} parameters " + + "were supplied") + end + if signature_length > 0 + signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature) + (1..signature.size).each do |i| + i -= 1 + params[i] = Protocol::XmlRpc::XmlRpcProtocol.ruby_to_xmlrpc(params[i], signature[i]) + end + end + params + end + + def transform_return_value(method_name, return_value) + info = @api.api_methods[method_name.to_sym] + return true unless signature = info[:returns] + signature = Protocol::XmlRpc::XmlRpcProtocol.transform_array_types(signature) + Protocol::XmlRpc::XmlRpcProtocol.xmlrpc_to_ruby(return_value, signature[0]) + end + + def public_name(method_name) + public_name = @api.public_api_method_name(method_name) + @handler_name ? "#{@handler_name}.#{public_name}" : public_name + end + end + end +end diff --git a/actionservice/lib/action_service/container.rb b/actionservice/lib/action_service/container.rb new file mode 100644 index 0000000000..b2317fc941 --- /dev/null +++ b/actionservice/lib/action_service/container.rb @@ -0,0 +1,232 @@ +module ActionService # :nodoc: + module Container # :nodoc: + class ContainerError < ActionService::ActionServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.class_inheritable_option(: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 diff --git a/actionservice/lib/action_service/invocation.rb b/actionservice/lib/action_service/invocation.rb new file mode 100644 index 0000000000..f20b546c5a --- /dev/null +++ b/actionservice/lib/action_service/invocation.rb @@ -0,0 +1,252 @@ +module ActionService # :nodoc: + module Invocation # :nodoc: + ConcreteInvocation = :concrete + VirtualInvocation = :virtual + UnpublishedConcreteInvocation = :unpublished_concrete + + class InvocationError < ActionService::ActionServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.extend(ClassMethods) + base.send(:include, ActionService::Invocation::InstanceMethods) + end + + # Invocation interceptors provide a means to execute custom code before + # and after method invocations on ActionService::Base objects. + # + # When running in _Direct_ dispatching mode, ActionController filters + # should be used for this functionality. + # + # 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 [false, "cancel reason"] 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: + # [:except] A list of methods for which the interceptor will NOT be called + # [:only] 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 diff --git a/actionservice/lib/action_service/protocol.rb b/actionservice/lib/action_service/protocol.rb new file mode 100644 index 0000000000..5e71b2bcfd --- /dev/null +++ b/actionservice/lib/action_service/protocol.rb @@ -0,0 +1,4 @@ +require 'action_service/protocol/abstract' +require 'action_service/protocol/registry' +require 'action_service/protocol/soap' +require 'action_service/protocol/xmlrpc' diff --git a/actionservice/lib/action_service/protocol/abstract.rb b/actionservice/lib/action_service/protocol/abstract.rb new file mode 100644 index 0000000000..ed41c49951 --- /dev/null +++ b/actionservice/lib/action_service/protocol/abstract.rb @@ -0,0 +1,128 @@ +module ActionService # :nodoc: + module Protocol # :nodoc: + CheckedMessage = :checked + UncheckedMessage = :unchecked + + class ProtocolError < ActionService::ActionServiceError # :nodoc: + end + + class AbstractProtocol # :nodoc: + attr :container_class + + def initialize(container_class) + @container_class = container_class + end + + def unmarshal_request(protocol_request) + raise NotImplementedError + end + + def marshal_response(protocol_request, return_value) + raise NotImplementedError + end + + def marshal_exception(exception) + raise NotImplementedError + end + + def self.create_protocol_request(container_class, action_pack_request) + nil + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + nil + end + end + + class AbstractProtocolMessage # :nodoc: + attr_accessor :signature + attr_accessor :return_signature + attr_accessor :type + attr :options + + def initialize(options={}) + @signature = @return_signature = nil + @options = options + @type = @options[:type] || CheckedMessage + end + + def signature=(value) + return if value.nil? + @signature = [] + value.each do |klass| + if klass.is_a?(Hash) + @signature << klass.values.shift + else + @signature << klass + end + end + @signature + end + + def checked? + @type == CheckedMessage + end + + def check_parameter_types(values, signature) + return unless checked? && signature + unless signature.length == values.length + raise(ProtocolError, "Signature and parameter lengths mismatch") + end + (1..signature.length).each do |i| + check_compatibility(signature[i-1], values[i-1].class) + end + end + + def check_compatibility(expected_class, received_class) + return if \ + (expected_class == TrueClass or expected_class == FalseClass) and \ + (received_class == TrueClass or received_class == FalseClass) + unless received_class.ancestors.include?(expected_class) or \ + expected_class.ancestors.include?(received_class) + raise(ProtocolError, "value of type #{received_class.name} is not " + + "compatible with expected type #{expected_class.name}") + end + end + end + + class ProtocolRequest < AbstractProtocolMessage # :nodoc: + attr :protocol + attr :raw_body + + attr_accessor :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 diff --git a/actionservice/lib/action_service/protocol/registry.rb b/actionservice/lib/action_service/protocol/registry.rb new file mode 100644 index 0000000000..e06361f916 --- /dev/null +++ b/actionservice/lib/action_service/protocol/registry.rb @@ -0,0 +1,55 @@ +module ActionService # :nodoc: + module Protocol # :nodoc: + HeaderAndBody = :header_and_body + BodyOnly = :body_only + + module Registry # :nodoc: + def self.append_features(base) # :nodoc: + super + base.extend(ClassMethods) + base.send(:include, ActionService::Protocol::Registry::InstanceMethods) + end + + module ClassMethods # :nodoc: + def register_protocol(type, klass) # :nodoc: + case type + when HeaderAndBody + write_inheritable_array("header_and_body_protocols", [klass]) + when BodyOnly + write_inheritable_array("body_only_protocols", [klass]) + else + raise(ProtocolError, "unknown protocol type #{type}") + end + end + end + + module InstanceMethods # :nodoc: + private + def probe_request_protocol(action_pack_request) + (header_and_body_protocols + body_only_protocols).each do |protocol| + protocol_request = protocol.create_protocol_request(self.class, action_pack_request) + return protocol_request if protocol_request + end + raise(ProtocolError, "unsupported request message format") + end + + def probe_protocol_client(api, protocol_name, endpoint_uri, options) + (header_and_body_protocols + body_only_protocols).each do |protocol| + protocol_client = protocol.create_protocol_client(api, protocol_name, endpoint_uri, options) + return protocol_client if protocol_client + end + raise(ProtocolError, "unsupported client protocol :#{protocol_name}") + end + + def header_and_body_protocols + self.class.read_inheritable_attribute("header_and_body_protocols") || [] + end + + def body_only_protocols + self.class.read_inheritable_attribute("body_only_protocols") || [] + end + end + + end + end +end diff --git a/actionservice/lib/action_service/protocol/soap.rb b/actionservice/lib/action_service/protocol/soap.rb new file mode 100644 index 0000000000..24cc554b05 --- /dev/null +++ b/actionservice/lib/action_service/protocol/soap.rb @@ -0,0 +1,484 @@ +require 'soap/processor' +require 'soap/mapping' +require 'soap/rpc/element' +require 'xsd/datatypes' +require 'xsd/ns' +require 'singleton' + +module ActionService # :nodoc: + module Protocol # :nodoc: + module Soap # :nodoc: + class ProtocolError < ActionService::ActionServiceError # :nodoc: + end + + def self.append_features(base) # :nodoc: + super + base.register_protocol(HeaderAndBody, SoapProtocol) + base.extend(ClassMethods) + base.wsdl_service_name('ActionService') + end + + module ClassMethods + # Specifies the WSDL service name to use when generating WSDL. Highly + # recommended that you set this value, or code generators may generate + # classes with very generic names. + # + # === Example + # class MyController < ActionController::Base + # wsdl_service_name 'MyService' + # end + def wsdl_service_name(name) + write_inheritable_attribute("soap_mapper", SoapMapper.new("urn:#{name}")) + end + + def soap_mapper # :nodoc: + read_inheritable_attribute("soap_mapper") + end + end + + class SoapProtocol < AbstractProtocol # :nodoc: + attr :mapper + + def initialize(mapper) + @mapper = mapper + end + + def self.create_protocol_request(container_class, action_pack_request) + soap_action = extract_soap_action(action_pack_request) + return nil unless soap_action + service_name = action_pack_request.parameters['action'] + public_method_name = soap_action.gsub(/^[\/]+/, '').split(/[\/]+/)[-1] + content_type = action_pack_request.env['HTTP_CONTENT_TYPE'] + content_type ||= 'text/xml' + protocol = SoapProtocol.new(container_class.soap_mapper) + ProtocolRequest.new(protocol, + action_pack_request.raw_post, + service_name.to_sym, + public_method_name, + content_type) + end + + def self.create_protocol_client(api, protocol_name, endpoint_uri, options) + return nil unless protocol_name.to_s.downcase.to_sym == :soap + ActionService::Client::Soap.new(api, endpoint_uri, options) + end + + def unmarshal_request(protocol_request) + unmarshal = lambda do + envelope = SOAP::Processor.unmarshal(protocol_request.raw_body) + request = envelope.body.request + values = request.collect{|k, v| request[k]} + soap_to_ruby_array(values) + end + signature = protocol_request.signature + if signature + map_signature_types(signature) + values = unmarshal.call + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types(values, signature) + values + else + if protocol_request.checked? + [] + else + unmarshal.call + end + end + end + + def marshal_response(protocol_request, return_value) + marshal = lambda do |signature| + mapping = mapper.lookup(signature[0]) + return_value = fixup_array_types(mapping, return_value) + signature = signature.map{|x|mapper.lookup(x).ruby_klass} + protocol_request.check_parameter_types([return_value], signature) + param_def = [['retval', 'return', mapping.registry_mapping]] + [param_def, ruby_to_soap(return_value)] + end + signature = protocol_request.return_signature + param_def = nil + if signature + param_def, return_value = marshal.call(signature) + else + if protocol_request.checked? + param_def, return_value = nil, nil + else + param_def, return_value = marshal.call([return_value.class]) + end + end + qname = XSD::QName.new(mapper.custom_namespace, + protocol_request.public_method_name) + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = return_value unless return_value.nil? + ProtocolResponse.new(self, create_response(response), 'text/xml') + end + + def marshal_exception(exc) + ProtocolResponse.new(self, create_exception_response(exc), 'text/xml') + end + + private + def self.extract_soap_action(request) + return nil unless request.method == :post + content_type = request.env['HTTP_CONTENT_TYPE'] || 'text/xml' + return nil unless content_type + soap_action = request.env['HTTP_SOAPACTION'] + return nil unless soap_action + soap_action.gsub!(/^"/, '') + soap_action.gsub!(/"$/, '') + soap_action.strip! + return nil if soap_action.empty? + soap_action + end + + def fixup_array_types(mapping, obj) + mapping.each_attribute do |name, type, attr_mapping| + if attr_mapping.custom_type? + attr_obj = obj.send(name) + new_obj = fixup_array_types(attr_mapping, attr_obj) + obj.send("#{name}=", new_obj) unless new_obj.equal?(attr_obj) + end + end + if mapping.is_a?(SoapArrayMapping) + obj = mapping.ruby_klass.new(obj) + # man, this is going to be slow for big arrays :( + (1..obj.size).each do |i| + i -= 1 + obj[i] = fixup_array_types(mapping.element_mapping, obj[i]) + end + else + if !mapping.generated_klass.nil? && mapping.generated_klass.respond_to?(:members) + # have to map the publically visible structure of the class + new_obj = mapping.generated_klass.new + mapping.generated_klass.members.each do |name, klass| + new_obj.send("#{name}=", obj.send(name)) + end + obj = new_obj + end + end + obj + end + + def map_signature_types(types) + types.collect{|type| mapper.map(type)} + end + + def create_response(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + envelope = SOAP::SOAPEnvelope.new(header, body) + SOAP::Processor.marshal(envelope) + end + + def create_exception_response(exc) + detail = SOAP::Mapping::SOAPException.new(exc) + body = SOAP::SOAPFault.new( + SOAP::SOAPString.new('Server'), + SOAP::SOAPString.new(exc.to_s), + SOAP::SOAPString.new(self.class.name), + SOAP::Mapping.obj2soap(detail)) + create_response(body) + end + + def ruby_to_soap(obj) + SOAP::Mapping.obj2soap(obj, mapper.registry) + end + + def soap_to_ruby(obj) + SOAP::Mapping.soap2obj(obj, mapper.registry) + end + + def soap_to_ruby_array(array) + array.map{|x| soap_to_ruby(x)} + end + end + + class SoapMapper # :nodoc: + attr :registry + attr :custom_namespace + attr :custom_types + + def initialize(custom_namespace) + @custom_namespace = custom_namespace + @registry = SOAP::Mapping::Registry.new + @klass2map = {} + @custom_types = {} + @ar2klass = {} + end + + def lookup(klass) + lookup_klass = klass.is_a?(Array) ? klass[0] : klass + generated_klass = nil + unless lookup_klass.respond_to?(:ancestors) + raise(ProtocolError, "expected parameter type definition to be a Class") + end + if lookup_klass.ancestors.include?(ActiveRecord::Base) + generated_klass = @ar2klass.has_key?(klass) ? @ar2klass[klass] : nil + klass = generated_klass if generated_klass + end + return @klass2map[klass] if @klass2map.has_key?(klass) + + custom_type = false + + ruby_klass = select_class(lookup_klass) + generated_klass = @ar2klass[lookup_klass] if @ar2klass.has_key?(lookup_klass) + type_name = ruby_klass.name + + # Array signatures generate a double-mapping and require generation + # of an Array subclass to represent the mapping in the SOAP + # registry + array_klass = nil + if klass.is_a?(Array) + array_klass = Class.new(Array) do + module_eval <<-END + def self.name + "#{type_name}Array" + end + END + end + end + + mapping = @registry.find_mapped_soap_class(ruby_klass) rescue nil + unless mapping + # Custom structured type, generate a mapping + info = { :type => XSD::QName.new(@custom_namespace, type_name) } + @registry.add(ruby_klass, + SOAP::SOAPStruct, + SOAP::Mapping::Registry::TypedStructFactory, + info) + mapping = ensure_mapped(ruby_klass) + custom_type = true + end + + array_mapping = nil + if array_klass + # Typed array always requires a custom type. The info of the array + # is the info of its element type (in mapping[2]), falling back + # to SOAP base types. + info = mapping[2] + info ||= {} + info[:type] ||= soap_base_type_qname(mapping[0]) + @registry.add(array_klass, + SOAP::SOAPArray, + SOAP::Mapping::Registry::TypedArrayFactory, + info) + array_mapping = ensure_mapped(array_klass) + end + + if array_mapping + @klass2map[ruby_klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @klass2map[klass] = SoapArrayMapping.new(self, + type_name, + array_klass, + array_mapping[0], + array_mapping, + @klass2map[ruby_klass]) + @custom_types[klass] = @klass2map[klass] + @custom_types[ruby_klass] = @klass2map[ruby_klass] if custom_type + else + @klass2map[klass] = SoapMapping.new(self, + type_name, + ruby_klass, + generated_klass, + mapping[0], + mapping, + custom_type) + @custom_types[klass] = @klass2map[klass] if custom_type + end + + @klass2map[klass] + end + alias :map :lookup + + def map_container_services(container, &block) + dispatching_mode = container.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 diff --git a/actionservice/lib/action_service/protocol/xmlrpc.rb b/actionservice/lib/action_service/protocol/xmlrpc.rb new file mode 100644 index 0000000000..7d29868b59 --- /dev/null +++ b/actionservice/lib/action_service/protocol/xmlrpc.rb @@ -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 diff --git a/actionservice/lib/action_service/router.rb b/actionservice/lib/action_service/router.rb new file mode 100644 index 0000000000..16f0ae4463 --- /dev/null +++ b/actionservice/lib/action_service/router.rb @@ -0,0 +1,2 @@ +require 'action_service/router/action_controller' +require 'action_service/router/wsdl' diff --git a/actionservice/lib/action_service/router/action_controller.rb b/actionservice/lib/action_service/router/action_controller.rb new file mode 100644 index 0000000000..01bd298bce --- /dev/null +++ b/actionservice/lib/action_service/router/action_controller.rb @@ -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 diff --git a/actionservice/lib/action_service/router/wsdl.rb b/actionservice/lib/action_service/router/wsdl.rb new file mode 100644 index 0000000000..ececa63322 --- /dev/null +++ b/actionservice/lib/action_service/router/wsdl.rb @@ -0,0 +1,210 @@ +module ActionService # :nodoc: + module Router # :nodoc: + module Wsdl # :nodoc: + def self.append_features(base) # :nodoc: + base.class_eval do + class << self + alias_method :inherited_without_wsdl, :inherited + end + end + base.extend(ClassMethods) + end + + module ClassMethods + def inherited(child) + inherited_without_wsdl(child) + child.send(:include, ActionService::Router::Wsdl::InstanceMethods) + end + end + + module InstanceMethods # :nodoc: + XsdNs = 'http://www.w3.org/2001/XMLSchema' + WsdlNs = 'http://schemas.xmlsoap.org/wsdl/' + SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/' + SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/' + SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http' + + def wsdl + case @request.method + when :get + begin + host_name = @request.env['HTTP_HOST']||@request.env['SERVER_NAME'] + uri = "http://#{host_name}/#{controller_name}/" + soap_action_base = "/#{controller_name}" + xml = to_wsdl(self, uri, soap_action_base) + send_data(xml, :type => 'text/xml', :disposition => 'inline') + rescue Exception => e + log_error e unless logger.nil? + render_text('', "500 #{e.message}") + end + when :post + render_text('', "500 POST not supported") + end + end + + private + def to_wsdl(container, uri, soap_action_base) + wsdl = "" + + 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 diff --git a/actionservice/lib/action_service/struct.rb b/actionservice/lib/action_service/struct.rb new file mode 100644 index 0000000000..8f2718883e --- /dev/null +++ b/actionservice/lib/action_service/struct.rb @@ -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 diff --git a/actionservice/lib/action_service/support/class_inheritable_options.rb b/actionservice/lib/action_service/support/class_inheritable_options.rb new file mode 100644 index 0000000000..4d1c2ed471 --- /dev/null +++ b/actionservice/lib/action_service/support/class_inheritable_options.rb @@ -0,0 +1,26 @@ +class Class # :nodoc: + def class_inheritable_option(sym, default_value=nil) + write_inheritable_attribute sym, default_value + class_eval <<-EOS + def self.#{sym}(value=nil) + if !value.nil? + write_inheritable_attribute(:#{sym}, value) + else + read_inheritable_attribute(:#{sym}) + end + end + + def self.#{sym}=(value) + write_inheritable_attribute(:#{sym}, value) + end + + def #{sym} + self.class.#{sym} + end + + def #{sym}=(value) + self.class.#{sym} = value + end + EOS + end +end diff --git a/actionservice/lib/action_service/support/signature.rb b/actionservice/lib/action_service/support/signature.rb new file mode 100644 index 0000000000..f7aae61a3f --- /dev/null +++ b/actionservice/lib/action_service/support/signature.rb @@ -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: + # + # [:int] any integer value + # [:float] any floating point value + # [:string] any string value + # [:bool] any boolean value + # [:time] any value containing both date and time + # [:date] 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 diff --git a/actionservice/setup.rb b/actionservice/setup.rb new file mode 100644 index 0000000000..0807023db2 --- /dev/null +++ b/actionservice/setup.rb @@ -0,0 +1,1360 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted windows stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end +end + +class BoolItem < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end +end + +class ProgramItem < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end +end + +class PackageSelectionItem < ConfigItem + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end +end + +class ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + end + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +major = c['MAJOR'].to_i +minor = c['MINOR'].to_i +teeny = c['TEENY'].to_i +version = "#{major}.#{minor}" + +# ruby ver. >= 1.4.4? +newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + +if c['rubylibdir'] + # V < 1.6.3 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', libdir, + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } +end +multipackage_conf = [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + +end + + +# +# File Operations +# + +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(dest) + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| f.write File.binread(src) } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file? srcfile(path) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.3.1' + Copyright = 'Copyright (c) 2000-2004 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task =~ arg + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + setup_rb_error "unknown global option '#{arg}'" + end + end + + nil + end + + + def parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + ConfigTable.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '$prefix' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_packages(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/actionservice/test/abstract_client.rb b/actionservice/test/abstract_client.rb new file mode 100644 index 0000000000..662bdd9431 --- /dev/null +++ b/actionservice/test/abstract_client.rb @@ -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 diff --git a/actionservice/test/abstract_soap.rb b/actionservice/test/abstract_soap.rb new file mode 100644 index 0000000000..7454be9bdf --- /dev/null +++ b/actionservice/test/abstract_soap.rb @@ -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 diff --git a/actionservice/test/abstract_unit.rb b/actionservice/test/abstract_unit.rb new file mode 100644 index 0000000000..54ca73b35c --- /dev/null +++ b/actionservice/test/abstract_unit.rb @@ -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 diff --git a/actionservice/test/api_test.rb b/actionservice/test/api_test.rb new file mode 100644 index 0000000000..2ef5cc7bda --- /dev/null +++ b/actionservice/test/api_test.rb @@ -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 diff --git a/actionservice/test/base_test.rb b/actionservice/test/base_test.rb new file mode 100644 index 0000000000..21456f6c5a --- /dev/null +++ b/actionservice/test/base_test.rb @@ -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 diff --git a/actionservice/test/client_soap_test.rb b/actionservice/test/client_soap_test.rb new file mode 100644 index 0000000000..e802812cc1 --- /dev/null +++ b/actionservice/test/client_soap_test.rb @@ -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 diff --git a/actionservice/test/client_xmlrpc_test.rb b/actionservice/test/client_xmlrpc_test.rb new file mode 100644 index 0000000000..9c6abc3520 --- /dev/null +++ b/actionservice/test/client_xmlrpc_test.rb @@ -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 diff --git a/actionservice/test/container_test.rb b/actionservice/test/container_test.rb new file mode 100644 index 0000000000..b222138087 --- /dev/null +++ b/actionservice/test/container_test.rb @@ -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 diff --git a/actionservice/test/invocation_test.rb b/actionservice/test/invocation_test.rb new file mode 100644 index 0000000000..d8ecdc4e0b --- /dev/null +++ b/actionservice/test/invocation_test.rb @@ -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 diff --git a/actionservice/test/protocol_registry_test.rb b/actionservice/test/protocol_registry_test.rb new file mode 100644 index 0000000000..8e2b9659a6 --- /dev/null +++ b/actionservice/test/protocol_registry_test.rb @@ -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 diff --git a/actionservice/test/protocol_soap_test.rb b/actionservice/test/protocol_soap_test.rb new file mode 100644 index 0000000000..eaccd64e4e --- /dev/null +++ b/actionservice/test/protocol_soap_test.rb @@ -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 diff --git a/actionservice/test/protocol_xmlrpc_test.rb b/actionservice/test/protocol_xmlrpc_test.rb new file mode 100644 index 0000000000..a9cafc0a73 --- /dev/null +++ b/actionservice/test/protocol_xmlrpc_test.rb @@ -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 diff --git a/actionservice/test/router_action_controller_test.rb b/actionservice/test/router_action_controller_test.rb new file mode 100644 index 0000000000..b541b46542 --- /dev/null +++ b/actionservice/test/router_action_controller_test.rb @@ -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 diff --git a/actionservice/test/router_wsdl_test.rb b/actionservice/test/router_wsdl_test.rb new file mode 100644 index 0000000000..b141d8304b --- /dev/null +++ b/actionservice/test/router_wsdl_test.rb @@ -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 diff --git a/actionservice/test/struct_test.rb b/actionservice/test/struct_test.rb new file mode 100644 index 0000000000..b883c6d991 --- /dev/null +++ b/actionservice/test/struct_test.rb @@ -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