├── CHANGELOG ├── MIT-LICENSE ├── README ├── Rakefile ├── TODO ├── actionwebservice.gemspec ├── examples ├── googlesearch │ ├── README │ ├── autoloading │ │ ├── google_search_api.rb │ │ └── google_search_controller.rb │ ├── delegated │ │ ├── google_search_service.rb │ │ └── search_controller.rb │ └── direct │ │ ├── google_search_api.rb │ │ └── search_controller.rb └── metaWeblog │ ├── README │ ├── apis │ ├── blogger_api.rb │ ├── blogger_service.rb │ ├── meta_weblog_api.rb │ └── meta_weblog_service.rb │ └── controllers │ └── xmlrpc_controller.rb ├── generators └── web_service │ ├── USAGE │ ├── templates │ ├── api_definition.rb │ ├── controller.rb │ └── functional_test.rb │ └── web_service_generator.rb ├── install.rb ├── lib ├── action_web_service.rb ├── action_web_service │ ├── api.rb │ ├── base.rb │ ├── casting.rb │ ├── client.rb │ ├── client │ │ ├── base.rb │ │ ├── soap_client.rb │ │ └── xmlrpc_client.rb │ ├── container.rb │ ├── container │ │ ├── action_controller_container.rb │ │ ├── delegated_container.rb │ │ └── direct_container.rb │ ├── dispatcher.rb │ ├── dispatcher │ │ ├── abstract.rb │ │ └── action_controller_dispatcher.rb │ ├── invocation.rb │ ├── protocol.rb │ ├── protocol │ │ ├── abstract.rb │ │ ├── discovery.rb │ │ ├── soap_protocol.rb │ │ ├── soap_protocol │ │ │ └── marshaler.rb │ │ └── xmlrpc_protocol.rb │ ├── scaffolding.rb │ ├── struct.rb │ ├── support │ │ ├── class_inheritable_options.rb │ │ └── signature_types.rb │ ├── templates │ │ └── scaffolds │ │ │ ├── layout.html.erb │ │ │ ├── methods.html.erb │ │ │ ├── parameters.html.erb │ │ │ └── result.html.erb │ ├── test_invoke.rb │ └── version.rb └── actionwebservice.rb ├── setup.rb └── test ├── abstract_client.rb ├── abstract_dispatcher.rb ├── abstract_unit.rb ├── api_test.rb ├── apis ├── auto_load_api.rb └── broken_auto_load_api.rb ├── base_test.rb ├── casting_test.rb ├── client_soap_test.rb ├── client_xmlrpc_test.rb ├── container_test.rb ├── dispatcher_action_controller_soap_test.rb ├── dispatcher_action_controller_xmlrpc_test.rb ├── fixtures ├── db_definitions │ └── mysql.sql └── users.yml ├── gencov ├── invocation_test.rb ├── run ├── scaffolded_controller_test.rb ├── struct_test.rb └── test_invoke_test.rb /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2005 Leon Breedt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Action Web Service -- Serving APIs on rails 2 | 3 | Action Web Service provides a way to publish interoperable web service APIs with 4 | Rails without spending a lot of time delving into protocol details. 5 | 6 | 7 | == Features 8 | 9 | * SOAP RPC protocol support 10 | * Dynamic WSDL generation for APIs 11 | * XML-RPC protocol support 12 | * Clients that use the same API definitions as the server for 13 | easy interoperability with other Action Web Service based applications 14 | * Type signature hints to improve interoperability with static languages 15 | * Active Record model class support in signatures 16 | 17 | 18 | == Defining your APIs 19 | 20 | You specify the methods you want to make available as API methods in an 21 | ActionWebService::API::Base derivative, and then specify this API 22 | definition class wherever you want to use that API. 23 | 24 | The implementation of the methods is done separately from the API 25 | specification. 26 | 27 | 28 | ==== Method name inflection 29 | 30 | Action Web Service will camelcase the method names according to Rails Inflector 31 | rules for the API visible to public callers. What this means, for example, 32 | is that the method names in generated WSDL will be camelcased, and callers will 33 | have to supply the camelcased name in their requests for the request to 34 | succeed. 35 | 36 | If you do not desire this behaviour, you can turn it off with the 37 | ActionWebService::API::Base +inflect_names+ option. 38 | 39 | 40 | ==== Inflection examples 41 | 42 | :add => Add 43 | :find_all => FindAll 44 | 45 | 46 | ==== Disabling inflection 47 | 48 | class PersonAPI < ActionWebService::API::Base 49 | inflect_names false 50 | end 51 | 52 | 53 | ==== API definition example 54 | 55 | class PersonAPI < ActionWebService::API::Base 56 | api_method :add, :expects => [:string, :string, :bool], :returns => [:int] 57 | api_method :remove, :expects => [:int], :returns => [:bool] 58 | end 59 | 60 | ==== API usage example 61 | 62 | class PersonController < ActionController::Base 63 | web_service_api PersonAPI 64 | 65 | def add 66 | end 67 | 68 | def remove 69 | end 70 | end 71 | 72 | 73 | == Publishing your APIs 74 | 75 | Action Web Service uses Action Pack to process protocol requests. There are two 76 | modes of dispatching protocol requests, _Direct_, and _Delegated_. 77 | 78 | 79 | === Direct dispatching 80 | 81 | This is the default mode. In this mode, public controller instance methods 82 | implement the API methods, and parameters are passed through to the methods in 83 | accordance with the API specification. 84 | 85 | The return value of the method is sent back as the return value to the 86 | caller. 87 | 88 | In this mode, a special api action is generated in the target 89 | controller to unwrap the protocol request, forward it on to the relevant method 90 | and send back the wrapped return value. This action must not be 91 | overridden. 92 | 93 | ==== Direct dispatching example 94 | 95 | class PersonController < ApplicationController 96 | web_service_api PersonAPI 97 | 98 | def add 99 | end 100 | 101 | def remove 102 | end 103 | end 104 | 105 | class PersonAPI < ActionWebService::API::Base 106 | ... 107 | end 108 | 109 | 110 | For this example, protocol requests for +Add+ and +Remove+ methods sent to 111 | /person/api will be routed to the controller methods +add+ and +remove+. 112 | 113 | 114 | === Delegated dispatching 115 | 116 | This mode can be turned on by setting the +web_service_dispatching_mode+ option 117 | in a controller to :delegated. 118 | 119 | In this mode, the controller contains one or more web service objects (objects 120 | that implement an ActionWebService::API::Base definition). These web service 121 | objects are each mapped onto one controller action only. 122 | 123 | ==== Delegated dispatching example 124 | 125 | class ApiController < ApplicationController 126 | web_service_dispatching_mode :delegated 127 | 128 | web_service :person, PersonService.new 129 | end 130 | 131 | class PersonService < ActionWebService::Base 132 | web_service_api PersonAPI 133 | 134 | def add 135 | end 136 | 137 | def remove 138 | end 139 | end 140 | 141 | class PersonAPI < ActionWebService::API::Base 142 | ... 143 | end 144 | 145 | 146 | For this example, all protocol requests for +PersonService+ are 147 | sent to the /api/person action. 148 | 149 | The /api/person action is generated when the +web_service+ 150 | method is called. This action must not be overridden. 151 | 152 | Other controller actions (actions that aren't the target of a +web_service+ call) 153 | are ignored for ActionWebService purposes, and can do normal action tasks. 154 | 155 | 156 | === Layered dispatching 157 | 158 | This mode can be turned on by setting the +web_service_dispatching_mode+ option 159 | in a controller to :layered. 160 | 161 | This mode is similar to _delegated_ mode, in that multiple web service objects 162 | can be attached to one controller, however, all protocol requests are sent to a 163 | single endpoint. 164 | 165 | Use this mode when you want to share code between XML-RPC and SOAP clients, 166 | for APIs where the XML-RPC method names have prefixes. An example of such 167 | a method name would be blogger.newPost. 168 | 169 | 170 | ==== Layered dispatching example 171 | 172 | 173 | class ApiController < ApplicationController 174 | web_service_dispatching_mode :layered 175 | 176 | web_service :mt, MovableTypeService.new 177 | web_service :blogger, BloggerService.new 178 | web_service :metaWeblog, MetaWeblogService.new 179 | end 180 | 181 | class MovableTypeService < ActionWebService::Base 182 | ... 183 | end 184 | 185 | class BloggerService < ActionWebService::Base 186 | ... 187 | end 188 | 189 | class MetaWeblogService < ActionWebService::API::Base 190 | ... 191 | end 192 | 193 | 194 | For this example, an XML-RPC call for a method with a name like 195 | mt.getCategories will be sent to the getCategories 196 | method on the :mt service. 197 | 198 | 199 | == Customizing WSDL generation 200 | 201 | You can customize the names used for the SOAP bindings in the generated 202 | WSDL by using the wsdl_service_name option in a controller: 203 | 204 | class WsController < ApplicationController 205 | wsdl_service_name 'MyApp' 206 | end 207 | 208 | You can also customize the namespace used in the generated WSDL for 209 | custom types and message definition types: 210 | 211 | class WsController < ApplicationController 212 | wsdl_namespace 'http://my.company.com/app/wsapi' 213 | end 214 | 215 | The default namespace used is 'urn:ActionWebService', if you don't supply 216 | one. 217 | 218 | 219 | == ActionWebService and UTF-8 220 | 221 | If you're going to be sending back strings containing non-ASCII UTF-8 222 | characters using the :string data type, you need to make sure that 223 | Ruby is using UTF-8 as the default encoding for its strings. 224 | 225 | The default in Ruby is to use US-ASCII encoding for strings, which causes a string 226 | validation check in the Ruby SOAP library to fail and your string to be sent 227 | back as a Base-64 value, which may confuse clients that expected strings 228 | because of the WSDL. 229 | 230 | Two ways of setting the default string encoding are: 231 | 232 | * Start Ruby using the -Ku command-line option to the Ruby executable 233 | * Set the $KCODE flag in config/environment.rb to the 234 | string 'UTF8' 235 | 236 | 237 | == Testing your APIs 238 | 239 | 240 | === Functional testing 241 | 242 | You can perform testing of your APIs by creating a functional test for the 243 | controller dispatching the API, and calling #invoke in the test case to 244 | perform the invocation. 245 | 246 | Example: 247 | 248 | class PersonApiControllerTest < Test::Unit::TestCase 249 | def setup 250 | @controller = PersonController.new 251 | @request = ActionController::TestRequest.new 252 | @response = ActionController::TestResponse.new 253 | end 254 | 255 | def test_add 256 | result = invoke :remove, 1 257 | assert_equal true, result 258 | end 259 | end 260 | 261 | This example invokes the API method test, defined on 262 | the PersonController, and returns the result. 263 | 264 | If you're not using SOAP (or you're having serialisation difficulties), 265 | you can test XMLRPC like this: 266 | 267 | class PersonApiControllerTest < Test::Unit::TestCase 268 | def setup 269 | @controller = PersonController.new 270 | @request = ActionController::TestRequest.new 271 | @response = ActionController::TestResponse.new 272 | 273 | @protocol = :xmlrpc # can also be :soap, the default 274 | end 275 | 276 | def test_add 277 | result = invoke :remove, 1 # no change here 278 | assert_equal true, result 279 | end 280 | end 281 | 282 | === Scaffolding 283 | 284 | You can also test your APIs with a web browser by attaching scaffolding 285 | to the controller. 286 | 287 | Example: 288 | 289 | class PersonController 290 | web_service_scaffold :invocation 291 | end 292 | 293 | This creates an action named invocation on the PersonController. 294 | 295 | Navigating to this action lets you select the method to invoke, supply the parameters, 296 | and view the result of the invocation. 297 | 298 | 299 | == Using the client support 300 | 301 | Action Web Service includes client classes that can use the same API 302 | definition as the server. The advantage of this approach is that your client 303 | will have the same support for Active Record and structured types as the 304 | server, and can just use them directly, and rely on the marshaling to Do The 305 | Right Thing. 306 | 307 | *Note*: The client support is intended for communication between Ruby on Rails 308 | applications that both use Action Web Service. It may work with other servers, but 309 | that is not its intended use, and interoperability can't be guaranteed, especially 310 | not for .NET web services. 311 | 312 | Web services protocol specifications are complex, and Action Web Service client 313 | support can only be guaranteed to work with a subset. 314 | 315 | 316 | ==== Factory created client example 317 | 318 | class BlogManagerController < ApplicationController 319 | web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger' 320 | end 321 | 322 | class SearchingController < ApplicationController 323 | web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch' 324 | end 325 | 326 | See ActionWebService::API::ActionController::ClassMethods for more details. 327 | 328 | ==== Manually created client example 329 | 330 | class PersonAPI < ActionWebService::API::Base 331 | api_method :find_all, :returns => [[Person]] 332 | end 333 | 334 | soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") 335 | persons = soap_client.find_all 336 | 337 | class BloggerAPI < ActionWebService::API::Base 338 | inflect_names false 339 | api_method :getRecentPosts, :returns => [[Blog::Post]] 340 | end 341 | 342 | blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger") 343 | posts = blog.getRecentPosts 344 | 345 | 346 | See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details. 347 | 348 | == Dependencies 349 | 350 | Action Web Service requires that the Action Pack and Active Record are either 351 | available to be required immediately or are accessible as GEMs. 352 | 353 | It also requires a version of Ruby that includes SOAP support in the standard 354 | library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this 355 | is the version tested against. 356 | 357 | 358 | == Download 359 | 360 | The latest Action Web Service version can be downloaded from 361 | http://rubyforge.org/projects/actionservice 362 | 363 | 364 | == Installation 365 | 366 | You can install Action Web Service with the following command. 367 | 368 | % [sudo] ruby setup.rb 369 | 370 | 371 | == License 372 | 373 | Action Web Service is released under the MIT license. 374 | 375 | 376 | == Support 377 | 378 | The Ruby on Rails mailing list 379 | 380 | Or, to contact the author, send mail to bitserf@gmail.com 381 | 382 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'rake/testtask' 4 | require 'rake/rdoctask' 5 | require 'rake/packagetask' 6 | require 'rake/gempackagetask' 7 | require 'rake/contrib/rubyforgepublisher' 8 | require 'fileutils' 9 | require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version') 10 | 11 | PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' 12 | PKG_NAME = 'actionwebservice' 13 | PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD 14 | PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" 15 | PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}" 16 | 17 | RELEASE_NAME = "REL #{PKG_VERSION}" 18 | 19 | RUBY_FORGE_PROJECT = "aws" 20 | RUBY_FORGE_USER = "webster132" 21 | 22 | desc "Default Task" 23 | task :default => [ :test ] 24 | 25 | 26 | # Run the unit tests 27 | Rake::TestTask.new { |t| 28 | t.libs << "test" 29 | t.test_files = Dir['test/*_test.rb'] 30 | t.verbose = true 31 | } 32 | 33 | SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions)) 34 | 35 | desc 'Build the MySQL test database' 36 | task :build_database do 37 | %x( mysqladmin -uroot create actionwebservice_unittest ) 38 | %x( mysql -uroot actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} ) 39 | end 40 | 41 | 42 | # Generate the RDoc documentation 43 | Rake::RDocTask.new { |rdoc| 44 | rdoc.rdoc_dir = 'doc' 45 | rdoc.title = "Action Web Service -- Web services for Action Pack" 46 | rdoc.options << '--line-numbers' << '--inline-source' 47 | rdoc.options << '--charset' << 'utf-8' 48 | rdoc.template = "#{ENV['template']}.rb" if ENV['template'] 49 | rdoc.rdoc_files.include('README') 50 | rdoc.rdoc_files.include('CHANGELOG') 51 | rdoc.rdoc_files.include('lib/action_web_service.rb') 52 | rdoc.rdoc_files.include('lib/action_web_service/*.rb') 53 | rdoc.rdoc_files.include('lib/action_web_service/api/*.rb') 54 | rdoc.rdoc_files.include('lib/action_web_service/client/*.rb') 55 | rdoc.rdoc_files.include('lib/action_web_service/container/*.rb') 56 | rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb') 57 | rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb') 58 | rdoc.rdoc_files.include('lib/action_web_service/support/*.rb') 59 | } 60 | 61 | 62 | # Create compressed packages 63 | spec = Gem::Specification.new do |s| 64 | s.platform = Gem::Platform::RUBY 65 | s.name = PKG_NAME 66 | s.summary = "Web service support for Action Pack." 67 | s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack} 68 | s.version = PKG_VERSION 69 | 70 | s.author = "Leon Breedt, Kent Sibilev" 71 | s.email = "bitserf@gmail.com, ksibilev@yahoo.com" 72 | s.rubyforge_project = "aws" 73 | s.homepage = "http://www.rubyonrails.org" 74 | 75 | s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD) 76 | s.add_dependency('activerecord', '= 2.3.2' + PKG_BUILD) 77 | 78 | s.has_rdoc = true 79 | s.requirements << 'none' 80 | s.require_path = 'lib' 81 | s.autorequire = 'actionwebservice' 82 | 83 | s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ] 84 | s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) } 85 | s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) } 86 | s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) } 87 | s.files = s.files + Dir.glob( "generators/**/*" ).delete_if { |item| item.match( /\.(svn|git)/ ) } 88 | end 89 | Rake::GemPackageTask.new(spec) do |p| 90 | p.gem_spec = spec 91 | p.need_tar = true 92 | p.need_zip = true 93 | end 94 | 95 | 96 | # Publish beta gem 97 | desc "Publish the API documentation" 98 | task :pgem => [:package] do 99 | Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload 100 | `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` 101 | end 102 | 103 | # Publish documentation 104 | desc "Publish the API documentation" 105 | task :pdoc => [:rdoc] do 106 | Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload 107 | end 108 | 109 | 110 | def each_source_file(*args) 111 | prefix, includes, excludes, open_file = args 112 | prefix ||= File.dirname(__FILE__) 113 | open_file = true if open_file.nil? 114 | includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$] 115 | excludes ||= %w[lib\/action_web_service\/vendor] 116 | Find.find(prefix) do |file_name| 117 | next if file_name =~ /\.svn/ 118 | file_name.gsub!(/^\.\//, '') 119 | continue = false 120 | includes.each do |inc| 121 | if file_name.match(/#{inc}/) 122 | continue = true 123 | break 124 | end 125 | end 126 | next unless continue 127 | excludes.each do |exc| 128 | if file_name.match(/#{exc}/) 129 | continue = false 130 | break 131 | end 132 | end 133 | next unless continue 134 | if open_file 135 | File.open(file_name) do |f| 136 | yield file_name, f 137 | end 138 | else 139 | yield file_name 140 | end 141 | end 142 | end 143 | 144 | desc "Count lines of the AWS source code" 145 | task :lines do 146 | total_lines = total_loc = 0 147 | puts "Per File:" 148 | each_source_file do |file_name, f| 149 | file_lines = file_loc = 0 150 | while line = f.gets 151 | file_lines += 1 152 | next if line =~ /^\s*$/ 153 | next if line =~ /^\s*#/ 154 | file_loc += 1 155 | end 156 | puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}" 157 | total_lines += file_lines 158 | total_loc += file_loc 159 | end 160 | puts "Total:" 161 | puts " Lines #{total_lines}, LOC #{total_loc}" 162 | end 163 | 164 | desc "Publish the release files to RubyForge." 165 | task :release => [ :package ] do 166 | require 'rubyforge' 167 | 168 | packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" } 169 | 170 | rubyforge = RubyForge.new 171 | rubyforge.login 172 | rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages) 173 | end 174 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | = Post-1.0 2 | - Document/Literal SOAP support 3 | - URL-based dispatching, URL identifies method 4 | 5 | - Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching 6 | in general. Support vanilla XML-format as a "Rails" protocol? 7 | XML::Simple deserialization into params? 8 | 9 | web_service_dispatching_mode :rest 10 | 11 | def method1(params) 12 | end 13 | 14 | def method2(params) 15 | end 16 | 17 | 18 | /ws/method1 19 | 20 | /ws/method2 21 | 22 | 23 | - Allow locking down a controller to only accept messages for a particular 24 | protocol. This will allow us to generate fully conformant error messages 25 | in cases where we currently fudge it if we don't know the protocol. 26 | 27 | - Allow AWS user to participate in typecasting, so they can centralize 28 | workarounds for buggy input in one place 29 | 30 | = Refactoring 31 | - Don't have clean way to go from SOAP Class object to the xsd:NAME type 32 | string -- NaHi possibly looking at remedying this situation 33 | -------------------------------------------------------------------------------- /actionwebservice.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.platform = Gem::Platform::RUBY 3 | s.name = 'actionwebservice' 4 | s.summary = "Web service support for Action Pack." 5 | s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack} 6 | s.version = '2.3.2' 7 | 8 | s.author = "Leon Breedt, Kent Sibilev" 9 | s.email = "bitserf@gmail.com, ksibilev@yahoo.com" 10 | s.rubyforge_project = "aws" 11 | s.homepage = "http://www.rubyonrails.org" 12 | 13 | s.add_dependency('actionpack', '= 2.3.2') 14 | s.add_dependency('activerecord', '= 2.3.2') 15 | 16 | s.has_rdoc = true 17 | s.requirements << 'none' 18 | s.require_path = 'lib' 19 | 20 | s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ] 21 | s.files = s.files + 22 | ["examples/googlesearch", "examples/googlesearch/autoloading", "examples/googlesearch/autoloading/google_search_api.rb", "examples/googlesearch/autoloading/google_search_controller.rb", "examples/googlesearch/delegated", "examples/googlesearch/delegated/google_search_service.rb", "examples/googlesearch/delegated/search_controller.rb", "examples/googlesearch/direct", "examples/googlesearch/direct/google_search_api.rb", "examples/googlesearch/direct/search_controller.rb", "examples/googlesearch/README", "examples/metaWeblog", "examples/metaWeblog/apis", "examples/metaWeblog/apis/blogger_api.rb", "examples/metaWeblog/apis/blogger_service.rb", "examples/metaWeblog/apis/meta_weblog_api.rb", "examples/metaWeblog/apis/meta_weblog_service.rb", "examples/metaWeblog/controllers", "examples/metaWeblog/controllers/xmlrpc_controller.rb", "examples/metaWeblog/README"] 23 | s.files = s.files + 24 | ["lib/action_web_service", "lib/action_web_service/api.rb", "lib/action_web_service/base.rb", "lib/action_web_service/casting.rb", "lib/action_web_service/client", "lib/action_web_service/client/base.rb", "lib/action_web_service/client/soap_client.rb", "lib/action_web_service/client/xmlrpc_client.rb", "lib/action_web_service/client.rb", "lib/action_web_service/container", "lib/action_web_service/container/action_controller_container.rb", "lib/action_web_service/container/delegated_container.rb", "lib/action_web_service/container/direct_container.rb", "lib/action_web_service/container.rb", "lib/action_web_service/dispatcher", "lib/action_web_service/dispatcher/abstract.rb", "lib/action_web_service/dispatcher/action_controller_dispatcher.rb", "lib/action_web_service/dispatcher.rb", "lib/action_web_service/invocation.rb", "lib/action_web_service/protocol", "lib/action_web_service/protocol/abstract.rb", "lib/action_web_service/protocol/discovery.rb", "lib/action_web_service/protocol/soap_protocol", "lib/action_web_service/protocol/soap_protocol/marshaler.rb", "lib/action_web_service/protocol/soap_protocol.rb", "lib/action_web_service/protocol/xmlrpc_protocol.rb", "lib/action_web_service/protocol.rb", "lib/action_web_service/scaffolding.rb", "lib/action_web_service/struct.rb", "lib/action_web_service/support", "lib/action_web_service/support/class_inheritable_options.rb", "lib/action_web_service/support/signature_types.rb", "lib/action_web_service/templates", "lib/action_web_service/templates/scaffolds", "lib/action_web_service/templates/scaffolds/layout.html.erb", "lib/action_web_service/templates/scaffolds/methods.html.erb", "lib/action_web_service/templates/scaffolds/parameters.html.erb", "lib/action_web_service/templates/scaffolds/result.html.erb", "lib/action_web_service/test_invoke.rb", "lib/action_web_service/version.rb", "lib/action_web_service.rb", "lib/actionwebservice.rb"] 25 | s.files = s.files + 26 | ["test/abstract_client.rb", "test/abstract_dispatcher.rb", "test/abstract_unit.rb", "test/api_test.rb", "test/apis", "test/apis/auto_load_api.rb", "test/apis/broken_auto_load_api.rb", "test/base_test.rb", "test/casting_test.rb", "test/client_soap_test.rb", "test/client_xmlrpc_test.rb", "test/container_test.rb", "test/dispatcher_action_controller_soap_test.rb", "test/dispatcher_action_controller_xmlrpc_test.rb", "test/fixtures", "test/fixtures/db_definitions", "test/fixtures/db_definitions/mysql.sql", "test/fixtures/users.yml", "test/gencov", "test/invocation_test.rb", "test/run", "test/scaffolded_controller_test.rb", "test/struct_test.rb", "test/test_invoke_test.rb"] 27 | s.files = s.files + 28 | ["generators/web_service", "generators/web_service/templates", "generators/web_service/templates/api_definition.rb", "generators/web_service/templates/controller.rb", "generators/web_service/templates/functional_test.rb", "generators/web_service/USAGE", "generators/web_service/web_service_generator.rb"] 29 | end 30 | -------------------------------------------------------------------------------- /examples/googlesearch/README: -------------------------------------------------------------------------------- 1 | = Google Service example 2 | 3 | This example shows how one would implement an API like Google 4 | Search that uses lots of structured types. 5 | 6 | There are examples for "Direct" and "Delegated" dispatching 7 | modes. 8 | 9 | There is also an example for API definition file autoloading. 10 | 11 | 12 | = Running the examples 13 | 14 | 1. Add the files to an Action Web Service enabled Rails project. 15 | 16 | "Direct" example: 17 | 18 | * Copy direct/search_controller.rb to "app/controllers" 19 | in a Rails project. 20 | * Copy direct/google_search_api.rb to "app/apis" 21 | in a Rails project 22 | 23 | "Delegated" example: 24 | 25 | * Copy delegated/search_controller.rb to "app/controllers" 26 | in a Rails project. 27 | * Copy delegated/google_search_service.rb to "lib" 28 | in a Rails project. 29 | 30 | "Autoloading" example: 31 | 32 | * Copy autoloading/google_search_api.rb to "app/apis" (create the directory 33 | if it doesn't exist) in a Rails project. 34 | 35 | * Copy autoloading/google_search_controller.rb "app/controllers" 36 | in a Rails project. 37 | 38 | 39 | 2. Go to the WSDL url in a browser, and check that it looks correct. 40 | 41 | "Direct" and "Delegated" examples: 42 | http://url_to_project/search/wsdl 43 | 44 | "Autoloading" example: 45 | http://url_to_project/google_search/wsdl 46 | 47 | You can compare it to Google's hand-coded WSDL at http://api.google.com/GoogleSearch.wsdl 48 | and see how close (or not) the generated version is. 49 | 50 | Note that I used GoogleSearch as the canonical "best practice" 51 | interoperable example when implementing WSDL/SOAP support, which might 52 | explain extreme similarities :) 53 | 54 | 55 | 3. Test that it works with .NET (Mono in this example): 56 | 57 | $ wget WSDL_URL 58 | $ mv wsdl GoogleSearch.wsdl 59 | $ wsdl -out:GoogleSearch.cs GoogleSearch.wsdl 60 | 61 | Add these lines to the GoogleSearchService class body (be mindful of the 62 | wrapping): 63 | 64 | public static void Main(string[] args) 65 | { 66 | GoogleSearchResult result; 67 | GoogleSearchService service; 68 | 69 | service = new GoogleSearchService(); 70 | result = service.doGoogleSearch("myApiKey", "my query", 10, 30, true, "restrict", false, "lr", "ie", "oe"); 71 | System.Console.WriteLine("documentFiltering: {0}", result.documentFiltering); 72 | System.Console.WriteLine("searchComments: {0}", result.searchComments); 73 | System.Console.WriteLine("estimatedTotalResultsCount: {0}", result.estimatedTotalResultsCount); 74 | System.Console.WriteLine("estimateIsExact: {0}", result.estimateIsExact); 75 | System.Console.WriteLine("resultElements:"); 76 | foreach (ResultElement element in result.resultElements) { 77 | System.Console.WriteLine("\tsummary: {0}", element.summary); 78 | System.Console.WriteLine("\tURL: {0}", element.URL); 79 | System.Console.WriteLine("\tsnippet: {0}", element.snippet); 80 | System.Console.WriteLine("\ttitle: {0}", element.title); 81 | System.Console.WriteLine("\tcachedSize: {0}", element.cachedSize); 82 | System.Console.WriteLine("\trelatedInformationPresent: {0}", element.relatedInformationPresent); 83 | System.Console.WriteLine("\thostName: {0}", element.hostName); 84 | System.Console.WriteLine("\tdirectoryCategory: {0}", element.directoryCategory.fullViewableName); 85 | System.Console.WriteLine("\tdirectoryTitle: {0}", element.directoryTitle); 86 | } 87 | System.Console.WriteLine("searchQuery: {0}", result.searchQuery); 88 | System.Console.WriteLine("startIndex: {0}", result.startIndex); 89 | System.Console.WriteLine("endIndex: {0}", result.endIndex); 90 | System.Console.WriteLine("searchTips: {0}", result.searchTips); 91 | System.Console.WriteLine("directoryCategories:"); 92 | foreach (DirectoryCategory cat in result.directoryCategories) { 93 | System.Console.WriteLine("\t{0} ({1})", cat.fullViewableName, cat.specialEncoding); 94 | } 95 | System.Console.WriteLine("searchTime: {0}", result.searchTime); 96 | } 97 | 98 | Now compile and run: 99 | 100 | $ mcs -reference:System.Web.Services GoogleSearch.cs 101 | $ mono GoogleSearch.exe 102 | 103 | 104 | If you had the application running (on the same host you got 105 | the WSDL from), you should see something like this: 106 | 107 | 108 | documentFiltering: True 109 | searchComments: 110 | estimatedTotalResultsCount: 322000 111 | estimateIsExact: False 112 | resultElements: 113 | summary: ONlamp.com: Rolling with Ruby on Rails 114 | URL: http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html 115 | snippet: Curt Hibbs shows off Ruby on Rails by building a simple ... 116 | title: Teh Railz0r 117 | cachedSize: Almost no lines of code! 118 | relatedInformationPresent: True 119 | hostName: rubyonrails.com 120 | directoryCategory: Web Development 121 | directoryTitle: 122 | searchQuery: http://www.google.com/search?q=ruby+on+rails 123 | startIndex: 10 124 | endIndex: 40 125 | searchTips: "on" is a very common word and was not included in your search [details] 126 | directoryCategories: 127 | Web Development (UTF-8) 128 | Programming (US-ASCII) 129 | searchTime: 1E-06 130 | 131 | 132 | Also, if an API method throws an exception, it will be sent back to the 133 | caller in the protocol's exception format, so they should get an exception 134 | thrown on their side with a meaningful error message. 135 | 136 | If you don't like this behaviour, you can do: 137 | 138 | class MyController < ActionController::Base 139 | web_service_exception_reporting false 140 | end 141 | 142 | 4. Crack open a beer. Publishing APIs for working with the same model as 143 | your Rails web app should be easy from now on :) 144 | -------------------------------------------------------------------------------- /examples/googlesearch/autoloading/google_search_api.rb: -------------------------------------------------------------------------------- 1 | class DirectoryCategory < ActionWebService::Struct 2 | member :fullViewableName, :string 3 | member :specialEncoding, :string 4 | end 5 | 6 | class ResultElement < ActionWebService::Struct 7 | member :summary, :string 8 | member :URL, :string 9 | member :snippet, :string 10 | member :title, :string 11 | member :cachedSize, :string 12 | member :relatedInformationPresent, :bool 13 | member :hostName, :string 14 | member :directoryCategory, DirectoryCategory 15 | member :directoryTitle, :string 16 | end 17 | 18 | class GoogleSearchResult < ActionWebService::Struct 19 | member :documentFiltering, :bool 20 | member :searchComments, :string 21 | member :estimatedTotalResultsCount, :int 22 | member :estimateIsExact, :bool 23 | member :resultElements, [ResultElement] 24 | member :searchQuery, :string 25 | member :startIndex, :int 26 | member :endIndex, :int 27 | member :searchTips, :string 28 | member :directoryCategories, [DirectoryCategory] 29 | member :searchTime, :float 30 | end 31 | 32 | class GoogleSearchAPI < ActionWebService::API::Base 33 | inflect_names false 34 | 35 | api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}] 36 | api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}] 37 | 38 | api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [ 39 | {:key=>:string}, 40 | {:q=>:string}, 41 | {:start=>:int}, 42 | {:maxResults=>:int}, 43 | {:filter=>:bool}, 44 | {:restrict=>:string}, 45 | {:safeSearch=>:bool}, 46 | {:lr=>:string}, 47 | {:ie=>:string}, 48 | {:oe=>:string} 49 | ] 50 | end 51 | -------------------------------------------------------------------------------- /examples/googlesearch/autoloading/google_search_controller.rb: -------------------------------------------------------------------------------- 1 | class GoogleSearchController < ApplicationController 2 | wsdl_service_name 'GoogleSearch' 3 | 4 | def doGetCachedPage 5 | "i am a cached page. my key was %s, url was %s" % [@params['key'], @params['url']] 6 | end 7 | 8 | def doSpellingSuggestion 9 | "%s: Did you mean '%s'?" % [@params['key'], @params['phrase']] 10 | end 11 | 12 | def doGoogleSearch 13 | resultElement = ResultElement.new 14 | resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails" 15 | resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html" 16 | resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " + 17 | "almost no Ruby experience. ... Rolling with Ruby on Rails. ..." 18 | resultElement.title = "Teh Railz0r" 19 | resultElement.cachedSize = "Almost no lines of code!" 20 | resultElement.relatedInformationPresent = true 21 | resultElement.hostName = "rubyonrails.com" 22 | resultElement.directoryCategory = category("Web Development", "UTF-8") 23 | 24 | result = GoogleSearchResult.new 25 | result.documentFiltering = @params['filter'] 26 | result.searchComments = "" 27 | result.estimatedTotalResultsCount = 322000 28 | result.estimateIsExact = false 29 | result.resultElements = [resultElement] 30 | result.searchQuery = "http://www.google.com/search?q=ruby+on+rails" 31 | result.startIndex = @params['start'] 32 | result.endIndex = @params['start'] + @params['maxResults'] 33 | result.searchTips = "\"on\" is a very common word and was not included in your search [details]" 34 | result.searchTime = 0.000001 35 | 36 | # For Mono, we have to clone objects if they're referenced by more than one place, otherwise 37 | # the Ruby SOAP collapses them into one instance and uses references all over the 38 | # place, confusing Mono. 39 | # 40 | # This has recently been fixed: 41 | # http://bugzilla.ximian.com/show_bug.cgi?id=72265 42 | result.directoryCategories = [ 43 | category("Web Development", "UTF-8"), 44 | category("Programming", "US-ASCII"), 45 | ] 46 | 47 | result 48 | end 49 | 50 | private 51 | def category(name, encoding) 52 | cat = DirectoryCategory.new 53 | cat.fullViewableName = name.dup 54 | cat.specialEncoding = encoding.dup 55 | cat 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /examples/googlesearch/delegated/google_search_service.rb: -------------------------------------------------------------------------------- 1 | class DirectoryCategory < ActionWebService::Struct 2 | member :fullViewableName, :string 3 | member :specialEncoding, :string 4 | end 5 | 6 | class ResultElement < ActionWebService::Struct 7 | member :summary, :string 8 | member :URL, :string 9 | member :snippet, :string 10 | member :title, :string 11 | member :cachedSize, :string 12 | member :relatedInformationPresent, :bool 13 | member :hostName, :string 14 | member :directoryCategory, DirectoryCategory 15 | member :directoryTitle, :string 16 | end 17 | 18 | class GoogleSearchResult < ActionWebService::Struct 19 | member :documentFiltering, :bool 20 | member :searchComments, :string 21 | member :estimatedTotalResultsCount, :int 22 | member :estimateIsExact, :bool 23 | member :resultElements, [ResultElement] 24 | member :searchQuery, :string 25 | member :startIndex, :int 26 | member :endIndex, :int 27 | member :searchTips, :string 28 | member :directoryCategories, [DirectoryCategory] 29 | member :searchTime, :float 30 | end 31 | 32 | class GoogleSearchAPI < ActionWebService::API::Base 33 | inflect_names false 34 | 35 | api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}] 36 | api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}] 37 | 38 | api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [ 39 | {:key=>:string}, 40 | {:q=>:string}, 41 | {:start=>:int}, 42 | {:maxResults=>:int}, 43 | {:filter=>:bool}, 44 | {:restrict=>:string}, 45 | {:safeSearch=>:bool}, 46 | {:lr=>:string}, 47 | {:ie=>:string}, 48 | {:oe=>:string} 49 | ] 50 | end 51 | 52 | class GoogleSearchService < ActionWebService::Base 53 | web_service_api GoogleSearchAPI 54 | 55 | def doGetCachedPage(key, url) 56 | "i am a cached page" 57 | end 58 | 59 | def doSpellingSuggestion(key, phrase) 60 | "Did you mean 'teh'?" 61 | end 62 | 63 | def doGoogleSearch(key, q, start, maxResults, filter, restrict, safeSearch, lr, ie, oe) 64 | resultElement = ResultElement.new 65 | resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails" 66 | resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html" 67 | resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " + 68 | "almost no Ruby experience. ... Rolling with Ruby on Rails. ..." 69 | resultElement.title = "Teh Railz0r" 70 | resultElement.cachedSize = "Almost no lines of code!" 71 | resultElement.relatedInformationPresent = true 72 | resultElement.hostName = "rubyonrails.com" 73 | resultElement.directoryCategory = category("Web Development", "UTF-8") 74 | 75 | result = GoogleSearchResult.new 76 | result.documentFiltering = filter 77 | result.searchComments = "" 78 | result.estimatedTotalResultsCount = 322000 79 | result.estimateIsExact = false 80 | result.resultElements = [resultElement] 81 | result.searchQuery = "http://www.google.com/search?q=ruby+on+rails" 82 | result.startIndex = start 83 | result.endIndex = start + maxResults 84 | result.searchTips = "\"on\" is a very common word and was not included in your search [details]" 85 | result.searchTime = 0.000001 86 | 87 | # For Mono, we have to clone objects if they're referenced by more than one place, otherwise 88 | # the Ruby SOAP collapses them into one instance and uses references all over the 89 | # place, confusing Mono. 90 | # 91 | # This has recently been fixed: 92 | # http://bugzilla.ximian.com/show_bug.cgi?id=72265 93 | result.directoryCategories = [ 94 | category("Web Development", "UTF-8"), 95 | category("Programming", "US-ASCII"), 96 | ] 97 | 98 | result 99 | end 100 | 101 | private 102 | def category(name, encoding) 103 | cat = DirectoryCategory.new 104 | cat.fullViewableName = name.dup 105 | cat.specialEncoding = encoding.dup 106 | cat 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /examples/googlesearch/delegated/search_controller.rb: -------------------------------------------------------------------------------- 1 | require 'google_search_service' 2 | 3 | class SearchController < ApplicationController 4 | wsdl_service_name 'GoogleSearch' 5 | web_service_dispatching_mode :delegated 6 | web_service :beta3, GoogleSearchService.new 7 | end 8 | -------------------------------------------------------------------------------- /examples/googlesearch/direct/google_search_api.rb: -------------------------------------------------------------------------------- 1 | class DirectoryCategory < ActionWebService::Struct 2 | member :fullViewableName, :string 3 | member :specialEncoding, :string 4 | end 5 | 6 | class ResultElement < ActionWebService::Struct 7 | member :summary, :string 8 | member :URL, :string 9 | member :snippet, :string 10 | member :title, :string 11 | member :cachedSize, :string 12 | member :relatedInformationPresent, :bool 13 | member :hostName, :string 14 | member :directoryCategory, DirectoryCategory 15 | member :directoryTitle, :string 16 | end 17 | 18 | class GoogleSearchResult < ActionWebService::Struct 19 | member :documentFiltering, :bool 20 | member :searchComments, :string 21 | member :estimatedTotalResultsCount, :int 22 | member :estimateIsExact, :bool 23 | member :resultElements, [ResultElement] 24 | member :searchQuery, :string 25 | member :startIndex, :int 26 | member :endIndex, :int 27 | member :searchTips, :string 28 | member :directoryCategories, [DirectoryCategory] 29 | member :searchTime, :float 30 | end 31 | 32 | class GoogleSearchAPI < ActionWebService::API::Base 33 | inflect_names false 34 | 35 | api_method :doGetCachedPage, :returns => [:string], :expects => [{:key=>:string}, {:url=>:string}] 36 | api_method :doGetSpellingSuggestion, :returns => [:string], :expects => [{:key=>:string}, {:phrase=>:string}] 37 | 38 | api_method :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [ 39 | {:key=>:string}, 40 | {:q=>:string}, 41 | {:start=>:int}, 42 | {:maxResults=>:int}, 43 | {:filter=>:bool}, 44 | {:restrict=>:string}, 45 | {:safeSearch=>:bool}, 46 | {:lr=>:string}, 47 | {:ie=>:string}, 48 | {:oe=>:string} 49 | ] 50 | end 51 | -------------------------------------------------------------------------------- /examples/googlesearch/direct/search_controller.rb: -------------------------------------------------------------------------------- 1 | class SearchController < ApplicationController 2 | web_service_api :google_search 3 | wsdl_service_name 'GoogleSearch' 4 | 5 | def doGetCachedPage 6 | "i am a cached page. my key was %s, url was %s" % [@params['key'], @params['url']] 7 | end 8 | 9 | def doSpellingSuggestion 10 | "%s: Did you mean '%s'?" % [@params['key'], @params['phrase']] 11 | end 12 | 13 | def doGoogleSearch 14 | resultElement = ResultElement.new 15 | resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails" 16 | resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html" 17 | resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " + 18 | "almost no Ruby experience. ... Rolling with Ruby on Rails. ..." 19 | resultElement.title = "Teh Railz0r" 20 | resultElement.cachedSize = "Almost no lines of code!" 21 | resultElement.relatedInformationPresent = true 22 | resultElement.hostName = "rubyonrails.com" 23 | resultElement.directoryCategory = category("Web Development", "UTF-8") 24 | 25 | result = GoogleSearchResult.new 26 | result.documentFiltering = @params['filter'] 27 | result.searchComments = "" 28 | result.estimatedTotalResultsCount = 322000 29 | result.estimateIsExact = false 30 | result.resultElements = [resultElement] 31 | result.searchQuery = "http://www.google.com/search?q=ruby+on+rails" 32 | result.startIndex = @params['start'] 33 | result.endIndex = @params['start'] + @params['maxResults'] 34 | result.searchTips = "\"on\" is a very common word and was not included in your search [details]" 35 | result.searchTime = 0.000001 36 | 37 | # For Mono, we have to clone objects if they're referenced by more than one place, otherwise 38 | # the Ruby SOAP collapses them into one instance and uses references all over the 39 | # place, confusing Mono. 40 | # 41 | # This has recently been fixed: 42 | # http://bugzilla.ximian.com/show_bug.cgi?id=72265 43 | result.directoryCategories = [ 44 | category("Web Development", "UTF-8"), 45 | category("Programming", "US-ASCII"), 46 | ] 47 | 48 | result 49 | end 50 | 51 | private 52 | def category(name, encoding) 53 | cat = DirectoryCategory.new 54 | cat.fullViewableName = name.dup 55 | cat.specialEncoding = encoding.dup 56 | cat 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /examples/metaWeblog/README: -------------------------------------------------------------------------------- 1 | = metaWeblog example 2 | 3 | This example shows how one might begin to go about adding metaWeblog 4 | (http://www.xmlrpc.com/metaWeblogApi) API support to a Rails-based 5 | blogging application. 6 | 7 | The example APIs are more verbose than you may want to make them, for documentation 8 | reasons. 9 | 10 | = Running 11 | 12 | 1. Copy the "apis" directory and its files into "app" in a Rails project. 13 | 14 | 2. Copy the "controllers" directory and its files into "app" in a Rails project 15 | 16 | 3. Fire up a desktop blogging application (such as w.bloggar, MarsEdit, or BloGTK), 17 | point it at http://localhost:3000/xmlrpc/api, and try creating or editing blog posts. 18 | -------------------------------------------------------------------------------- /examples/metaWeblog/apis/blogger_api.rb: -------------------------------------------------------------------------------- 1 | # 2 | # see the blogger API spec at http://www.blogger.com/developers/api/1_docs/ 3 | # note that the method signatures are subtly different to metaWeblog, they 4 | # are not identical. take care to ensure you handle the different semantics 5 | # properly if you want to support blogger API too, to get maximum compatibility. 6 | # 7 | 8 | module Blog 9 | class Blog < ActionWebService::Struct 10 | member :url, :string 11 | member :blogid, :string 12 | member :blogName, :string 13 | end 14 | 15 | class User < ActionWebService::Struct 16 | member :nickname, :string 17 | member :userid, :string 18 | member :url, :string 19 | member :email, :string 20 | member :lastname, :string 21 | member :firstname, :string 22 | end 23 | end 24 | 25 | # 26 | # blogger 27 | # 28 | class BloggerAPI < ActionWebService::API::Base 29 | inflect_names false 30 | 31 | api_method :newPost, :returns => [:string], :expects => [ 32 | {:appkey=>:string}, 33 | {:blogid=>:string}, 34 | {:username=>:string}, 35 | {:password=>:string}, 36 | {:content=>:string}, 37 | {:publish=>:bool} 38 | ] 39 | 40 | api_method :editPost, :returns => [:bool], :expects => [ 41 | {:appkey=>:string}, 42 | {:postid=>:string}, 43 | {:username=>:string}, 44 | {:password=>:string}, 45 | {:content=>:string}, 46 | {:publish=>:bool} 47 | ] 48 | 49 | api_method :getUsersBlogs, :returns => [[Blog::Blog]], :expects => [ 50 | {:appkey=>:string}, 51 | {:username=>:string}, 52 | {:password=>:string} 53 | ] 54 | 55 | api_method :getUserInfo, :returns => [Blog::User], :expects => [ 56 | {:appkey=>:string}, 57 | {:username=>:string}, 58 | {:password=>:string} 59 | ] 60 | end 61 | -------------------------------------------------------------------------------- /examples/metaWeblog/apis/blogger_service.rb: -------------------------------------------------------------------------------- 1 | require 'blogger_api' 2 | 3 | class BloggerService < ActionWebService::Base 4 | web_service_api BloggerAPI 5 | 6 | def initialize 7 | @postid = 0 8 | end 9 | 10 | def newPost(key, id, user, pw, content, publish) 11 | $stderr.puts "id=#{id} user=#{user} pw=#{pw}, content=#{content.inspect} [#{publish}]" 12 | (@postid += 1).to_s 13 | end 14 | 15 | def editPost(key, post_id, user, pw, content, publish) 16 | $stderr.puts "id=#{post_id} user=#{user} pw=#{pw} content=#{content.inspect} [#{publish}]" 17 | true 18 | end 19 | 20 | def getUsersBlogs(key, user, pw) 21 | $stderr.puts "getting blogs for #{user}" 22 | blog = Blog::Blog.new( 23 | :url =>'http://blog', 24 | :blogid => 'myblog', 25 | :blogName => 'My Blog' 26 | ) 27 | [blog] 28 | end 29 | 30 | def getUserInfo(key, user, pw) 31 | $stderr.puts "getting user info for #{user}" 32 | Blog::User.new(:nickname => 'user', :email => 'user@test.com') 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/metaWeblog/apis/meta_weblog_api.rb: -------------------------------------------------------------------------------- 1 | # 2 | # here lie structures, cousins of those on http://www.xmlrpc.com/metaWeblog 3 | # but they don't necessarily the real world reflect 4 | # so if you do, find that your client complains: 5 | # please tell, of problems you suffered through 6 | # 7 | 8 | module Blog 9 | class Post < ActionWebService::Struct 10 | member :title, :string 11 | member :link, :string 12 | member :description, :string 13 | member :author, :string 14 | member :category, :string 15 | member :comments, :string 16 | member :guid, :string 17 | member :pubDate, :string 18 | end 19 | 20 | class Category < ActionWebService::Struct 21 | member :description, :string 22 | member :htmlUrl, :string 23 | member :rssUrl, :string 24 | end 25 | end 26 | 27 | # 28 | # metaWeblog 29 | # 30 | class MetaWeblogAPI < ActionWebService::API::Base 31 | inflect_names false 32 | 33 | api_method :newPost, :returns => [:string], :expects => [ 34 | {:blogid=>:string}, 35 | {:username=>:string}, 36 | {:password=>:string}, 37 | {:struct=>Blog::Post}, 38 | {:publish=>:bool} 39 | ] 40 | 41 | api_method :editPost, :returns => [:bool], :expects => [ 42 | {:postid=>:string}, 43 | {:username=>:string}, 44 | {:password=>:string}, 45 | {:struct=>Blog::Post}, 46 | {:publish=>:bool}, 47 | ] 48 | 49 | api_method :getPost, :returns => [Blog::Post], :expects => [ 50 | {:postid=>:string}, 51 | {:username=>:string}, 52 | {:password=>:string}, 53 | ] 54 | 55 | api_method :getCategories, :returns => [[Blog::Category]], :expects => [ 56 | {:blogid=>:string}, 57 | {:username=>:string}, 58 | {:password=>:string}, 59 | ] 60 | 61 | api_method :getRecentPosts, :returns => [[Blog::Post]], :expects => [ 62 | {:blogid=>:string}, 63 | {:username=>:string}, 64 | {:password=>:string}, 65 | {:numberOfPosts=>:int}, 66 | ] 67 | end 68 | -------------------------------------------------------------------------------- /examples/metaWeblog/apis/meta_weblog_service.rb: -------------------------------------------------------------------------------- 1 | require 'meta_weblog_api' 2 | 3 | class MetaWeblogService < ActionWebService::Base 4 | web_service_api MetaWeblogAPI 5 | 6 | def initialize 7 | @postid = 0 8 | end 9 | 10 | def newPost(id, user, pw, struct, publish) 11 | $stderr.puts "id=#{id} user=#{user} pw=#{pw}, struct=#{struct.inspect} [#{publish}]" 12 | (@postid += 1).to_s 13 | end 14 | 15 | def editPost(post_id, user, pw, struct, publish) 16 | $stderr.puts "id=#{post_id} user=#{user} pw=#{pw} struct=#{struct.inspect} [#{publish}]" 17 | true 18 | end 19 | 20 | def getPost(post_id, user, pw) 21 | $stderr.puts "get post #{post_id}" 22 | Blog::Post.new(:title => 'hello world', :description => 'first post!') 23 | end 24 | 25 | def getCategories(id, user, pw) 26 | $stderr.puts "categories for #{user}" 27 | cat = Blog::Category.new( 28 | :description => 'Tech', 29 | :htmlUrl => 'http://blog/tech', 30 | :rssUrl => 'http://blog/tech.rss') 31 | [cat] 32 | end 33 | 34 | def getRecentPosts(id, user, pw, num) 35 | $stderr.puts "recent #{num} posts for #{user} on blog #{id}" 36 | post1 = Blog::Post.new( 37 | :title => 'first post!', 38 | :link => 'http://blog.xeraph.org/testOne.html', 39 | :description => 'this is the first post' 40 | ) 41 | post2 = Blog::Post.new( 42 | :title => 'second post!', 43 | :link => 'http://blog.xeraph.org/testTwo.html', 44 | :description => 'this is the second post' 45 | ) 46 | [post1, post2] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /examples/metaWeblog/controllers/xmlrpc_controller.rb: -------------------------------------------------------------------------------- 1 | # 2 | # example controller implementing both blogger and metaWeblog APIs 3 | # in a way that should be compatible with clients supporting both/either. 4 | # 5 | # test by pointing your client at http://URL/xmlrpc/api 6 | # 7 | 8 | require 'meta_weblog_service' 9 | require 'blogger_service' 10 | 11 | class XmlrpcController < ApplicationController 12 | web_service_dispatching_mode :layered 13 | 14 | web_service :metaWeblog, MetaWeblogService.new 15 | web_service :blogger, BloggerService.new 16 | end 17 | -------------------------------------------------------------------------------- /generators/web_service/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | The web service generator creates the controller and API definition for 3 | a web service. 4 | 5 | The generator takes a web service name and a list of API methods as arguments. 6 | The web service name may be given in CamelCase or under_score and should 7 | contain no extra suffixes. To create a web service within a 8 | module, specify the web service name as 'module/webservice'. 9 | 10 | The generator creates a controller class in app/controllers, an API definition 11 | in app/apis, and a functional test suite in test/functional. 12 | 13 | Example: 14 | ./script/generate web_service User add edit list remove 15 | 16 | User web service. 17 | Controller: app/controllers/user_controller.rb 18 | API: app/apis/user_api.rb 19 | Test: test/functional/user_api_test.rb 20 | 21 | Modules Example: 22 | ./script/generate web_service 'api/registration' register renew 23 | 24 | Registration web service. 25 | Controller: app/controllers/api/registration_controller.rb 26 | API: app/apis/api/registration_api.rb 27 | Test: test/functional/api/registration_api_test.rb 28 | 29 | -------------------------------------------------------------------------------- /generators/web_service/templates/api_definition.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Api < ActionWebService::API::Base 2 | <% for method_name in args -%> 3 | api_method :<%= method_name %> 4 | <% end -%> 5 | end 6 | -------------------------------------------------------------------------------- /generators/web_service/templates/controller.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %>Controller < ApplicationController 2 | wsdl_service_name '<%= class_name %>' 3 | <% for method_name in args -%> 4 | 5 | def <%= method_name %> 6 | end 7 | <% end -%> 8 | end 9 | -------------------------------------------------------------------------------- /generators/web_service/templates/functional_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper' 2 | require '<%= file_path %>_controller' 3 | 4 | class <%= class_name %>Controller; def rescue_action(e) raise e end; end 5 | 6 | class <%= class_name %>ControllerApiTest < Test::Unit::TestCase 7 | def setup 8 | @controller = <%= class_name %>Controller.new 9 | @request = ActionController::TestRequest.new 10 | @response = ActionController::TestResponse.new 11 | end 12 | <% for method_name in args -%> 13 | 14 | def test_<%= method_name %> 15 | result = invoke :<%= method_name %> 16 | assert_equal nil, result 17 | end 18 | <% end -%> 19 | end 20 | -------------------------------------------------------------------------------- /generators/web_service/web_service_generator.rb: -------------------------------------------------------------------------------- 1 | class WebServiceGenerator < Rails::Generator::NamedBase 2 | def manifest 3 | record do |m| 4 | # Check for class naming collisions. 5 | m.class_collisions class_path, "#{class_name}Api", "#{class_name}Controller", "#{class_name}ApiTest" 6 | 7 | # API and test directories. 8 | m.directory File.join('app/services', class_path) 9 | m.directory File.join('app/controllers', class_path) 10 | m.directory File.join('test/functional', class_path) 11 | 12 | # API definition, controller, and functional test. 13 | m.template 'api_definition.rb', 14 | File.join('app/services', 15 | class_path, 16 | "#{file_name}_api.rb") 17 | 18 | m.template 'controller.rb', 19 | File.join('app/controllers', 20 | class_path, 21 | "#{file_name}_controller.rb") 22 | 23 | m.template 'functional_test.rb', 24 | File.join('test/functional', 25 | class_path, 26 | "#{file_name}_api_test.rb") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'find' 3 | require 'ftools' 4 | 5 | include Config 6 | 7 | # this was adapted from rdoc's install.rb by way of Log4r 8 | 9 | $sitedir = CONFIG["sitelibdir"] 10 | unless $sitedir 11 | version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] 12 | $libdir = File.join(CONFIG["libdir"], "ruby", version) 13 | $sitedir = $:.find {|x| x =~ /site_ruby/ } 14 | if !$sitedir 15 | $sitedir = File.join($libdir, "site_ruby") 16 | elsif $sitedir !~ Regexp.quote(version) 17 | $sitedir = File.join($sitedir, version) 18 | end 19 | end 20 | 21 | # the acual gruntwork 22 | Dir.chdir("lib") 23 | 24 | Find.find("action_web_service", "action_web_service.rb") { |f| 25 | if f[-3..-1] == ".rb" 26 | File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) 27 | else 28 | File::makedirs(File.join($sitedir, *f.split(/\//))) 29 | end 30 | } 31 | -------------------------------------------------------------------------------- /lib/action_web_service.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # Copyright (C) 2005 Leon Breedt 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | #++ 23 | 24 | begin 25 | require 'active_support' 26 | require 'action_controller' 27 | require 'active_record' 28 | rescue LoadError 29 | require 'rubygems' 30 | gem 'activesupport', '>= 2.3.0' 31 | gem 'actionpack', '>= 2.3.0' 32 | gem 'activerecord', '>= 2.3.0' 33 | end 34 | 35 | $:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/") 36 | 37 | require 'action_web_service/support/class_inheritable_options' 38 | require 'action_web_service/support/signature_types' 39 | require 'action_web_service/base' 40 | require 'action_web_service/client' 41 | require 'action_web_service/invocation' 42 | require 'action_web_service/api' 43 | require 'action_web_service/casting' 44 | require 'action_web_service/struct' 45 | require 'action_web_service/container' 46 | require 'action_web_service/protocol' 47 | require 'action_web_service/dispatcher' 48 | require 'action_web_service/scaffolding' 49 | 50 | ActionWebService::Base.class_eval do 51 | include ActionWebService::Container::Direct 52 | include ActionWebService::Invocation 53 | end 54 | 55 | ActionController::Base.class_eval do 56 | include ActionWebService::Protocol::Discovery 57 | include ActionWebService::Protocol::Soap 58 | include ActionWebService::Protocol::XmlRpc 59 | include ActionWebService::Container::Direct 60 | include ActionWebService::Container::Delegated 61 | include ActionWebService::Container::ActionController 62 | include ActionWebService::Invocation 63 | include ActionWebService::Dispatcher 64 | include ActionWebService::Dispatcher::ActionController 65 | include ActionWebService::Scaffolding 66 | end 67 | -------------------------------------------------------------------------------- /lib/action_web_service/base.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | class ActionWebServiceError < StandardError # :nodoc: 3 | end 4 | 5 | # An Action Web Service object implements a specified API. 6 | # 7 | # Used by controllers operating in _Delegated_ dispatching mode. 8 | # 9 | # ==== Example 10 | # 11 | # class PersonService < ActionWebService::Base 12 | # web_service_api PersonAPI 13 | # 14 | # def find_person(criteria) 15 | # Person.find(:all) [...] 16 | # end 17 | # 18 | # def delete_person(id) 19 | # Person.find_by_id(id).destroy 20 | # end 21 | # end 22 | # 23 | # class PersonAPI < ActionWebService::API::Base 24 | # api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]] 25 | # api_method :delete_person, :expects => [:int] 26 | # end 27 | # 28 | # class SearchCriteria < ActionWebService::Struct 29 | # member :firstname, :string 30 | # member :lastname, :string 31 | # member :email, :string 32 | # end 33 | class Base 34 | # Whether to report exceptions back to the caller in the protocol's exception 35 | # format 36 | class_inheritable_option :web_service_exception_reporting, true 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/action_web_service/casting.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'date' 3 | require 'xmlrpc/datetime' 4 | 5 | module ActionWebService # :nodoc: 6 | module Casting # :nodoc: 7 | class CastingError < ActionWebServiceError # :nodoc: 8 | end 9 | 10 | # Performs casting of arbitrary values into the correct types for the signature 11 | class BaseCaster # :nodoc: 12 | def initialize(api_method) 13 | @api_method = api_method 14 | end 15 | 16 | # Coerces the parameters in +params+ (an Enumerable) into the types 17 | # this method expects 18 | def cast_expects(params) 19 | self.class.cast_expects(@api_method, params) 20 | end 21 | 22 | # Coerces the given +return_value+ into the type returned by this 23 | # method 24 | def cast_returns(return_value) 25 | self.class.cast_returns(@api_method, return_value) 26 | end 27 | 28 | class << self 29 | include ActionWebService::SignatureTypes 30 | 31 | def cast_expects(api_method, params) # :nodoc: 32 | return [] if api_method.expects.nil? 33 | api_method.expects.zip(params).map{ |type, param| cast(param, type) } 34 | end 35 | 36 | def cast_returns(api_method, return_value) # :nodoc: 37 | return nil if api_method.returns.nil? 38 | cast(return_value, api_method.returns[0]) 39 | end 40 | 41 | def cast(value, signature_type) # :nodoc: 42 | return value if signature_type.nil? # signature.length != params.length 43 | return nil if value.nil? 44 | # XMLRPC protocol doesn't support nil values. It uses false instead. 45 | # It should never happen for SOAP. 46 | if signature_type.structured? && value.equal?(false) 47 | return nil 48 | end 49 | unless signature_type.array? || signature_type.structured? 50 | return value if canonical_type(value.class) == signature_type.type 51 | end 52 | if signature_type.array? 53 | unless value.respond_to?(:entries) && !value.is_a?(String) 54 | raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}" 55 | end 56 | value.entries.map do |entry| 57 | cast(entry, signature_type.element_type) 58 | end 59 | elsif signature_type.structured? 60 | cast_to_structured_type(value, signature_type) 61 | elsif !signature_type.custom? 62 | cast_base_type(value, signature_type) 63 | end 64 | end 65 | 66 | def cast_base_type(value, signature_type) # :nodoc: 67 | # This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type 68 | # in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time, 69 | # with the caveat that we won't be able to handle pre-1970 dates that are sent to us. 70 | # 71 | # See http://dev.rubyonrails.com/ticket/2516 72 | value = value.to_time if value.is_a?(XMLRPC::DateTime) 73 | 74 | case signature_type.type 75 | when :int 76 | Integer(value) 77 | when :string 78 | value.to_s 79 | when :base64 80 | if value.is_a?(ActionWebService::Base64) 81 | value 82 | else 83 | ActionWebService::Base64.new(value.to_s) 84 | end 85 | when :bool 86 | return false if value.nil? 87 | return value if value == true || value == false 88 | case value.to_s.downcase 89 | when '1', 'true', 'y', 'yes' 90 | true 91 | when '0', 'false', 'n', 'no' 92 | false 93 | else 94 | raise CastingError, "Don't know how to cast #{value.class} into Boolean" 95 | end 96 | when :float 97 | Float(value) 98 | when :decimal 99 | BigDecimal(value.to_s) 100 | when :time 101 | value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) 102 | if value.kind_of?(Time) 103 | value 104 | elsif value.kind_of?(DateTime) 105 | value.to_time 106 | else 107 | Time.parse(value.to_s) 108 | end 109 | when :date 110 | value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash) 111 | value.kind_of?(Date) ? value : Date.parse(value.to_s) 112 | when :datetime 113 | value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) 114 | value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s) 115 | end 116 | end 117 | 118 | def cast_to_structured_type(value, signature_type) # :nodoc: 119 | obj = nil 120 | # if the canonical classes are the same or if the given value is of 121 | # a type that is derived from the signature_type do not attempt to 122 | # "cast" the value into the signature_type as it's already good to go 123 | obj = ( 124 | canonical_type(value.class) == canonical_type(signature_type.type) or 125 | derived_from?(signature_type.type, value.class) 126 | ) ? value : signature_type.type_class.new 127 | if value.respond_to?(:each_pair) 128 | klass = signature_type.type_class 129 | value.each_pair do |name, val| 130 | type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil 131 | val = cast(val, type) if type 132 | # See http://dev.rubyonrails.com/ticket/3567 133 | val = val.to_time if val.is_a?(XMLRPC::DateTime) 134 | obj.__send__("#{name}=", val) if obj.respond_to?(name) 135 | end 136 | elsif value.respond_to?(:attributes) 137 | signature_type.each_member do |name, type| 138 | val = value.__send__(name) 139 | obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name) 140 | end 141 | else 142 | raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}" 143 | end 144 | obj 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/action_web_service/client.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service/client/base' 2 | require 'action_web_service/client/soap_client' 3 | require 'action_web_service/client/xmlrpc_client' 4 | -------------------------------------------------------------------------------- /lib/action_web_service/client/base.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Client # :nodoc: 3 | class ClientError < StandardError # :nodoc: 4 | end 5 | 6 | class Base # :nodoc: 7 | def initialize(api, endpoint_uri) 8 | @api = api 9 | @endpoint_uri = endpoint_uri 10 | end 11 | 12 | def method_missing(name, *args) # :nodoc: 13 | call_name = method_name(name) 14 | return super(name, *args) if call_name.nil? 15 | self.perform_invocation(call_name, args) 16 | end 17 | 18 | private 19 | def method_name(name) 20 | if @api.has_api_method?(name.to_sym) 21 | name.to_s 22 | elsif @api.has_public_api_method?(name.to_s) 23 | @api.api_method_name(name.to_s).to_s 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/action_web_service/client/soap_client.rb: -------------------------------------------------------------------------------- 1 | require 'soap/rpc/driver' 2 | require 'uri' 3 | 4 | module ActionWebService # :nodoc: 5 | module Client # :nodoc: 6 | 7 | # Implements SOAP client support (using RPC encoding for the messages). 8 | # 9 | # ==== Example Usage 10 | # 11 | # class PersonAPI < ActionWebService::API::Base 12 | # api_method :find_all, :returns => [[Person]] 13 | # end 14 | # 15 | # soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") 16 | # persons = soap_client.find_all 17 | # 18 | class Soap < Base 19 | # provides access to the underlying soap driver 20 | attr_reader :driver 21 | 22 | # Creates a new web service client using the SOAP RPC protocol. 23 | # 24 | # +api+ must be an ActionWebService::API::Base derivative, and 25 | # +endpoint_uri+ must point at the relevant URL to which protocol requests 26 | # will be sent with HTTP POST. 27 | # 28 | # Valid options: 29 | # [:namespace] If the remote server has used a custom namespace to 30 | # declare its custom types, you can specify it here. This would 31 | # be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute 32 | # in .NET, for example. 33 | # [:driver_options] If you want to supply any custom SOAP RPC driver 34 | # options, you can provide them as a Hash here 35 | # 36 | # The :driver_options option can be used to configure the backend SOAP 37 | # RPC driver. An example of configuring the SOAP backend to do 38 | # client-certificate authenticated SSL connections to the server: 39 | # 40 | # opts = {} 41 | # opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER' 42 | # opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path 43 | # opts['protocol.http.ssl_config.client_key'] = client_key_file_path 44 | # opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path 45 | # client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts) 46 | def initialize(api, endpoint_uri, options={}) 47 | super(api, endpoint_uri) 48 | @namespace = options[:namespace] || 'urn:ActionWebService' 49 | @driver_options = options[:driver_options] || {} 50 | @protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace 51 | @soap_action_base = options[:soap_action_base] 52 | @soap_action_base ||= URI.parse(endpoint_uri).path 53 | @driver = create_soap_rpc_driver(api, endpoint_uri) 54 | @driver_options.each do |name, value| 55 | @driver.options[name.to_s] = value 56 | end 57 | end 58 | 59 | protected 60 | def perform_invocation(method_name, args) 61 | method = @api.api_methods[method_name.to_sym] 62 | args = method.cast_expects(args.dup) rescue args 63 | return_value = @driver.send(method_name, *args) 64 | method.cast_returns(return_value.dup) rescue return_value 65 | end 66 | 67 | def soap_action(method_name) 68 | "#{@soap_action_base}/#{method_name}" 69 | end 70 | 71 | private 72 | def create_soap_rpc_driver(api, endpoint_uri) 73 | @protocol.register_api(api) 74 | driver = SoapDriver.new(endpoint_uri, nil) 75 | driver.mapping_registry = @protocol.marshaler.registry 76 | api.api_methods.each do |name, method| 77 | qname = XSD::QName.new(@namespace, method.public_name) 78 | action = soap_action(method.public_name) 79 | expects = method.expects 80 | returns = method.returns 81 | param_def = [] 82 | if expects 83 | expects.each do |type| 84 | type_binding = @protocol.marshaler.lookup_type(type) 85 | if SOAP::Version >= "1.5.5" 86 | param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]] 87 | else 88 | param_def << ['in', type.name, type_binding.mapping] 89 | end 90 | end 91 | end 92 | if returns 93 | type_binding = @protocol.marshaler.lookup_type(returns[0]) 94 | if SOAP::Version >= "1.5.5" 95 | param_def << ['retval', 'return', [type_binding.type.type_class.to_s]] 96 | else 97 | param_def << ['retval', 'return', type_binding.mapping] 98 | end 99 | end 100 | driver.add_method(qname, action, method.name.to_s, param_def) 101 | end 102 | driver 103 | end 104 | 105 | class SoapDriver < SOAP::RPC::Driver # :nodoc: 106 | def add_method(qname, soapaction, name, param_def) 107 | @proxy.add_rpc_method(qname, soapaction, name, param_def) 108 | add_rpc_method_interface(name, param_def) 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/action_web_service/client/xmlrpc_client.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'xmlrpc/client' 3 | 4 | module ActionWebService # :nodoc: 5 | module Client # :nodoc: 6 | 7 | # Implements XML-RPC client support 8 | # 9 | # ==== Example Usage 10 | # 11 | # class BloggerAPI < ActionWebService::API::Base 12 | # inflect_names false 13 | # api_method :getRecentPosts, :returns => [[Blog::Post]] 14 | # end 15 | # 16 | # blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger") 17 | # posts = blog.getRecentPosts 18 | class XmlRpc < Base 19 | 20 | # Creates a new web service client using the XML-RPC protocol. 21 | # 22 | # +api+ must be an ActionWebService::API::Base derivative, and 23 | # +endpoint_uri+ must point at the relevant URL to which protocol requests 24 | # will be sent with HTTP POST. 25 | # 26 | # Valid options: 27 | # [:handler_name] If the remote server defines its services inside special 28 | # handler (the Blogger API uses a "blogger" handler name for example), 29 | # provide it here, or your method calls will fail 30 | def initialize(api, endpoint_uri, options={}) 31 | @api = api 32 | @handler_name = options[:handler_name] 33 | @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new 34 | @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout]) 35 | end 36 | 37 | protected 38 | def perform_invocation(method_name, args) 39 | method = @api.api_methods[method_name.to_sym] 40 | if method.expects && method.expects.length != args.length 41 | raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})") 42 | end 43 | args = method.cast_expects(args.dup) rescue args 44 | if method.expects 45 | method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) } 46 | end 47 | ok, return_value = @client.call2(public_name(method_name), *args) 48 | return (method.cast_returns(return_value.dup) rescue return_value) if ok 49 | raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}") 50 | end 51 | 52 | def public_name(method_name) 53 | public_name = @api.public_api_method_name(method_name) 54 | @handler_name ? "#{@handler_name}.#{public_name}" : public_name 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/action_web_service/container.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service/container/direct_container' 2 | require 'action_web_service/container/delegated_container' 3 | require 'action_web_service/container/action_controller_container' 4 | -------------------------------------------------------------------------------- /lib/action_web_service/container/action_controller_container.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Container # :nodoc: 3 | module ActionController # :nodoc: 4 | def self.included(base) # :nodoc: 5 | class << base 6 | include ClassMethods 7 | alias_method_chain :inherited, :api 8 | alias_method_chain :web_service_api, :require 9 | end 10 | end 11 | 12 | module ClassMethods 13 | # Creates a client for accessing remote web services, using the 14 | # given +protocol+ to communicate with the +endpoint_uri+. 15 | # 16 | # ==== Example 17 | # 18 | # class MyController < ActionController::Base 19 | # web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger' 20 | # end 21 | # 22 | # In this example, a protected method named blogger will 23 | # now exist on the controller, and calling it will return the 24 | # XML-RPC client object for working with that remote service. 25 | # 26 | # +options+ is the set of protocol client specific options (see 27 | # a protocol client class for details). 28 | # 29 | # If your API definition does not exist on the load path with the 30 | # correct rules for it to be found using +name+, you can pass in 31 | # the API definition class via +options+, using a key of :api 32 | def web_client_api(name, protocol, endpoint_uri, options={}) 33 | unless method_defined?(name) 34 | api_klass = options.delete(:api) || require_web_service_api(name) 35 | class_eval do 36 | define_method(name) do 37 | create_web_service_client(api_klass, protocol, endpoint_uri, options) 38 | end 39 | protected name 40 | end 41 | end 42 | end 43 | 44 | def web_service_api_with_require(definition=nil) # :nodoc: 45 | return web_service_api_without_require if definition.nil? 46 | case definition 47 | when String, Symbol 48 | klass = require_web_service_api(definition) 49 | else 50 | klass = definition 51 | end 52 | web_service_api_without_require(klass) 53 | end 54 | 55 | def require_web_service_api(name) # :nodoc: 56 | case name 57 | when String, Symbol 58 | file_name = name.to_s.underscore + "_api" 59 | class_name = file_name.camelize 60 | class_names = [class_name, class_name.sub(/Api$/, 'API')] 61 | begin 62 | require_dependency(file_name) 63 | rescue LoadError => load_error 64 | requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1] 65 | msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}" 66 | raise LoadError.new(msg).copy_blame!(load_error) 67 | end 68 | klass = nil 69 | class_names.each do |name| 70 | klass = name.constantize rescue nil 71 | break unless klass.nil? 72 | end 73 | unless klass 74 | raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found") 75 | end 76 | klass 77 | else 78 | raise(ArgumentError, "expected String or Symbol argument") 79 | end 80 | end 81 | 82 | private 83 | def inherited_with_api(child) 84 | inherited_without_api(child) 85 | begin child.web_service_api(child.controller_path) 86 | rescue MissingSourceFile => e 87 | raise unless e.is_missing?("apis/#{child.controller_path}_api") 88 | end 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/action_web_service/container/delegated_container.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Container # :nodoc: 3 | module Delegated # :nodoc: 4 | class ContainerError < ActionWebServiceError # :nodoc: 5 | end 6 | 7 | def self.included(base) # :nodoc: 8 | base.extend(ClassMethods) 9 | base.send(:include, ActionWebService::Container::Delegated::InstanceMethods) 10 | end 11 | 12 | module ClassMethods 13 | # Declares a web service that will provide access to the API of the given 14 | # +object+. +object+ must be an ActionWebService::Base derivative. 15 | # 16 | # Web service object creation can either be _immediate_, where the object 17 | # instance is given at class definition time, or _deferred_, where 18 | # object instantiation is delayed until request time. 19 | # 20 | # ==== Immediate web service object example 21 | # 22 | # class ApiController < ApplicationController 23 | # web_service_dispatching_mode :delegated 24 | # 25 | # web_service :person, PersonService.new 26 | # end 27 | # 28 | # For deferred instantiation, a block should be given instead of an 29 | # object instance. This block will be executed in controller instance 30 | # context, so it can rely on controller instance variables being present. 31 | # 32 | # ==== Deferred web service object example 33 | # 34 | # class ApiController < ApplicationController 35 | # web_service_dispatching_mode :delegated 36 | # 37 | # web_service(:person) { PersonService.new(request.env) } 38 | # end 39 | def web_service(name, object=nil, &block) 40 | if (object && block_given?) || (object.nil? && block.nil?) 41 | raise(ContainerError, "either service, or a block must be given") 42 | end 43 | name = name.to_sym 44 | if block_given? 45 | info = { name => { :block => block } } 46 | else 47 | info = { name => { :object => object } } 48 | end 49 | write_inheritable_hash("web_services", info) 50 | call_web_service_definition_callbacks(self, name, info) 51 | end 52 | 53 | # Whether this service contains a service with the given +name+ 54 | def has_web_service?(name) 55 | web_services.has_key?(name.to_sym) 56 | end 57 | 58 | def web_services # :nodoc: 59 | read_inheritable_attribute("web_services") || {} 60 | end 61 | 62 | def add_web_service_definition_callback(&block) # :nodoc: 63 | write_inheritable_array("web_service_definition_callbacks", [block]) 64 | end 65 | 66 | private 67 | def call_web_service_definition_callbacks(container_class, web_service_name, service_info) 68 | (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block| 69 | block.call(container_class, web_service_name, service_info) 70 | end 71 | end 72 | end 73 | 74 | module InstanceMethods # :nodoc: 75 | def web_service_object(web_service_name) 76 | info = self.class.web_services[web_service_name.to_sym] 77 | unless info 78 | raise(ContainerError, "no such web service '#{web_service_name}'") 79 | end 80 | service = info[:block] 81 | service ? self.instance_eval(&service) : info[:object] 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/action_web_service/container/direct_container.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Container # :nodoc: 3 | module Direct # :nodoc: 4 | class ContainerError < ActionWebServiceError # :nodoc: 5 | end 6 | 7 | def self.included(base) # :nodoc: 8 | base.extend(ClassMethods) 9 | end 10 | 11 | module ClassMethods 12 | # Attaches ActionWebService API +definition+ to the calling class. 13 | # 14 | # Action Controllers can have a default associated API, removing the need 15 | # to call this method if you follow the Action Web Service naming conventions. 16 | # 17 | # A controller with a class name of GoogleSearchController will 18 | # implicitly load app/apis/google_search_api.rb, and expect the 19 | # API definition class to be named GoogleSearchAPI or 20 | # GoogleSearchApi. 21 | # 22 | # ==== Service class example 23 | # 24 | # class MyService < ActionWebService::Base 25 | # web_service_api MyAPI 26 | # end 27 | # 28 | # class MyAPI < ActionWebService::API::Base 29 | # ... 30 | # end 31 | # 32 | # ==== Controller class example 33 | # 34 | # class MyController < ActionController::Base 35 | # web_service_api MyAPI 36 | # end 37 | # 38 | # class MyAPI < ActionWebService::API::Base 39 | # ... 40 | # end 41 | def web_service_api(definition=nil) 42 | if definition.nil? 43 | read_inheritable_attribute("web_service_api") 44 | else 45 | if definition.is_a?(Symbol) 46 | raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller") 47 | end 48 | unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base) 49 | raise(ContainerError, "#{definition.to_s} is not a valid API definition") 50 | end 51 | write_inheritable_attribute("web_service_api", definition) 52 | call_web_service_api_callbacks(self, definition) 53 | end 54 | end 55 | 56 | def add_web_service_api_callback(&block) # :nodoc: 57 | write_inheritable_array("web_service_api_callbacks", [block]) 58 | end 59 | 60 | private 61 | def call_web_service_api_callbacks(container_class, definition) 62 | (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block| 63 | block.call(container_class, definition) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/action_web_service/dispatcher.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service/dispatcher/abstract' 2 | require 'action_web_service/dispatcher/action_controller_dispatcher' 3 | -------------------------------------------------------------------------------- /lib/action_web_service/dispatcher/abstract.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | 3 | module ActionWebService # :nodoc: 4 | module Dispatcher # :nodoc: 5 | class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc: 6 | def initialize(*args) 7 | super 8 | set_backtrace(caller) 9 | end 10 | end 11 | 12 | def self.included(base) # :nodoc: 13 | base.class_inheritable_option(:web_service_dispatching_mode, :direct) 14 | base.class_inheritable_option(:web_service_exception_reporting, true) 15 | base.send(:include, ActionWebService::Dispatcher::InstanceMethods) 16 | end 17 | 18 | module InstanceMethods # :nodoc: 19 | private 20 | def invoke_web_service_request(protocol_request) 21 | invocation = web_service_invocation(protocol_request) 22 | if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol) 23 | xmlrpc_multicall_invoke(invocation) 24 | else 25 | web_service_invoke(invocation) 26 | end 27 | end 28 | 29 | def web_service_direct_invoke(invocation) 30 | @method_params = invocation.method_ordered_params 31 | arity = method(invocation.api_method.name).arity rescue 0 32 | if arity < 0 || arity > 0 33 | params = @method_params 34 | else 35 | params = [] 36 | end 37 | web_service_filtered_invoke(invocation, params) 38 | end 39 | 40 | def web_service_delegated_invoke(invocation) 41 | web_service_filtered_invoke(invocation, invocation.method_ordered_params) 42 | end 43 | 44 | def web_service_filtered_invoke(invocation, params) 45 | cancellation_reason = nil 46 | return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x| 47 | cancellation_reason = x 48 | end 49 | if cancellation_reason 50 | raise(DispatcherError, "request canceled: #{cancellation_reason}") 51 | end 52 | return_value 53 | end 54 | 55 | def web_service_invoke(invocation) 56 | case web_service_dispatching_mode 57 | when :direct 58 | return_value = web_service_direct_invoke(invocation) 59 | when :delegated, :layered 60 | return_value = web_service_delegated_invoke(invocation) 61 | end 62 | web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value) 63 | end 64 | 65 | def xmlrpc_multicall_invoke(invocations) 66 | responses = [] 67 | invocations.each do |invocation| 68 | if invocation.is_a?(Hash) 69 | responses << [invocation, nil] 70 | next 71 | end 72 | begin 73 | case web_service_dispatching_mode 74 | when :direct 75 | return_value = web_service_direct_invoke(invocation) 76 | when :delegated, :layered 77 | return_value = web_service_delegated_invoke(invocation) 78 | end 79 | api_method = invocation.api_method 80 | if invocation.api.has_api_method?(api_method.name) 81 | response_type = (api_method.returns ? api_method.returns[0] : nil) 82 | return_value = api_method.cast_returns(return_value) 83 | else 84 | response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) 85 | end 86 | responses << [return_value, response_type] 87 | rescue Exception => e 88 | responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil] 89 | end 90 | end 91 | invocation = invocations[0] 92 | invocation.protocol.encode_multicall_response(responses, invocation.protocol_options) 93 | end 94 | 95 | def web_service_invocation(request, level = 0) 96 | public_method_name = request.method_name 97 | invocation = Invocation.new 98 | invocation.protocol = request.protocol 99 | invocation.protocol_options = request.protocol_options 100 | invocation.service_name = request.service_name 101 | if web_service_dispatching_mode == :layered 102 | case invocation.protocol 103 | when Protocol::Soap::SoapProtocol 104 | soap_action = request.protocol_options[:soap_action] 105 | if soap_action && soap_action =~ /^\/\w+\/(\w+)\// 106 | invocation.service_name = $1 107 | end 108 | when Protocol::XmlRpc::XmlRpcProtocol 109 | if request.method_name =~ /^([^\.]+)\.(.*)$/ 110 | public_method_name = $2 111 | invocation.service_name = $1 112 | end 113 | end 114 | end 115 | if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol 116 | if public_method_name == 'multicall' && invocation.service_name == 'system' 117 | if level > 0 118 | raise(DispatcherError, "Recursive system.multicall invocations not allowed") 119 | end 120 | multicall = request.method_params.dup 121 | unless multicall.is_a?(Array) && multicall[0].is_a?(Array) 122 | raise(DispatcherError, "Malformed multicall (expected array of Hash elements)") 123 | end 124 | multicall = multicall[0] 125 | return multicall.map do |item| 126 | raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash) 127 | raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName') 128 | method_name = item['methodName'] 129 | params = item.has_key?('params') ? item['params'] : [] 130 | multicall_request = request.dup 131 | multicall_request.method_name = method_name 132 | multicall_request.method_params = params 133 | begin 134 | web_service_invocation(multicall_request, level + 1) 135 | rescue Exception => e 136 | {'faultCode' => 4, 'faultMessage' => e.message} 137 | end 138 | end 139 | end 140 | end 141 | case web_service_dispatching_mode 142 | when :direct 143 | invocation.api = self.class.web_service_api 144 | invocation.service = self 145 | when :delegated, :layered 146 | invocation.service = web_service_object(invocation.service_name) 147 | invocation.api = invocation.service.class.web_service_api 148 | end 149 | if invocation.api.nil? 150 | raise(DispatcherError, "no API attached to #{invocation.service.class}") 151 | end 152 | invocation.protocol.register_api(invocation.api) 153 | request.api = invocation.api 154 | if invocation.api.has_public_api_method?(public_method_name) 155 | invocation.api_method = invocation.api.public_api_method_instance(public_method_name) 156 | else 157 | if invocation.api.default_api_method.nil? 158 | raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}") 159 | else 160 | invocation.api_method = invocation.api.default_api_method_instance 161 | end 162 | end 163 | if invocation.service.nil? 164 | raise(DispatcherError, "no service available for service name #{invocation.service_name}") 165 | end 166 | unless invocation.service.respond_to?(invocation.api_method.name) 167 | raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})") 168 | end 169 | request.api_method = invocation.api_method 170 | begin 171 | invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup) 172 | rescue 173 | logger.warn "Casting of method parameters failed" unless logger.nil? 174 | invocation.method_ordered_params = request.method_params 175 | end 176 | request.method_params = invocation.method_ordered_params 177 | invocation.method_named_params = {} 178 | invocation.api_method.param_names.inject(0) do |m, n| 179 | invocation.method_named_params[n] = invocation.method_ordered_params[m] 180 | m + 1 181 | end 182 | invocation 183 | end 184 | 185 | def web_service_create_response(protocol, protocol_options, api, api_method, return_value) 186 | if api.has_api_method?(api_method.name) 187 | return_type = api_method.returns ? api_method.returns[0] : nil 188 | return_value = api_method.cast_returns(return_value) 189 | else 190 | return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) 191 | end 192 | protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options) 193 | end 194 | 195 | class Invocation # :nodoc: 196 | attr_accessor :protocol 197 | attr_accessor :protocol_options 198 | attr_accessor :service_name 199 | attr_accessor :api 200 | attr_accessor :api_method 201 | attr_accessor :method_ordered_params 202 | attr_accessor :method_named_params 203 | attr_accessor :service 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/action_web_service/invocation.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Invocation # :nodoc: 3 | class InvocationError < ActionWebService::ActionWebServiceError # :nodoc: 4 | end 5 | 6 | def self.included(base) # :nodoc: 7 | base.extend(ClassMethods) 8 | base.send(:include, ActionWebService::Invocation::InstanceMethods) 9 | end 10 | 11 | # Invocation interceptors provide a means to execute custom code before 12 | # and after method invocations on ActionWebService::Base objects. 13 | # 14 | # When running in _Direct_ dispatching mode, ActionController filters 15 | # should be used for this functionality instead. 16 | # 17 | # The semantics of invocation interceptors are the same as ActionController 18 | # filters, and accept the same parameters and options. 19 | # 20 | # A _before_ interceptor can also cancel execution by returning +false+, 21 | # or returning a [false, "cancel reason"] array if it wishes to supply 22 | # a reason for canceling the request. 23 | # 24 | # === Example 25 | # 26 | # class CustomService < ActionWebService::Base 27 | # before_invocation :intercept_add, :only => [:add] 28 | # 29 | # def add(a, b) 30 | # a + b 31 | # end 32 | # 33 | # private 34 | # def intercept_add 35 | # return [false, "permission denied"] # cancel it 36 | # end 37 | # end 38 | # 39 | # Options: 40 | # [:except] A list of methods for which the interceptor will NOT be called 41 | # [:only] A list of methods for which the interceptor WILL be called 42 | module ClassMethods 43 | # Appends the given +interceptors+ to be called 44 | # _before_ method invocation. 45 | def append_before_invocation(*interceptors, &block) 46 | conditions = extract_conditions!(interceptors) 47 | interceptors << block if block_given? 48 | add_interception_conditions(interceptors, conditions) 49 | append_interceptors_to_chain("before", interceptors) 50 | end 51 | 52 | # Prepends the given +interceptors+ to be called 53 | # _before_ method invocation. 54 | def prepend_before_invocation(*interceptors, &block) 55 | conditions = extract_conditions!(interceptors) 56 | interceptors << block if block_given? 57 | add_interception_conditions(interceptors, conditions) 58 | prepend_interceptors_to_chain("before", interceptors) 59 | end 60 | 61 | alias :before_invocation :append_before_invocation 62 | 63 | # Appends the given +interceptors+ to be called 64 | # _after_ method invocation. 65 | def append_after_invocation(*interceptors, &block) 66 | conditions = extract_conditions!(interceptors) 67 | interceptors << block if block_given? 68 | add_interception_conditions(interceptors, conditions) 69 | append_interceptors_to_chain("after", interceptors) 70 | end 71 | 72 | # Prepends the given +interceptors+ to be called 73 | # _after_ method invocation. 74 | def prepend_after_invocation(*interceptors, &block) 75 | conditions = extract_conditions!(interceptors) 76 | interceptors << block if block_given? 77 | add_interception_conditions(interceptors, conditions) 78 | prepend_interceptors_to_chain("after", interceptors) 79 | end 80 | 81 | alias :after_invocation :append_after_invocation 82 | 83 | def before_invocation_interceptors # :nodoc: 84 | read_inheritable_attribute("before_invocation_interceptors") 85 | end 86 | 87 | def after_invocation_interceptors # :nodoc: 88 | read_inheritable_attribute("after_invocation_interceptors") 89 | end 90 | 91 | def included_intercepted_methods # :nodoc: 92 | read_inheritable_attribute("included_intercepted_methods") || {} 93 | end 94 | 95 | def excluded_intercepted_methods # :nodoc: 96 | read_inheritable_attribute("excluded_intercepted_methods") || {} 97 | end 98 | 99 | private 100 | def append_interceptors_to_chain(condition, interceptors) 101 | write_inheritable_array("#{condition}_invocation_interceptors", interceptors) 102 | end 103 | 104 | def prepend_interceptors_to_chain(condition, interceptors) 105 | interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors") 106 | write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors) 107 | end 108 | 109 | def extract_conditions!(interceptors) 110 | return nil unless interceptors.last.is_a? Hash 111 | interceptors.pop 112 | end 113 | 114 | def add_interception_conditions(interceptors, conditions) 115 | return unless conditions 116 | included, excluded = conditions[:only], conditions[:except] 117 | write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included 118 | write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded 119 | end 120 | 121 | def condition_hash(interceptors, *methods) 122 | interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})} 123 | end 124 | end 125 | 126 | module InstanceMethods # :nodoc: 127 | def self.included(base) 128 | base.class_eval do 129 | alias_method_chain :perform_invocation, :interception 130 | end 131 | end 132 | 133 | def perform_invocation_with_interception(method_name, params, &block) 134 | return if before_invocation(method_name, params, &block) == false 135 | return_value = perform_invocation_without_interception(method_name, params) 136 | after_invocation(method_name, params, return_value) 137 | return_value 138 | end 139 | 140 | def perform_invocation(method_name, params) 141 | send(method_name, *params) 142 | end 143 | 144 | def before_invocation(name, args, &block) 145 | call_interceptors(self.class.before_invocation_interceptors, [name, args], &block) 146 | end 147 | 148 | def after_invocation(name, args, result) 149 | call_interceptors(self.class.after_invocation_interceptors, [name, args, result]) 150 | end 151 | 152 | private 153 | 154 | def call_interceptors(interceptors, interceptor_args, &block) 155 | if interceptors and not interceptors.empty? 156 | interceptors.each do |interceptor| 157 | next if method_exempted?(interceptor, interceptor_args[0].to_s) 158 | result = case 159 | when interceptor.is_a?(Symbol) 160 | self.send(interceptor, *interceptor_args) 161 | when interceptor_block?(interceptor) 162 | interceptor.call(self, *interceptor_args) 163 | when interceptor_class?(interceptor) 164 | interceptor.intercept(self, *interceptor_args) 165 | else 166 | raise( 167 | InvocationError, 168 | "Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method" 169 | ) 170 | end 171 | reason = nil 172 | if result.is_a?(Array) 173 | reason = result[1] if result[1] 174 | result = result[0] 175 | end 176 | if result == false 177 | block.call(reason) if block && reason 178 | return false 179 | end 180 | end 181 | end 182 | end 183 | 184 | def interceptor_block?(interceptor) 185 | interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1) 186 | end 187 | 188 | def interceptor_class?(interceptor) 189 | interceptor.respond_to?("intercept") 190 | end 191 | 192 | def method_exempted?(interceptor, method_name) 193 | case 194 | when self.class.included_intercepted_methods[interceptor] 195 | !self.class.included_intercepted_methods[interceptor].include?(method_name) 196 | when self.class.excluded_intercepted_methods[interceptor] 197 | self.class.excluded_intercepted_methods[interceptor].include?(method_name) 198 | end 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service/protocol/abstract' 2 | require 'action_web_service/protocol/discovery' 3 | require 'action_web_service/protocol/soap_protocol' 4 | require 'action_web_service/protocol/xmlrpc_protocol' 5 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol/abstract.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Protocol # :nodoc: 3 | class ProtocolError < ActionWebServiceError # :nodoc: 4 | end 5 | 6 | class AbstractProtocol # :nodoc: 7 | def setup(controller) 8 | end 9 | 10 | def decode_action_pack_request(action_pack_request) 11 | end 12 | 13 | def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) 14 | klass = options[:request_class] || SimpleActionPackRequest 15 | request = klass.new({}) 16 | request.request_parameters['action'] = service_name.to_s 17 | request.env['RAW_POST_DATA'] = raw_body 18 | request.env['REQUEST_METHOD'] = 'POST' 19 | request.env['HTTP_CONTENT_TYPE'] = 'text/xml' 20 | request 21 | end 22 | 23 | def decode_request(raw_request, service_name, protocol_options={}) 24 | end 25 | 26 | def encode_request(method_name, params, param_types) 27 | end 28 | 29 | def decode_response(raw_response) 30 | end 31 | 32 | def encode_response(method_name, return_value, return_type, protocol_options={}) 33 | end 34 | 35 | def protocol_client(api, protocol_name, endpoint_uri, options) 36 | end 37 | 38 | def register_api(api) 39 | end 40 | end 41 | 42 | class Request # :nodoc: 43 | attr :protocol 44 | attr_accessor :method_name 45 | attr_accessor :method_params 46 | attr :service_name 47 | attr_accessor :api 48 | attr_accessor :api_method 49 | attr :protocol_options 50 | 51 | def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil) 52 | @protocol = protocol 53 | @method_name = method_name 54 | @method_params = method_params 55 | @service_name = service_name 56 | @api = api 57 | @api_method = api_method 58 | @protocol_options = protocol_options || {} 59 | end 60 | end 61 | 62 | class Response # :nodoc: 63 | attr :body 64 | attr :content_type 65 | attr :return_value 66 | 67 | def initialize(body, content_type, return_value) 68 | @body = body 69 | @content_type = content_type 70 | @return_value = return_value 71 | end 72 | end 73 | 74 | class SimpleActionPackRequest < ActionController::Request # :nodoc: 75 | def initialize(env = {}) 76 | @env = env 77 | @qparams = {} 78 | @rparams = {} 79 | @cookies = {} 80 | reset_session 81 | end 82 | 83 | def query_parameters 84 | @qparams 85 | end 86 | 87 | def request_parameters 88 | @rparams 89 | end 90 | 91 | def env 92 | @env 93 | end 94 | 95 | def host 96 | '' 97 | end 98 | 99 | def cookies 100 | @cookies 101 | end 102 | 103 | def session 104 | @session 105 | end 106 | 107 | def reset_session 108 | @session = {} 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol/discovery.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | module Protocol # :nodoc: 3 | module Discovery # :nodoc: 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods) 7 | end 8 | 9 | module ClassMethods # :nodoc: 10 | def register_protocol(klass) 11 | write_inheritable_array("web_service_protocols", [klass]) 12 | end 13 | end 14 | 15 | module InstanceMethods # :nodoc: 16 | private 17 | def discover_web_service_request(action_pack_request) 18 | (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| 19 | protocol = protocol.create(self) 20 | request = protocol.decode_action_pack_request(action_pack_request) 21 | return request unless request.nil? 22 | end 23 | nil 24 | end 25 | 26 | def create_web_service_client(api, protocol_name, endpoint_uri, options) 27 | (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| 28 | protocol = protocol.create(self) 29 | client = protocol.protocol_client(api, protocol_name, endpoint_uri, options) 30 | return client unless client.nil? 31 | end 32 | nil 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol/soap_protocol.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service/protocol/soap_protocol/marshaler' 2 | require 'soap/streamHandler' 3 | require 'action_web_service/client/soap_client' 4 | 5 | module ActionWebService # :nodoc: 6 | module API # :nodoc: 7 | class Base # :nodoc: 8 | def self.soap_client(endpoint_uri, options={}) 9 | ActionWebService::Client::Soap.new self, endpoint_uri, options 10 | end 11 | end 12 | end 13 | 14 | module Protocol # :nodoc: 15 | module Soap # :nodoc: 16 | def self.included(base) 17 | base.register_protocol(SoapProtocol) 18 | base.class_inheritable_option(:wsdl_service_name) 19 | base.class_inheritable_option(:wsdl_namespace) 20 | end 21 | 22 | class SoapProtocol < AbstractProtocol # :nodoc: 23 | AWSEncoding = 'UTF-8' 24 | XSDEncoding = 'UTF8' 25 | 26 | attr :marshaler 27 | 28 | def initialize(namespace=nil) 29 | namespace ||= 'urn:ActionWebService' 30 | @marshaler = SoapMarshaler.new namespace 31 | end 32 | 33 | def self.create(controller) 34 | SoapProtocol.new(controller.wsdl_namespace) 35 | end 36 | 37 | def decode_action_pack_request(action_pack_request) 38 | return nil unless soap_action = has_valid_soap_action?(action_pack_request) 39 | service_name = action_pack_request.parameters['action'] 40 | input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE']) 41 | protocol_options = { 42 | :soap_action => soap_action, 43 | :charset => input_encoding 44 | } 45 | decode_request(action_pack_request.raw_post, service_name, protocol_options) 46 | end 47 | 48 | def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) 49 | request = super 50 | request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name] 51 | request 52 | end 53 | 54 | def decode_request(raw_request, service_name, protocol_options={}) 55 | envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset]) 56 | unless envelope 57 | raise ProtocolError, "Failed to parse SOAP request message" 58 | end 59 | request = envelope.body.request 60 | method_name = request.elename.name 61 | params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) } 62 | Request.new(self, method_name, params, service_name, nil, nil, protocol_options) 63 | end 64 | 65 | def encode_request(method_name, params, param_types) 66 | param_types.each{ |type| marshaler.register_type(type) } if param_types 67 | qname = XSD::QName.new(marshaler.namespace, method_name) 68 | param_def = [] 69 | if param_types 70 | params = param_types.zip(params).map do |type, param| 71 | param_def << ['in', type.name, marshaler.lookup_type(type).mapping] 72 | [type.name, marshaler.ruby_to_soap(param)] 73 | end 74 | else 75 | params = [] 76 | end 77 | request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def) 78 | request.set_param(params) 79 | envelope = create_soap_envelope(request) 80 | SOAP::Processor.marshal(envelope) 81 | end 82 | 83 | def decode_response(raw_response) 84 | envelope = SOAP::Processor.unmarshal(raw_response) 85 | unless envelope 86 | raise ProtocolError, "Failed to parse SOAP request message" 87 | end 88 | method_name = envelope.body.request.elename.name 89 | return_value = envelope.body.response 90 | return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil? 91 | [method_name, return_value] 92 | end 93 | 94 | def encode_response(method_name, return_value, return_type, protocol_options={}) 95 | if return_type 96 | return_binding = marshaler.register_type(return_type) 97 | marshaler.annotate_arrays(return_binding, return_value) 98 | end 99 | qname = XSD::QName.new(marshaler.namespace, method_name) 100 | if return_value.nil? 101 | response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) 102 | else 103 | if return_value.is_a?(Exception) 104 | detail = SOAP::Mapping::SOAPException.new(return_value) 105 | response = SOAP::SOAPFault.new( 106 | SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']), 107 | SOAP::SOAPString.new(return_value.to_s), 108 | SOAP::SOAPString.new(self.class.name), 109 | marshaler.ruby_to_soap(detail)) 110 | else 111 | if return_type 112 | param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]] 113 | response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) 114 | response.retval = marshaler.ruby_to_soap(return_value) 115 | else 116 | response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) 117 | end 118 | end 119 | end 120 | envelope = create_soap_envelope(response) 121 | 122 | # FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only 123 | # reads target encoding from the XSD::Charset.encoding variable. 124 | # This is required to ensure $KCODE strings are converted 125 | # correctly to UTF-8 for any values of $KCODE. 126 | previous_encoding = XSD::Charset.encoding 127 | XSD::Charset.encoding = XSDEncoding 128 | response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding) 129 | XSD::Charset.encoding = previous_encoding 130 | 131 | Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value) 132 | end 133 | 134 | def protocol_client(api, protocol_name, endpoint_uri, options={}) 135 | return nil unless protocol_name == :soap 136 | ActionWebService::Client::Soap.new(api, endpoint_uri, options) 137 | end 138 | 139 | def register_api(api) 140 | api.api_methods.each do |name, method| 141 | method.expects.each{ |type| marshaler.register_type(type) } if method.expects 142 | method.returns.each{ |type| marshaler.register_type(type) } if method.returns 143 | end 144 | end 145 | 146 | private 147 | def has_valid_soap_action?(request) 148 | return nil unless request.method == :post 149 | soap_action = request.env['HTTP_SOAPACTION'] 150 | return nil unless soap_action 151 | soap_action = soap_action.dup 152 | soap_action.gsub!(/^"/, '') 153 | soap_action.gsub!(/"$/, '') 154 | soap_action.strip! 155 | return nil if soap_action.empty? 156 | soap_action 157 | end 158 | 159 | def create_soap_envelope(body) 160 | header = SOAP::SOAPHeader.new 161 | body = SOAP::SOAPBody.new(body) 162 | SOAP::SOAPEnvelope.new(header, body) 163 | end 164 | 165 | def parse_charset(content_type) 166 | return AWSEncoding if content_type.nil? 167 | if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type 168 | $1 169 | else 170 | AWSEncoding 171 | end 172 | end 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol/soap_protocol/marshaler.rb: -------------------------------------------------------------------------------- 1 | require 'soap/mapping' 2 | 3 | # hack to improve the .Net interoperability 4 | class SOAP::Mapping::Object 5 | def each_pair 6 | self.__xmlele.each { |n, v| yield n.name, v.to_s } 7 | end 8 | end 9 | 10 | module ActionWebService 11 | module Protocol 12 | module Soap 13 | # Workaround for SOAP4R return values changing 14 | class Registry < SOAP::Mapping::Registry 15 | if SOAP::Version >= "1.5.4" 16 | def find_mapped_soap_class(obj_class) 17 | return @map.instance_eval { @obj2soap[obj_class][0] } 18 | end 19 | 20 | def find_mapped_obj_class(soap_class) 21 | return @map.instance_eval { @soap2obj[soap_class][0] } 22 | end 23 | end 24 | end 25 | 26 | class SoapMarshaler 27 | attr :namespace 28 | attr :registry 29 | 30 | def initialize(namespace=nil) 31 | @namespace = namespace || 'urn:ActionWebService' 32 | @registry = Registry.new 33 | @type2binding = {} 34 | register_static_factories 35 | end 36 | 37 | def soap_to_ruby(obj) 38 | SOAP::Mapping.soap2obj(obj, @registry) 39 | end 40 | 41 | def ruby_to_soap(obj) 42 | soap = SOAP::Mapping.obj2soap(obj, @registry) 43 | soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY 44 | soap 45 | end 46 | 47 | def register_type(type) 48 | return @type2binding[type] if @type2binding.has_key?(type) 49 | 50 | if type.array? 51 | array_mapping = @registry.find_mapped_soap_class(Array) 52 | qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array') 53 | element_type_binding = register_type(type.element_type) 54 | @type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding) 55 | elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil) 56 | qname = mapping[2] ? mapping[2][:type] : nil 57 | qname ||= soap_base_type_name(mapping[0]) 58 | @type2binding[type] = SoapBinding.new(self, qname, type, mapping) 59 | else 60 | qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name)) 61 | @registry.add(type.type_class, 62 | SOAP::SOAPStruct, 63 | typed_struct_factory(type.type_class), 64 | { :type => qname }) 65 | mapping = @registry.find_mapped_soap_class(type.type_class) 66 | @type2binding[type] = SoapBinding.new(self, qname, type, mapping) 67 | end 68 | 69 | if type.structured? 70 | type.each_member do |m_name, m_type| 71 | register_type(m_type) 72 | end 73 | end 74 | 75 | @type2binding[type] 76 | end 77 | alias :lookup_type :register_type 78 | 79 | def annotate_arrays(binding, value) 80 | if value.nil? 81 | return 82 | elsif binding.type.array? 83 | mark_typed_array(value, binding.element_binding.qname) 84 | if binding.element_binding.type.custom? 85 | value.each do |element| 86 | annotate_arrays(binding.element_binding, element) 87 | end 88 | end 89 | elsif binding.type.structured? 90 | binding.type.each_member do |name, type| 91 | member_binding = register_type(type) 92 | member_value = value.respond_to?('[]') ? value[name] : value.send(name) 93 | annotate_arrays(member_binding, member_value) if type.custom? 94 | end 95 | end 96 | end 97 | 98 | private 99 | def typed_struct_factory(type_class) 100 | if Object.const_defined?('ActiveRecord') 101 | if type_class.ancestors.include?(ActiveRecord::Base) 102 | qname = XSD::QName.new(@namespace, soap_type_name(type_class.name)) 103 | type_class.instance_variable_set('@qname', qname) 104 | return SoapActiveRecordStructFactory.new 105 | end 106 | end 107 | SOAP::Mapping::Registry::TypedStructFactory 108 | end 109 | 110 | def mark_typed_array(array, qname) 111 | (class << array; self; end).class_eval do 112 | define_method(:arytype) do 113 | qname 114 | end 115 | end 116 | end 117 | 118 | def soap_base_type_name(type) 119 | xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' } 120 | xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type 121 | end 122 | 123 | def soap_type_name(type_name) 124 | type_name.gsub(/::/, '..') 125 | end 126 | 127 | def register_static_factories 128 | @registry.add(ActionWebService::Base64, SOAP::SOAPBase64, SoapBase64Factory.new, nil) 129 | mapping = @registry.find_mapped_soap_class(ActionWebService::Base64) 130 | @type2binding[ActionWebService::Base64] = 131 | SoapBinding.new(self, SOAP::SOAPBase64::Type, ActionWebService::Base64, mapping) 132 | @registry.add(Array, SOAP::SOAPArray, SoapTypedArrayFactory.new, nil) 133 | @registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true}) 134 | end 135 | end 136 | 137 | class SoapBinding 138 | attr :qname 139 | attr :type 140 | attr :mapping 141 | attr :element_binding 142 | 143 | def initialize(marshaler, qname, type, mapping, element_binding=nil) 144 | @marshaler = marshaler 145 | @qname = qname 146 | @type = type 147 | @mapping = mapping 148 | @element_binding = element_binding 149 | end 150 | 151 | def type_name 152 | @type.custom? ? @qname.name : nil 153 | end 154 | 155 | def qualified_type_name(ns=nil) 156 | if @type.custom? 157 | "#{ns ? ns : @qname.namespace}:#{@qname.name}" 158 | else 159 | ns = XSD::NS.new 160 | ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) 161 | ns.assign(SOAP::EncodingNamespace, "soapenc") 162 | xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')} 163 | return ns.name(XSD::AnyTypeName) unless xsd_klass 164 | ns.name(xsd_klass.const_get('Type')) 165 | end 166 | end 167 | 168 | def eql?(other) 169 | @qname == other.qname 170 | end 171 | alias :== :eql? 172 | 173 | def hash 174 | @qname.hash 175 | end 176 | end 177 | 178 | class SoapActiveRecordStructFactory < SOAP::Mapping::Factory 179 | def obj2soap(soap_class, obj, info, map) 180 | unless obj.is_a?(ActiveRecord::Base) 181 | return nil 182 | end 183 | soap_obj = soap_class.new(obj.class.instance_variable_get('@qname')) 184 | obj.class.columns.each do |column| 185 | key = column.name.to_s 186 | value = obj.send(key) 187 | soap_obj[key] = SOAP::Mapping._obj2soap(value, map) 188 | end 189 | soap_obj 190 | end 191 | 192 | def soap2obj(obj_class, node, info, map) 193 | unless node.type == obj_class.instance_variable_get('@qname') 194 | return false 195 | end 196 | obj = obj_class.new 197 | node.each do |key, value| 198 | obj[key] = value.data 199 | end 200 | obj.instance_variable_set('@new_record', false) 201 | return true, obj 202 | end 203 | end 204 | 205 | class SoapTypedArrayFactory < SOAP::Mapping::Factory 206 | def obj2soap(soap_class, obj, info, map) 207 | unless obj.respond_to?(:arytype) 208 | return nil 209 | end 210 | soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype) 211 | mark_marshalled_obj(obj, soap_obj) 212 | obj.each do |item| 213 | child = SOAP::Mapping._obj2soap(item, map) 214 | soap_obj.add(child) 215 | end 216 | soap_obj 217 | end 218 | 219 | def soap2obj(obj_class, node, info, map) 220 | return false 221 | end 222 | end 223 | 224 | class SoapBase64Factory < SOAP::Mapping::Factory 225 | def obj2soap(soap_class, obj, info, map) 226 | unless obj.is_a?(ActionWebService::Base64) 227 | return nil 228 | end 229 | return soap_class.new(obj) 230 | end 231 | 232 | def soap2obj(obj_class, node, info, map) 233 | unless node.type == SOAP::SOAPBase64::Type 234 | return false 235 | end 236 | return true, obj_class.new(node.string) 237 | end 238 | end 239 | 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/action_web_service/protocol/xmlrpc_protocol.rb: -------------------------------------------------------------------------------- 1 | require 'xmlrpc/marshal' 2 | require 'action_web_service/client/xmlrpc_client' 3 | 4 | module XMLRPC # :nodoc: 5 | class FaultException # :nodoc: 6 | alias :message :faultString 7 | end 8 | 9 | class Create 10 | def wrong_type(value) 11 | if BigDecimal === value 12 | [true, value.to_f] 13 | else 14 | false 15 | end 16 | end 17 | end 18 | end 19 | 20 | module ActionWebService # :nodoc: 21 | module API # :nodoc: 22 | class Base # :nodoc: 23 | def self.xmlrpc_client(endpoint_uri, options={}) 24 | ActionWebService::Client::XmlRpc.new self, endpoint_uri, options 25 | end 26 | end 27 | end 28 | 29 | module Protocol # :nodoc: 30 | module XmlRpc # :nodoc: 31 | def self.included(base) 32 | base.register_protocol(XmlRpcProtocol) 33 | end 34 | 35 | class XmlRpcProtocol < AbstractProtocol # :nodoc: 36 | def self.create(controller) 37 | XmlRpcProtocol.new 38 | end 39 | 40 | def decode_action_pack_request(action_pack_request) 41 | service_name = action_pack_request.parameters['action'] 42 | decode_request(action_pack_request.raw_post, service_name) 43 | end 44 | 45 | def decode_request(raw_request, service_name) 46 | method_name, params = XMLRPC::Marshal.load_call(raw_request) 47 | Request.new(self, method_name, params, service_name) 48 | rescue 49 | return nil 50 | end 51 | 52 | def encode_request(method_name, params, param_types) 53 | if param_types 54 | params = params.dup 55 | param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) } 56 | end 57 | XMLRPC::Marshal.dump_call(method_name, *params) 58 | end 59 | 60 | def decode_response(raw_response) 61 | [nil, XMLRPC::Marshal.load_response(raw_response)] 62 | end 63 | 64 | def encode_response(method_name, return_value, return_type, protocol_options={}) 65 | if return_value && return_type 66 | return_value = value_to_xmlrpc_wire_format(return_value, return_type) 67 | end 68 | return_value = false if return_value.nil? 69 | raw_response = XMLRPC::Marshal.dump_response(return_value) 70 | Response.new(raw_response, 'text/xml', return_value) 71 | end 72 | 73 | def encode_multicall_response(responses, protocol_options={}) 74 | result = responses.map do |return_value, return_type| 75 | if return_value && return_type 76 | return_value = value_to_xmlrpc_wire_format(return_value, return_type) 77 | return_value = [return_value] unless return_value.nil? 78 | end 79 | return_value = false if return_value.nil? 80 | return_value 81 | end 82 | raw_response = XMLRPC::Marshal.dump_response(result) 83 | Response.new(raw_response, 'text/xml', result) 84 | end 85 | 86 | def protocol_client(api, protocol_name, endpoint_uri, options={}) 87 | return nil unless protocol_name == :xmlrpc 88 | ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options) 89 | end 90 | 91 | def value_to_xmlrpc_wire_format(value, value_type) 92 | if value_type.array? 93 | value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) } 94 | else 95 | if value.is_a?(ActionWebService::Struct) 96 | struct = {} 97 | value.class.members.each do |name, type| 98 | member_value = value[name] 99 | next if member_value.nil? 100 | struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type) 101 | end 102 | struct 103 | elsif value.is_a?(ActiveRecord::Base) 104 | struct = {} 105 | value.attributes.each do |key, member_value| 106 | next if member_value.nil? 107 | struct[key.to_s] = member_value 108 | end 109 | struct 110 | elsif value.is_a?(ActionWebService::Base64) 111 | XMLRPC::Base64.new(value) 112 | elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException) 113 | XMLRPC::FaultException.new(2, value.message) 114 | else 115 | value 116 | end 117 | end 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/action_web_service/scaffolding.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'pathname' 3 | 4 | module ActionWebService 5 | module Scaffolding # :nodoc: 6 | class ScaffoldingError < ActionWebServiceError # :nodoc: 7 | end 8 | 9 | def self.included(base) 10 | base.extend(ClassMethods) 11 | end 12 | 13 | # Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The 14 | # generated scaffold actions have default views to let you enter the method parameters and view the 15 | # results. 16 | # 17 | # Example: 18 | # 19 | # class ApiController < ActionController 20 | # web_service_scaffold :invoke 21 | # end 22 | # 23 | # This example generates an +invoke+ action in the +ApiController+ that you can navigate to from 24 | # your browser, select the API method, enter its parameters, and perform the invocation. 25 | # 26 | # If you want to customize the default views, create the following views in "app/views": 27 | # 28 | # * action_name/methods.html.erb 29 | # * action_name/parameters.html.erb 30 | # * action_name/result.html.erb 31 | # * action_name/layout.html.erb 32 | # 33 | # Where action_name is the name of the action you gave to ClassMethods#web_service_scaffold. 34 | # 35 | # You can use the default views in RAILS_DIR/lib/action_web_service/templates/scaffolds as 36 | # a guide. 37 | module ClassMethods 38 | # Generates web service invocation scaffolding for the current controller. The given action name 39 | # can then be used as the entry point for invoking API methods from a web browser. 40 | def web_service_scaffold(action_name) 41 | add_template_helper(Helpers) 42 | module_eval <<-"end_eval", __FILE__, __LINE__ + 1 43 | def #{action_name} 44 | if request.method == :get 45 | setup_invocation_assigns 46 | render_invocation_scaffold 'methods' 47 | end 48 | end 49 | 50 | def #{action_name}_method_params 51 | if request.method == :get 52 | setup_invocation_assigns 53 | render_invocation_scaffold 'parameters' 54 | end 55 | end 56 | 57 | def #{action_name}_submit 58 | if request.method == :post 59 | setup_invocation_assigns 60 | protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap 61 | case protocol_name 62 | when :soap 63 | @protocol = Protocol::Soap::SoapProtocol.create(self) 64 | when :xmlrpc 65 | @protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self) 66 | end 67 | bm = Benchmark.measure do 68 | @protocol.register_api(@scaffold_service.api) 69 | post_params = params['method_params'] ? params['method_params'].dup : nil 70 | params = [] 71 | @scaffold_method.expects.each_with_index do |spec, i| 72 | params << post_params[i.to_s] 73 | end if @scaffold_method.expects 74 | params = @scaffold_method.cast_expects(params) 75 | method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name) 76 | @method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects) 77 | new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml) 78 | prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name) 79 | self.request = new_request 80 | if @scaffold_container.dispatching_mode != :direct 81 | request.parameters['action'] = @scaffold_service.name 82 | end 83 | dispatch_web_service_request 84 | @method_response_xml = response.body 85 | method_name, obj = @protocol.decode_response(@method_response_xml) 86 | return if handle_invocation_exception(obj) 87 | @method_return_value = @scaffold_method.cast_returns(obj) 88 | end 89 | @method_elapsed = bm.real 90 | reset_invocation_response 91 | render_invocation_scaffold 'result' 92 | end 93 | end 94 | 95 | private 96 | def setup_invocation_assigns 97 | @scaffold_class = self.class 98 | @scaffold_action_name = "#{action_name}" 99 | @scaffold_container = WebServiceModel::Container.new(self) 100 | if params['service'] && params['method'] 101 | @scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] } 102 | @scaffold_method = @scaffold_service.api_methods[params['method']] 103 | end 104 | end 105 | 106 | def render_invocation_scaffold(action) 107 | customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}" 108 | default_template = scaffold_path(action) 109 | begin 110 | content = @template.render(:file => customized_template) 111 | rescue ActionView::MissingTemplate 112 | content = @template.render(:file => default_template) 113 | end 114 | @template.instance_variable_set("@content_for_layout", content) 115 | if self.active_layout.nil? 116 | render :file => scaffold_path("layout") 117 | else 118 | render :file => self.active_layout, :use_full_path => true 119 | end 120 | end 121 | 122 | def scaffold_path(template_name) 123 | File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".html.erb" 124 | end 125 | 126 | def reset_invocation_response 127 | erase_render_results 128 | response.instance_variable_set :@header, Rack::Utils::HeaderHash.new(::ActionController::Response::DEFAULT_HEADERS.merge("cookie" => [])) 129 | end 130 | 131 | def public_method_name(service_name, method_name) 132 | if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) 133 | service_name + '.' + method_name 134 | else 135 | method_name 136 | end 137 | end 138 | 139 | def prepare_request(new_request, service_name, method_name) 140 | new_request.parameters.update(request.parameters) 141 | request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) } 142 | if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol) 143 | new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}" 144 | end 145 | end 146 | 147 | def handle_invocation_exception(obj) 148 | exception = nil 149 | if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception) 150 | exception = obj.detail.cause 151 | elsif obj.is_a?(XMLRPC::FaultException) 152 | exception = obj 153 | end 154 | return unless exception 155 | reset_invocation_response 156 | rescue_action(exception) 157 | true 158 | end 159 | end_eval 160 | end 161 | end 162 | 163 | module Helpers # :nodoc: 164 | def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false) 165 | if type.array? 166 | return content_tag('em', "Typed array input fields not supported yet (#{type.name})") 167 | end 168 | if type.structured? 169 | return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured 170 | parameters = "" 171 | type.each_member do |member_name, member_type| 172 | label = method_parameter_label(member_name, member_type) 173 | nested_content = method_parameter_input_fields( 174 | method, 175 | member_type, 176 | "#{field_name_base}[#{idx}][#{member_name}]", 177 | idx, 178 | true) 179 | if member_type.custom? 180 | parameters << content_tag('li', label) 181 | parameters << content_tag('ul', nested_content) 182 | else 183 | parameters << content_tag('li', label + ' ' + nested_content) 184 | end 185 | end 186 | content_tag('ul', parameters) 187 | else 188 | # If the data source was structured previously we already have the index set 189 | field_name_base = "#{field_name_base}[#{idx}]" unless was_structured 190 | 191 | case type.type 192 | when :int 193 | text_field_tag "#{field_name_base}" 194 | when :string 195 | text_field_tag "#{field_name_base}" 196 | when :base64 197 | text_area_tag "#{field_name_base}", nil, :size => "40x5" 198 | when :bool 199 | radio_button_tag("#{field_name_base}", "true") + " True" + 200 | radio_button_tag("#{field_name_base}", "false") + "False" 201 | when :float 202 | text_field_tag "#{field_name_base}" 203 | when :time, :datetime 204 | time = Time.now 205 | i = 0 206 | %w|year month day hour minute second|.map do |name| 207 | i += 1 208 | send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) 209 | end.join 210 | when :date 211 | date = Date.today 212 | i = 0 213 | %w|year month day|.map do |name| 214 | i += 1 215 | send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) 216 | end.join 217 | end 218 | end 219 | end 220 | 221 | def method_parameter_label(name, type) 222 | name.to_s.capitalize + ' (' + type.human_name(false) + ')' 223 | end 224 | 225 | def service_method_list(service) 226 | action = @scaffold_action_name + '_method_params' 227 | methods = service.api_methods_full.map do |desc, name| 228 | content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name)) 229 | end 230 | content_tag("ul", methods.join("\n")) 231 | end 232 | end 233 | 234 | module WebServiceModel # :nodoc: 235 | class Container # :nodoc: 236 | attr :services 237 | attr :dispatching_mode 238 | 239 | def initialize(real_container) 240 | @real_container = real_container 241 | @dispatching_mode = @real_container.class.web_service_dispatching_mode 242 | @services = [] 243 | if @dispatching_mode == :direct 244 | @services << Service.new(@real_container.controller_name, @real_container) 245 | else 246 | @real_container.class.web_services.each do |name, obj| 247 | @services << Service.new(name, @real_container.instance_eval{ web_service_object(name) }) 248 | end 249 | end 250 | end 251 | end 252 | 253 | class Service # :nodoc: 254 | attr :name 255 | attr :object 256 | attr :api 257 | attr :api_methods 258 | attr :api_methods_full 259 | 260 | def initialize(name, real_service) 261 | @name = name.to_s 262 | @object = real_service 263 | @api = @object.class.web_service_api 264 | if @api.nil? 265 | raise ScaffoldingError, "No web service API attached to #{object.class}" 266 | end 267 | @api_methods = {} 268 | @api_methods_full = [] 269 | @api.api_methods.each do |name, method| 270 | @api_methods[method.public_name.to_s] = method 271 | @api_methods_full << [method.to_s, method.public_name.to_s] 272 | end 273 | end 274 | 275 | def to_s 276 | self.name.camelize 277 | end 278 | end 279 | end 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /lib/action_web_service/struct.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService 2 | # To send structured types across the wire, derive from ActionWebService::Struct, 3 | # and use +member+ to declare structure members. 4 | # 5 | # ActionWebService::Struct should be used in method signatures when you want to accept or return 6 | # structured types that have no Active Record model class representations, or you don't 7 | # want to expose your entire Active Record model to remote callers. 8 | # 9 | # === Example 10 | # 11 | # class Person < ActionWebService::Struct 12 | # member :id, :int 13 | # member :firstnames, [:string] 14 | # member :lastname, :string 15 | # member :email, :string 16 | # end 17 | # person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe') 18 | # 19 | # Active Record model classes are already implicitly supported in method 20 | # signatures. 21 | class Struct 22 | # If a Hash is given as argument to an ActionWebService::Struct constructor, 23 | # it can contain initial values for the structure member. 24 | def initialize(values={}) 25 | if values.is_a?(Hash) 26 | values.map{|k,v| __send__('%s=' % k.to_s, v)} 27 | end 28 | end 29 | 30 | # The member with the given name 31 | def [](name) 32 | send(name.to_s) 33 | end 34 | 35 | # Iterates through each member 36 | def each_pair(&block) 37 | self.class.members.each do |name, type| 38 | yield name, self.__send__(name) 39 | end 40 | end 41 | 42 | class << self 43 | # Creates a structure member with the specified +name+ and +type+. Generates 44 | # accessor methods for reading and writing the member value. 45 | def member(name, type) 46 | name = name.to_sym 47 | type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0) 48 | write_inheritable_hash("struct_members", name => type) 49 | class_eval <<-END 50 | def #{name}; @#{name}; end 51 | def #{name}=(value); @#{name} = value; end 52 | END 53 | end 54 | 55 | def members # :nodoc: 56 | read_inheritable_attribute("struct_members") || {} 57 | end 58 | 59 | def member_type(name) # :nodoc: 60 | members[name.to_sym] 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/action_web_service/support/class_inheritable_options.rb: -------------------------------------------------------------------------------- 1 | class Class # :nodoc: 2 | def class_inheritable_option(sym, default_value=nil) 3 | write_inheritable_attribute sym, default_value 4 | class_eval <<-EOS 5 | def self.#{sym}(value=nil) 6 | if !value.nil? 7 | write_inheritable_attribute(:#{sym}, value) 8 | else 9 | read_inheritable_attribute(:#{sym}) 10 | end 11 | end 12 | 13 | def self.#{sym}=(value) 14 | write_inheritable_attribute(:#{sym}, value) 15 | end 16 | 17 | def #{sym} 18 | self.class.#{sym} 19 | end 20 | 21 | def #{sym}=(value) 22 | self.class.#{sym} = value 23 | end 24 | EOS 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/action_web_service/support/signature_types.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService # :nodoc: 2 | # Action Web Service supports the following base types in a signature: 3 | # 4 | # [:int] Represents an integer value, will be cast to an integer using Integer(value) 5 | # [:string] Represents a string value, will be cast to an string using the to_s method on an object 6 | # [:base64] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller 7 | # [:bool] Represents a boolean value, whatever is passed will be cast to boolean (true, '1', 'true', 'y', 'yes' are taken to represent true; false, '0', 'false', 'n', 'no' and nil represent false) 8 | # [:float] Represents a floating point value, will be cast to a float using Float(value) 9 | # [:time] Represents a timestamp, will be cast to a Time object 10 | # [:datetime] Represents a timestamp, will be cast to a DateTime object 11 | # [:date] Represents a date, will be cast to a Date object 12 | # 13 | # For structured types, you'll need to pass in the Class objects of 14 | # ActionWebService::Struct and ActiveRecord::Base derivatives. 15 | module SignatureTypes 16 | def canonical_signature(signature) # :nodoc: 17 | return nil if signature.nil? 18 | unless signature.is_a?(Array) 19 | raise(ActionWebServiceError, "Expected signature to be an Array") 20 | end 21 | i = -1 22 | signature.map{ |spec| canonical_signature_entry(spec, i += 1) } 23 | end 24 | 25 | def canonical_signature_entry(spec, i) # :nodoc: 26 | orig_spec = spec 27 | name = "param#{i}" 28 | if spec.is_a?(Hash) 29 | name, spec = spec.keys.first, spec.values.first 30 | end 31 | type = spec 32 | if spec.is_a?(Array) 33 | ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name) 34 | else 35 | type = canonical_type(type) 36 | if type.is_a?(Symbol) 37 | BaseType.new(orig_spec, type, name) 38 | else 39 | StructuredType.new(orig_spec, type, name) 40 | end 41 | end 42 | end 43 | 44 | def canonical_type(type) # :nodoc: 45 | type_name = symbol_name(type) || class_to_type_name(type) 46 | type = type_name || type 47 | return canonical_type_name(type) if type.is_a?(Symbol) 48 | type 49 | end 50 | 51 | def canonical_type_name(name) # :nodoc: 52 | name = name.to_sym 53 | case name 54 | when :int, :integer, :fixnum, :bignum 55 | :int 56 | when :string, :text 57 | :string 58 | when :base64, :binary 59 | :base64 60 | when :bool, :boolean 61 | :bool 62 | when :float, :double 63 | :float 64 | when :decimal 65 | :decimal 66 | when :time, :timestamp 67 | :time 68 | when :datetime 69 | :datetime 70 | when :date 71 | :date 72 | else 73 | raise(TypeError, "#{name} is not a valid base type") 74 | end 75 | end 76 | 77 | def canonical_type_class(type) # :nodoc: 78 | type = canonical_type(type) 79 | type.is_a?(Symbol) ? type_name_to_class(type) : type 80 | end 81 | 82 | def symbol_name(name) # :nodoc: 83 | return name.to_sym if name.is_a?(Symbol) || name.is_a?(String) 84 | nil 85 | end 86 | 87 | def class_to_type_name(klass) # :nodoc: 88 | klass = klass.class unless klass.is_a?(Class) 89 | if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass) 90 | :int 91 | elsif klass == String 92 | :string 93 | elsif klass == Base64 94 | :base64 95 | elsif klass == TrueClass || klass == FalseClass 96 | :bool 97 | elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass) 98 | :float 99 | elsif klass == Time 100 | :time 101 | elsif klass == DateTime 102 | :datetime 103 | elsif klass == Date 104 | :date 105 | else 106 | nil 107 | end 108 | end 109 | 110 | def type_name_to_class(name) # :nodoc: 111 | case canonical_type_name(name) 112 | when :int 113 | Integer 114 | when :string 115 | String 116 | when :base64 117 | Base64 118 | when :bool 119 | TrueClass 120 | when :float 121 | Float 122 | when :decimal 123 | BigDecimal 124 | when :time 125 | Time 126 | when :date 127 | Date 128 | when :datetime 129 | DateTime 130 | else 131 | nil 132 | end 133 | end 134 | 135 | def derived_from?(ancestor, child) # :nodoc: 136 | child.ancestors.include?(ancestor) 137 | end 138 | 139 | module_function :type_name_to_class 140 | module_function :class_to_type_name 141 | module_function :symbol_name 142 | module_function :canonical_type_class 143 | module_function :canonical_type_name 144 | module_function :canonical_type 145 | module_function :canonical_signature_entry 146 | module_function :canonical_signature 147 | module_function :derived_from? 148 | end 149 | 150 | class BaseType # :nodoc: 151 | include SignatureTypes 152 | 153 | attr :spec 154 | attr :type 155 | attr :type_class 156 | attr :name 157 | 158 | def initialize(spec, type, name) 159 | @spec = spec 160 | @type = canonical_type(type) 161 | @type_class = canonical_type_class(@type) 162 | @name = name 163 | end 164 | 165 | def custom? 166 | false 167 | end 168 | 169 | def array? 170 | false 171 | end 172 | 173 | def structured? 174 | false 175 | end 176 | 177 | def human_name(show_name=true) 178 | type_type = array? ? element_type.type.to_s : self.type.to_s 179 | str = array? ? (type_type + '[]') : type_type 180 | show_name ? (str + " " + name.to_s) : str 181 | end 182 | end 183 | 184 | class ArrayType < BaseType # :nodoc: 185 | attr :element_type 186 | 187 | def initialize(spec, element_type, name) 188 | super(spec, Array, name) 189 | @element_type = element_type 190 | end 191 | 192 | def custom? 193 | true 194 | end 195 | 196 | def array? 197 | true 198 | end 199 | end 200 | 201 | class StructuredType < BaseType # :nodoc: 202 | def each_member 203 | if @type_class.respond_to?(:members) 204 | @type_class.members.each do |name, type| 205 | yield name, type 206 | end 207 | elsif @type_class.respond_to?(:columns) 208 | i = -1 209 | @type_class.columns.each do |column| 210 | yield column.name, canonical_signature_entry(column.type, i += 1) 211 | end 212 | end 213 | end 214 | 215 | def custom? 216 | true 217 | end 218 | 219 | def structured? 220 | true 221 | end 222 | end 223 | 224 | class Base64 < String # :nodoc: 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /lib/action_web_service/templates/scaffolds/layout.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= @scaffold_class.wsdl_service_name %> Web Service 4 | 59 | 60 | 61 | 62 | <%= @content_for_layout %> 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /lib/action_web_service/templates/scaffolds/methods.html.erb: -------------------------------------------------------------------------------- 1 | <% @scaffold_container.services.each do |service| %> 2 | 3 |

API Methods for <%= service %>

4 | <%= service_method_list(service) %> 5 | 6 | <% end %> 7 | -------------------------------------------------------------------------------- /lib/action_web_service/templates/scaffolds/parameters.html.erb: -------------------------------------------------------------------------------- 1 |

Method Invocation Details for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

2 | 3 | <% form_tag(:action => @scaffold_action_name + '_submit') do -%> 4 | <%= hidden_field_tag "service", @scaffold_service.name %> 5 | <%= hidden_field_tag "method", @scaffold_method.public_name %> 6 | 7 |

8 |
9 | <%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], params['protocol']) %> 10 |

11 | 12 | <% if @scaffold_method.expects %> 13 | 14 | Method Parameters:
15 | <% @scaffold_method.expects.each_with_index do |type, i| %> 16 |

17 |
18 | <%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %> 19 |

20 | <% end %> 21 | 22 | <% end %> 23 | 24 | <%= submit_tag "Invoke" %> 25 | <% end -%> 26 | 27 |

28 | <%= link_to "Back", :action => @scaffold_action_name %> 29 |

30 | -------------------------------------------------------------------------------- /lib/action_web_service/templates/scaffolds/result.html.erb: -------------------------------------------------------------------------------- 1 |

Method Invocation Result for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

2 | 3 |

4 | Invocation took <%= '%f' % @method_elapsed %> seconds 5 |

6 | 7 |

8 | Return Value:
9 |

10 | <%= h @method_return_value.inspect %>
11 | 
12 |

13 | 14 |

15 | Request XML:
16 |

17 | <%= h @method_request_xml %>
18 | 
19 |

20 | 21 |

22 | Response XML:
23 |

24 | <%= h @method_response_xml %>
25 | 
26 |

27 | 28 |

29 | <%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %> 30 |

31 | -------------------------------------------------------------------------------- /lib/action_web_service/test_invoke.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | module Test # :nodoc: 4 | module Unit # :nodoc: 5 | class TestCase # :nodoc: 6 | private 7 | # invoke the specified API method 8 | def invoke_direct(method_name, *args) 9 | prepare_request('api', 'api', method_name, *args) 10 | @controller.process(@request, @response) 11 | decode_rpc_response 12 | end 13 | alias_method :invoke, :invoke_direct 14 | 15 | # invoke the specified API method on the specified service 16 | def invoke_delegated(service_name, method_name, *args) 17 | prepare_request(service_name.to_s, service_name, method_name, *args) 18 | @controller.process(@request, @response) 19 | decode_rpc_response 20 | end 21 | 22 | # invoke the specified layered API method on the correct service 23 | def invoke_layered(service_name, method_name, *args) 24 | prepare_request('api', service_name, method_name, *args) 25 | @controller.process(@request, @response) 26 | decode_rpc_response 27 | end 28 | 29 | # ---------------------- internal --------------------------- 30 | 31 | def prepare_request(action, service_name, api_method_name, *args) 32 | @request.recycle! 33 | @request.request_parameters['action'] = action 34 | @request.env['REQUEST_METHOD'] = 'POST' 35 | @request.env['HTTP_CONTENT_TYPE'] = 'text/xml' 36 | @request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args) 37 | case protocol 38 | when ActionWebService::Protocol::Soap::SoapProtocol 39 | soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}" 40 | @request.env['HTTP_SOAPACTION'] = soap_action 41 | when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol 42 | @request.env.delete('HTTP_SOAPACTION') 43 | end 44 | end 45 | 46 | def encode_rpc_call(service_name, api_method_name, *args) 47 | case @controller.web_service_dispatching_mode 48 | when :direct 49 | api = @controller.class.web_service_api 50 | when :delegated, :layered 51 | api = @controller.web_service_object(service_name.to_sym).class.web_service_api 52 | end 53 | protocol.register_api(api) 54 | method = api.api_methods[api_method_name.to_sym] 55 | raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" if method && method.expects && args.length != method.expects.length 56 | protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects) 57 | end 58 | 59 | def decode_rpc_response 60 | public_method_name, return_value = protocol.decode_response(@response.body) 61 | exception = is_exception?(return_value) 62 | raise exception if exception 63 | return_value 64 | end 65 | 66 | def public_method_name(service_name, api_method_name) 67 | public_name = service_api(service_name).public_api_method_name(api_method_name) 68 | if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) 69 | '%s.%s' % [service_name.to_s, public_name] 70 | else 71 | public_name 72 | end 73 | end 74 | 75 | def service_api(service_name) 76 | case @controller.web_service_dispatching_mode 77 | when :direct 78 | @controller.class.web_service_api 79 | when :delegated, :layered 80 | @controller.web_service_object(service_name.to_sym).class.web_service_api 81 | end 82 | end 83 | 84 | def protocol 85 | if @protocol.nil? 86 | @protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) 87 | else 88 | case @protocol 89 | when :xmlrpc 90 | @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller) 91 | when :soap 92 | @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) 93 | else 94 | @protocol 95 | end 96 | end 97 | end 98 | 99 | def is_exception?(obj) 100 | case protocol 101 | when :soap, ActionWebService::Protocol::Soap::SoapProtocol 102 | (obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ 103 | obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil 104 | when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol 105 | obj.is_a?(XMLRPC::FaultException) ? obj : nil 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/action_web_service/version.rb: -------------------------------------------------------------------------------- 1 | module ActionWebService 2 | module VERSION #:nodoc: 3 | MAJOR = 2 4 | MINOR = 3 5 | TINY = 2 6 | 7 | STRING = [MAJOR, MINOR, TINY].join('.') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/actionwebservice.rb: -------------------------------------------------------------------------------- 1 | require 'action_web_service' 2 | -------------------------------------------------------------------------------- /test/abstract_client.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | require 'webrick' 3 | require 'webrick/log' 4 | require 'singleton' 5 | 6 | module ClientTest 7 | class Person < ActionWebService::Struct 8 | member :firstnames, [:string] 9 | member :lastname, :string 10 | 11 | def ==(other) 12 | firstnames == other.firstnames && lastname == other.lastname 13 | end 14 | end 15 | 16 | class Inner < ActionWebService::Struct 17 | member :name, :string 18 | end 19 | 20 | class Outer < ActionWebService::Struct 21 | member :name, :string 22 | member :inner, Inner 23 | end 24 | 25 | class User < ActiveRecord::Base 26 | end 27 | 28 | module Accounting 29 | class User < ActiveRecord::Base 30 | end 31 | end 32 | 33 | class WithModel < ActionWebService::Struct 34 | member :user, User 35 | member :users, [User] 36 | end 37 | 38 | class WithMultiDimArray < ActionWebService::Struct 39 | member :pref, [[:string]] 40 | end 41 | 42 | class API < ActionWebService::API::Base 43 | api_method :void 44 | api_method :normal, :expects => [:int, :int], :returns => [:int] 45 | api_method :array_return, :returns => [[Person]] 46 | api_method :struct_pass, :expects => [[Person]], :returns => [:bool] 47 | api_method :nil_struct_return, :returns => [Person] 48 | api_method :inner_nil, :returns => [Outer] 49 | api_method :client_container, :returns => [:int] 50 | api_method :named_parameters, :expects => [{:key=>:string}, {:id=>:int}] 51 | api_method :thrower 52 | api_method :user_return, :returns => [User] 53 | api_method :with_model_return, :returns => [WithModel] 54 | api_method :scoped_model_return, :returns => [Accounting::User] 55 | api_method :multi_dim_return, :returns => [WithMultiDimArray] 56 | end 57 | 58 | class NullLogOut 59 | def <<(*args); end 60 | end 61 | 62 | class Container < ActionController::Base 63 | web_service_api API 64 | 65 | attr_accessor :value_void 66 | attr_accessor :value_normal 67 | attr_accessor :value_array_return 68 | attr_accessor :value_struct_pass 69 | attr_accessor :value_named_parameters 70 | 71 | def initialize 72 | @value_void = nil 73 | @value_normal = nil 74 | @value_array_return = nil 75 | @value_struct_pass = nil 76 | @value_named_parameters = nil 77 | end 78 | 79 | def void 80 | @value_void = @method_params 81 | end 82 | 83 | def normal 84 | @value_normal = @method_params 85 | 5 86 | end 87 | 88 | def array_return 89 | person = Person.new 90 | person.firstnames = ["one", "two"] 91 | person.lastname = "last" 92 | @value_array_return = [person] 93 | end 94 | 95 | def struct_pass 96 | @value_struct_pass = @method_params 97 | true 98 | end 99 | 100 | def nil_struct_return 101 | nil 102 | end 103 | 104 | def inner_nil 105 | Outer.new :name => 'outer', :inner => nil 106 | end 107 | 108 | def client_container 109 | 50 110 | end 111 | 112 | def named_parameters 113 | @value_named_parameters = @method_params 114 | end 115 | 116 | def thrower 117 | raise "Hi" 118 | end 119 | 120 | def user_return 121 | User.find(1) 122 | end 123 | 124 | def with_model_return 125 | WithModel.new :user => User.find(1), :users => User.find(:all) 126 | end 127 | 128 | def scoped_model_return 129 | Accounting::User.find(1) 130 | end 131 | 132 | def multi_dim_return 133 | WithMultiDimArray.new :pref => [%w{pref1 value1}, %w{pref2 value2}] 134 | end 135 | end 136 | 137 | class AbstractClientLet < WEBrick::HTTPServlet::AbstractServlet 138 | def initialize(controller) 139 | @controller = controller 140 | end 141 | 142 | def get_instance(*args) 143 | self 144 | end 145 | 146 | def require_path_info? 147 | false 148 | end 149 | 150 | def do_GET(req, res) 151 | raise WEBrick::HTTPStatus::MethodNotAllowed, "GET request not allowed." 152 | end 153 | 154 | def do_POST(req, res) 155 | raise NotImplementedError 156 | end 157 | end 158 | 159 | class AbstractServer 160 | include ClientTest 161 | include Singleton 162 | attr :container 163 | def initialize 164 | @container = Container.new 165 | @clientlet = create_clientlet(@container) 166 | log = WEBrick::BasicLog.new(NullLogOut.new) 167 | @server = WEBrick::HTTPServer.new(:Port => server_port, :Logger => log, :AccessLog => []) 168 | @server.mount('/', @clientlet) 169 | @thr = Thread.new { @server.start } 170 | until @server.status == :Running; end 171 | at_exit { @server.stop; @thr.join } 172 | end 173 | 174 | protected 175 | def create_clientlet 176 | raise NotImplementedError 177 | end 178 | 179 | def server_port 180 | raise NotImplementedError 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /test/abstract_unit.rb: -------------------------------------------------------------------------------- 1 | $: << "#{File.dirname(__FILE__)}/../lib" 2 | ENV["RAILS_ENV"] = "test" 3 | require 'rubygems' 4 | require 'test/unit' 5 | require 'action_web_service' 6 | require 'action_controller' 7 | require 'action_controller/test_case' 8 | require 'action_view' 9 | require 'action_view/test_case' 10 | 11 | # Show backtraces for deprecated behavior for quicker cleanup. 12 | ActiveSupport::Deprecation.debug = true 13 | 14 | 15 | ActiveRecord::Base.logger = ActionController::Base.logger = Logger.new("debug.log") 16 | 17 | begin 18 | require 'activerecord' 19 | require "active_record/test_case" 20 | require "active_record/fixtures" unless Object.const_defined?(:Fixtures) 21 | rescue LoadError => e 22 | fail "\nFailed to load activerecord: #{e}" 23 | end 24 | 25 | ActiveRecord::Base.configurations = { 26 | 'mysql' => { 27 | :adapter => "mysql", 28 | :username => "root", 29 | :encoding => "utf8", 30 | :database => "actionwebservice_unittest" 31 | } 32 | } 33 | 34 | ActiveRecord::Base.establish_connection 'mysql' 35 | 36 | class ActiveSupport::TestCase 37 | include ActiveRecord::TestFixtures 38 | self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" 39 | end 40 | -------------------------------------------------------------------------------- /test/api_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module APITest 4 | class API < ActionWebService::API::Base 5 | api_method :void 6 | api_method :expects_and_returns, :expects_and_returns => [:string] 7 | api_method :expects, :expects => [:int, :bool] 8 | api_method :returns, :returns => [:int, [:string]] 9 | api_method :named_signature, :expects => [{:appkey=>:int}, {:publish=>:bool}] 10 | api_method :string_types, :expects => ['int', 'string', 'bool', 'base64'] 11 | api_method :class_types, :expects => [TrueClass, Bignum, String] 12 | end 13 | end 14 | 15 | class TC_API < ActiveSupport::TestCase 16 | API = APITest::API 17 | 18 | def test_api_method_declaration 19 | %w( 20 | void 21 | expects_and_returns 22 | expects 23 | returns 24 | named_signature 25 | string_types 26 | class_types 27 | ).each do |name| 28 | name = name.to_sym 29 | public_name = API.public_api_method_name(name) 30 | assert(API.has_api_method?(name)) 31 | assert(API.has_public_api_method?(public_name)) 32 | assert(API.api_method_name(public_name) == name) 33 | assert(API.api_methods.has_key?(name)) 34 | end 35 | end 36 | 37 | def test_signature_canonicalization 38 | assert_equal(nil, API.api_methods[:void].expects) 39 | assert_equal(nil, API.api_methods[:void].returns) 40 | assert_equal([String], API.api_methods[:expects_and_returns].expects.map{|x| x.type_class}) 41 | assert_equal([String], API.api_methods[:expects_and_returns].returns.map{|x| x.type_class}) 42 | assert_equal([Integer, TrueClass], API.api_methods[:expects].expects.map{|x| x.type_class}) 43 | assert_equal(nil, API.api_methods[:expects].returns) 44 | assert_equal(nil, API.api_methods[:returns].expects) 45 | assert_equal([Integer, [String]], API.api_methods[:returns].returns.map{|x| x.array?? [x.element_type.type_class] : x.type_class}) 46 | assert_equal([[:appkey, Integer], [:publish, TrueClass]], API.api_methods[:named_signature].expects.map{|x| [x.name, x.type_class]}) 47 | assert_equal(nil, API.api_methods[:named_signature].returns) 48 | assert_equal([Integer, String, TrueClass, ActionWebService::Base64], API.api_methods[:string_types].expects.map{|x| x.type_class}) 49 | assert_equal(nil, API.api_methods[:string_types].returns) 50 | assert_equal([TrueClass, Integer, String], API.api_methods[:class_types].expects.map{|x| x.type_class}) 51 | assert_equal(nil, API.api_methods[:class_types].returns) 52 | end 53 | 54 | def test_not_instantiable 55 | assert_raises(NoMethodError) do 56 | API.new 57 | end 58 | end 59 | 60 | def test_api_errors 61 | assert_raises(ActionWebService::ActionWebServiceError) do 62 | klass = Class.new(ActionWebService::API::Base) do 63 | api_method :test, :expects => [ActiveRecord::Base] 64 | end 65 | end 66 | klass = Class.new(ActionWebService::API::Base) do 67 | allow_active_record_expects true 68 | api_method :test2, :expects => [ActiveRecord::Base] 69 | end 70 | assert_raises(ActionWebService::ActionWebServiceError) do 71 | klass = Class.new(ActionWebService::API::Base) do 72 | api_method :test, :invalid => [:int] 73 | end 74 | end 75 | end 76 | 77 | def test_parameter_names 78 | method = API.api_methods[:named_signature] 79 | assert_equal 0, method.expects_index_of(:appkey) 80 | assert_equal 1, method.expects_index_of(:publish) 81 | assert_equal 1, method.expects_index_of('publish') 82 | assert_equal 0, method.expects_index_of('appkey') 83 | assert_equal -1, method.expects_index_of('blah') 84 | assert_equal -1, method.expects_index_of(:missing) 85 | assert_equal -1, API.api_methods[:void].expects_index_of('test') 86 | end 87 | 88 | def test_parameter_hash 89 | method = API.api_methods[:named_signature] 90 | hash = method.expects_to_hash([5, false]) 91 | assert_equal({:appkey => 5, :publish => false}, hash) 92 | end 93 | 94 | def test_api_methods_compat 95 | sig = API.api_methods[:named_signature][:expects] 96 | assert_equal [{:appkey=>Integer}, {:publish=>TrueClass}], sig 97 | end 98 | 99 | def test_to_s 100 | assert_equal 'void Expects(int param0, bool param1)', APITest::API.api_methods[:expects].to_s 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/apis/auto_load_api.rb: -------------------------------------------------------------------------------- 1 | class AutoLoadAPI < ActionWebService::API::Base 2 | api_method :void 3 | end 4 | -------------------------------------------------------------------------------- /test/apis/broken_auto_load_api.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/base_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module BaseTest 4 | class API < ActionWebService::API::Base 5 | api_method :add, :expects => [:int, :int], :returns => [:int] 6 | api_method :void 7 | end 8 | 9 | class PristineAPI < ActionWebService::API::Base 10 | inflect_names false 11 | 12 | api_method :add 13 | api_method :under_score 14 | end 15 | 16 | class Service < ActionWebService::Base 17 | web_service_api API 18 | 19 | def add(a, b) 20 | end 21 | 22 | def void 23 | end 24 | end 25 | 26 | class PristineService < ActionWebService::Base 27 | web_service_api PristineAPI 28 | 29 | def add 30 | end 31 | 32 | def under_score 33 | end 34 | end 35 | end 36 | 37 | class TC_Base < ActiveSupport::TestCase 38 | def test_options 39 | assert(BaseTest::PristineService.web_service_api.inflect_names == false) 40 | assert(BaseTest::Service.web_service_api.inflect_names == true) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/casting_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module CastingTest 4 | class A < ActionWebService::Struct; end 5 | class B < A; end 6 | class API < ActionWebService::API::Base 7 | api_method :int, :expects => [:int] 8 | api_method :str, :expects => [:string] 9 | api_method :base64, :expects => [:base64] 10 | api_method :bool, :expects => [:bool] 11 | api_method :float, :expects => [:float] 12 | api_method :time, :expects => [:time] 13 | api_method :datetime, :expects => [:datetime] 14 | api_method :date, :expects => [:date] 15 | 16 | api_method :int_array, :expects => [[:int]] 17 | api_method :str_array, :expects => [[:string]] 18 | api_method :bool_array, :expects => [[:bool]] 19 | 20 | api_method :a, :expects => [A] 21 | end 22 | end 23 | 24 | class TC_Casting < Test::Unit::TestCase 25 | include CastingTest 26 | 27 | def test_base_type_casting_valid 28 | assert_equal 10000, cast_expects(:int, '10000')[0] 29 | assert_equal '10000', cast_expects(:str, 10000)[0] 30 | base64 = cast_expects(:base64, 10000)[0] 31 | assert_equal '10000', base64 32 | assert_instance_of ActionWebService::Base64, base64 33 | [1, '1', 'true', 'y', 'yes'].each do |val| 34 | assert_equal true, cast_expects(:bool, val)[0] 35 | end 36 | [0, '0', 'false', 'n', 'no'].each do |val| 37 | assert_equal false, cast_expects(:bool, val)[0] 38 | end 39 | assert_equal 3.14159, cast_expects(:float, '3.14159')[0] 40 | now = Time.at(Time.now.tv_sec) 41 | casted = cast_expects(:time, now.to_s)[0] 42 | assert_equal now, casted 43 | now = DateTime.now 44 | assert_equal now.to_s, cast_expects(:datetime, now.to_s)[0].to_s 45 | today = Date.today 46 | assert_equal today, cast_expects(:date, today.to_s)[0] 47 | end 48 | 49 | def test_base_type_casting_invalid 50 | assert_raises ArgumentError do 51 | cast_expects(:int, 'this is not a number') 52 | end 53 | assert_raises ActionWebService::Casting::CastingError do 54 | # neither true or false ;) 55 | cast_expects(:bool, 'i always lie') 56 | end 57 | assert_raises ArgumentError do 58 | cast_expects(:float, 'not a float') 59 | end 60 | assert_raises ArgumentError do 61 | cast_expects(:time, '111111111111111111111111111111111') 62 | end 63 | assert_raises ArgumentError do 64 | cast_expects(:datetime, '-1') 65 | end 66 | assert_raises ArgumentError do 67 | cast_expects(:date, '') 68 | end 69 | end 70 | 71 | def test_array_type_casting 72 | assert_equal [1, 2, 3213992, 4], cast_expects(:int_array, ['1', '2', '3213992', '4'])[0] 73 | assert_equal ['one', 'two', '5.0', '200', nil, 'true'], cast_expects(:str_array, [:one, 'two', 5.0, 200, nil, true])[0] 74 | assert_equal [true, nil, true, true, false], cast_expects(:bool_array, ['1', nil, 'y', true, 'false'])[0] 75 | end 76 | 77 | def test_array_type_casting_failure 78 | assert_raises ActionWebService::Casting::CastingError do 79 | cast_expects(:bool_array, ['false', 'blahblah']) 80 | end 81 | assert_raises ArgumentError do 82 | cast_expects(:int_array, ['1', '2.021', '4']) 83 | end 84 | end 85 | 86 | def test_structured_type_casting_with_polymorphism 87 | assert cast_expects(:a, B.new)[0].is_a?(B) 88 | end 89 | 90 | private 91 | def cast_expects(method_name, *args) 92 | API.api_method_instance(method_name.to_sym).cast_expects([*args]) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/client_soap_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_client' 2 | 3 | 4 | module ClientSoapTest 5 | PORT = 8998 6 | 7 | class SoapClientLet < ClientTest::AbstractClientLet 8 | def do_POST(req, res) 9 | test_request = ActionController::TestRequest.new 10 | test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1] 11 | test_request.env['REQUEST_METHOD'] = "POST" 12 | test_request.env['HTTP_CONTENTTYPE'] = 'text/xml' 13 | test_request.env['HTTP_SOAPACTION'] = req.header['soapaction'][0] 14 | test_request.env['RAW_POST_DATA'] = req.body 15 | response = ActionController::TestResponse.new 16 | @controller.process(test_request, response) 17 | res.header['content-type'] = 'text/xml' 18 | res.body = response.body 19 | rescue Exception => e 20 | $stderr.puts e.message 21 | $stderr.puts e.backtrace.join("\n") 22 | ensure 23 | ActiveRecord::Base.clear_active_connections! 24 | end 25 | end 26 | 27 | class ClientContainer < ActionController::Base 28 | web_client_api :client, :soap, "http://localhost:#{PORT}/client/api", :api => ClientTest::API 29 | web_client_api :invalid, :null, "", :api => true 30 | 31 | def get_client 32 | client 33 | end 34 | 35 | def get_invalid 36 | invalid 37 | end 38 | end 39 | 40 | class SoapServer < ClientTest::AbstractServer 41 | def create_clientlet(controller) 42 | SoapClientLet.new(controller) 43 | end 44 | 45 | def server_port 46 | PORT 47 | end 48 | end 49 | end 50 | 51 | class TC_ClientSoap < ActiveSupport::TestCase 52 | include ClientTest 53 | include ClientSoapTest 54 | 55 | fixtures :users 56 | 57 | def setup 58 | @server = SoapServer.instance 59 | @container = @server.container 60 | @client = ActionWebService::Client::Soap.new(API, "http://localhost:#{@server.server_port}/client/api") 61 | end 62 | 63 | def test_void 64 | assert(@container.value_void.nil?) 65 | @client.void 66 | assert(!@container.value_void.nil?) 67 | end 68 | 69 | def test_normal 70 | assert(@container.value_normal.nil?) 71 | assert_equal(5, @client.normal(5, 6)) 72 | assert_equal([5, 6], @container.value_normal) 73 | assert_equal(5, @client.normal("7", "8")) 74 | assert_equal([7, 8], @container.value_normal) 75 | assert_equal(5, @client.normal(true, false)) 76 | end 77 | 78 | def test_array_return 79 | assert(@container.value_array_return.nil?) 80 | new_person = Person.new 81 | new_person.firstnames = ["one", "two"] 82 | new_person.lastname = "last" 83 | assert_equal([new_person], @client.array_return) 84 | assert_equal([new_person], @container.value_array_return) 85 | end 86 | 87 | def test_struct_pass 88 | assert(@container.value_struct_pass.nil?) 89 | new_person = Person.new 90 | new_person.firstnames = ["one", "two"] 91 | new_person.lastname = "last" 92 | assert_equal(true, @client.struct_pass([new_person])) 93 | assert_equal([[new_person]], @container.value_struct_pass) 94 | end 95 | 96 | def test_nil_struct_return 97 | assert_nil @client.nil_struct_return 98 | end 99 | 100 | def test_inner_nil 101 | outer = @client.inner_nil 102 | assert_equal 'outer', outer.name 103 | assert_nil outer.inner 104 | end 105 | 106 | def test_client_container 107 | assert_equal(50, ClientContainer.new.get_client.client_container) 108 | assert(ClientContainer.new.get_invalid.nil?) 109 | end 110 | 111 | def test_named_parameters 112 | assert(@container.value_named_parameters.nil?) 113 | assert(@client.named_parameters("key", 5).nil?) 114 | assert_equal(["key", 5], @container.value_named_parameters) 115 | end 116 | 117 | def test_capitalized_method_name 118 | @container.value_normal = nil 119 | assert_equal(5, @client.Normal(5, 6)) 120 | assert_equal([5, 6], @container.value_normal) 121 | @container.value_normal = nil 122 | end 123 | 124 | def test_model_return 125 | user = @client.user_return 126 | assert_equal 1, user.id 127 | assert_equal 'Kent', user.name 128 | assert user.active? 129 | assert_kind_of Date, user.created_on 130 | assert_equal Date.today, user.created_on 131 | assert_equal BigDecimal('12.2'), user.balance 132 | end 133 | 134 | def test_with_model 135 | with_model = @client.with_model_return 136 | assert_equal 'Kent', with_model.user.name 137 | assert_equal 2, with_model.users.size 138 | with_model.users.each do |user| 139 | assert_kind_of User, user 140 | end 141 | end 142 | 143 | def test_scoped_model_return 144 | scoped_model = @client.scoped_model_return 145 | assert_kind_of Accounting::User, scoped_model 146 | assert_equal 'Kent', scoped_model.name 147 | end 148 | 149 | def test_multi_dim_return 150 | md_struct = @client.multi_dim_return 151 | assert_kind_of Array, md_struct.pref 152 | assert_equal 2, md_struct.pref.size 153 | assert_kind_of Array, md_struct.pref[0] 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /test/client_xmlrpc_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_client' 2 | 3 | 4 | module ClientXmlRpcTest 5 | PORT = 8999 6 | 7 | class XmlRpcClientLet < ClientTest::AbstractClientLet 8 | def do_POST(req, res) 9 | test_request = ActionController::TestRequest.new 10 | test_request.request_parameters['action'] = req.path.gsub(/^\//, '').split(/\//)[1] 11 | test_request.env['REQUEST_METHOD'] = "POST" 12 | test_request.env['HTTP_CONTENT_TYPE'] = 'text/xml' 13 | test_request.env['RAW_POST_DATA'] = req.body 14 | response = ActionController::TestResponse.new 15 | @controller.process(test_request, response) 16 | res.header['content-type'] = 'text/xml' 17 | res.body = response.body 18 | rescue Exception => e 19 | $stderr.puts e.message 20 | $stderr.puts e.backtrace.join("\n") 21 | ensure 22 | ActiveRecord::Base.clear_active_connections! 23 | end 24 | end 25 | 26 | class ClientContainer < ActionController::Base 27 | web_client_api :client, :xmlrpc, "http://localhost:#{PORT}/client/api", :api => ClientTest::API 28 | 29 | def get_client 30 | client 31 | end 32 | end 33 | 34 | class XmlRpcServer < ClientTest::AbstractServer 35 | def create_clientlet(controller) 36 | XmlRpcClientLet.new(controller) 37 | end 38 | 39 | def server_port 40 | PORT 41 | end 42 | end 43 | end 44 | 45 | class TC_ClientXmlRpc < ActiveSupport::TestCase 46 | include ClientTest 47 | include ClientXmlRpcTest 48 | 49 | fixtures :users 50 | 51 | def setup 52 | @server = XmlRpcServer.instance 53 | @container = @server.container 54 | @client = ActionWebService::Client::XmlRpc.new(API, "http://localhost:#{@server.server_port}/client/api") 55 | end 56 | 57 | def test_void 58 | assert(@container.value_void.nil?) 59 | @client.void 60 | assert(!@container.value_void.nil?) 61 | end 62 | 63 | def test_normal 64 | assert(@container.value_normal.nil?) 65 | assert_equal(5, @client.normal(5, 6)) 66 | assert_equal([5, 6], @container.value_normal) 67 | assert_equal(5, @client.normal("7", "8")) 68 | assert_equal([7, 8], @container.value_normal) 69 | assert_equal(5, @client.normal(true, false)) 70 | end 71 | 72 | def test_array_return 73 | assert(@container.value_array_return.nil?) 74 | new_person = Person.new 75 | new_person.firstnames = ["one", "two"] 76 | new_person.lastname = "last" 77 | assert_equal([new_person], @client.array_return) 78 | assert_equal([new_person], @container.value_array_return) 79 | end 80 | 81 | def test_struct_pass 82 | assert(@container.value_struct_pass.nil?) 83 | new_person = Person.new 84 | new_person.firstnames = ["one", "two"] 85 | new_person.lastname = "last" 86 | assert_equal(true, @client.struct_pass([new_person])) 87 | assert_equal([[new_person]], @container.value_struct_pass) 88 | end 89 | 90 | def test_nil_struct_return 91 | assert_equal false, @client.nil_struct_return 92 | end 93 | 94 | def test_inner_nil 95 | outer = @client.inner_nil 96 | assert_equal 'outer', outer.name 97 | assert_nil outer.inner 98 | end 99 | 100 | def test_client_container 101 | assert_equal(50, ClientContainer.new.get_client.client_container) 102 | end 103 | 104 | def test_named_parameters 105 | assert(@container.value_named_parameters.nil?) 106 | assert_equal(false, @client.named_parameters("xxx", 7)) 107 | assert_equal(["xxx", 7], @container.value_named_parameters) 108 | end 109 | 110 | def test_exception 111 | assert_raises(ActionWebService::Client::ClientError) do 112 | assert(@client.thrower) 113 | end 114 | end 115 | 116 | def test_invalid_signature 117 | assert_raises(ArgumentError) do 118 | @client.normal 119 | end 120 | end 121 | 122 | def test_model_return 123 | user = @client.user_return 124 | assert_equal 1, user.id 125 | assert_equal 'Kent', user.name 126 | assert user.active? 127 | assert_kind_of Time, user.created_on 128 | assert_equal Time.utc(Time.now.year, Time.now.month, Time.now.day), user.created_on 129 | assert_equal BigDecimal('12.2'), user.balance 130 | end 131 | 132 | def test_with_model 133 | with_model = @client.with_model_return 134 | assert_equal 'Kent', with_model.user.name 135 | assert_equal 2, with_model.users.size 136 | with_model.users.each do |user| 137 | assert_kind_of User, user 138 | end 139 | end 140 | 141 | def test_scoped_model_return 142 | scoped_model = @client.scoped_model_return 143 | assert_kind_of Accounting::User, scoped_model 144 | assert_equal 'Kent', scoped_model.name 145 | end 146 | 147 | def test_multi_dim_return 148 | md_struct = @client.multi_dim_return 149 | assert_kind_of Array, md_struct.pref 150 | assert_equal 2, md_struct.pref.size 151 | assert_kind_of Array, md_struct.pref[0] 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/container_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module ContainerTest 4 | $immediate_service = Object.new 5 | $deferred_service = Object.new 6 | 7 | class DelegateContainer < ActionController::Base 8 | web_service_dispatching_mode :delegated 9 | 10 | attr :flag 11 | attr :previous_flag 12 | 13 | def initialize 14 | @previous_flag = nil 15 | @flag = true 16 | end 17 | 18 | web_service :immediate_service, $immediate_service 19 | web_service(:deferred_service) { @previous_flag = @flag; @flag = false; $deferred_service } 20 | end 21 | 22 | class DirectContainer < ActionController::Base 23 | web_service_dispatching_mode :direct 24 | end 25 | 26 | class InvalidContainer 27 | include ActionWebService::Container::Direct 28 | end 29 | end 30 | 31 | class TC_Container < Test::Unit::TestCase 32 | include ContainerTest 33 | 34 | def setup 35 | @delegate_container = DelegateContainer.new 36 | @direct_container = DirectContainer.new 37 | end 38 | 39 | def test_registration 40 | assert(DelegateContainer.has_web_service?(:immediate_service)) 41 | assert(DelegateContainer.has_web_service?(:deferred_service)) 42 | assert(!DelegateContainer.has_web_service?(:fake_service)) 43 | assert_raises(ActionWebService::Container::Delegated::ContainerError) do 44 | DelegateContainer.web_service('invalid') 45 | end 46 | end 47 | 48 | def test_service_object 49 | assert_raises(ActionWebService::Container::Delegated::ContainerError) do 50 | @delegate_container.web_service_object(:nonexistent) 51 | end 52 | assert(@delegate_container.flag == true) 53 | assert(@delegate_container.web_service_object(:immediate_service) == $immediate_service) 54 | assert(@delegate_container.previous_flag.nil?) 55 | assert(@delegate_container.flag == true) 56 | assert(@delegate_container.web_service_object(:deferred_service) == $deferred_service) 57 | assert(@delegate_container.previous_flag == true) 58 | assert(@delegate_container.flag == false) 59 | end 60 | 61 | def test_direct_container 62 | assert(DirectContainer.web_service_dispatching_mode == :direct) 63 | end 64 | 65 | def test_validity 66 | assert_raises(ActionWebService::Container::Direct::ContainerError) do 67 | InvalidContainer.web_service_api :test 68 | end 69 | assert_raises(ActionWebService::Container::Direct::ContainerError) do 70 | InvalidContainer.web_service_api 50.0 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/dispatcher_action_controller_soap_test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__) + '/apis') 2 | require File.dirname(__FILE__) + '/abstract_dispatcher' 3 | require 'wsdl/parser' 4 | 5 | class ActionController::Base 6 | class << self 7 | alias :inherited_without_name_error :inherited 8 | def inherited(child) 9 | begin 10 | inherited_without_name_error(child) 11 | rescue NameError => e 12 | end 13 | end 14 | end 15 | end 16 | 17 | class AutoLoadController < ActionController::Base; end 18 | class FailingAutoLoadController < ActionController::Base; end 19 | class BrokenAutoLoadController < ActionController::Base; end 20 | 21 | class TC_DispatcherActionControllerSoap < Test::Unit::TestCase 22 | include DispatcherTest 23 | include DispatcherCommonTests 24 | 25 | def setup 26 | @direct_controller = DirectController.new 27 | @delegated_controller = DelegatedController.new 28 | @virtual_controller = VirtualController.new 29 | @layered_controller = LayeredController.new 30 | @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@direct_controller) 31 | end 32 | 33 | def test_wsdl_generation 34 | ensure_valid_wsdl_generation DelegatedController.new, DispatcherTest::WsdlNamespace 35 | ensure_valid_wsdl_generation DirectController.new, DispatcherTest::WsdlNamespace 36 | end 37 | 38 | def test_wsdl_action 39 | delegated_types = ensure_valid_wsdl_action DelegatedController.new 40 | delegated_names = delegated_types.map{|x| x.name.name} 41 | assert(delegated_names.include?('DispatcherTest..NodeArray')) 42 | assert(delegated_names.include?('DispatcherTest..Node')) 43 | direct_types = ensure_valid_wsdl_action DirectController.new 44 | direct_names = direct_types.map{|x| x.name.name} 45 | assert(direct_names.include?('DispatcherTest..NodeArray')) 46 | assert(direct_names.include?('DispatcherTest..Node')) 47 | assert(direct_names.include?('IntegerArray')) 48 | end 49 | 50 | def test_autoloading 51 | assert(!AutoLoadController.web_service_api.nil?) 52 | assert(AutoLoadController.web_service_api.has_public_api_method?('Void')) 53 | assert(FailingAutoLoadController.web_service_api.nil?) 54 | assert_raises(MissingSourceFile) do 55 | FailingAutoLoadController.require_web_service_api :blah 56 | end 57 | assert_raises(ArgumentError) do 58 | FailingAutoLoadController.require_web_service_api 50.0 59 | end 60 | assert(BrokenAutoLoadController.web_service_api.nil?) 61 | end 62 | 63 | def test_layered_dispatching 64 | mt_cats = do_method_call(@layered_controller, 'mt.getCategories') 65 | assert_equal(["mtCat1", "mtCat2"], mt_cats) 66 | blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories') 67 | assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats) 68 | end 69 | 70 | def test_utf8 71 | @direct_controller.web_service_exception_reporting = true 72 | $KCODE = 'u' 73 | assert_equal(Utf8String, do_method_call(@direct_controller, 'TestUtf8')) 74 | retval = SOAP::Processor.unmarshal(@response_body).body.response 75 | assert retval.is_a?(SOAP::SOAPString) 76 | 77 | # If $KCODE is not set to UTF-8, any strings with non-ASCII UTF-8 data 78 | # will be sent back as base64 by SOAP4R. By the time we get it here though, 79 | # it will be decoded back into a string. So lets read the base64 value 80 | # from the message body directly. 81 | $KCODE = 'NONE' 82 | do_method_call(@direct_controller, 'TestUtf8') 83 | retval = SOAP::Processor.unmarshal(@response_body).body.response 84 | assert retval.is_a?(SOAP::SOAPBase64) 85 | assert_equal "T25lIFdvcmxkIENhZsOp", retval.data.to_s 86 | end 87 | 88 | protected 89 | def exception_message(soap_fault_exception) 90 | soap_fault_exception.detail.cause.message 91 | end 92 | 93 | def is_exception?(obj) 94 | obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ 95 | obj.detail.cause.is_a?(Exception) 96 | end 97 | 98 | def service_name(container) 99 | container.is_a?(DelegatedController) ? 'test_service' : 'api' 100 | end 101 | 102 | def ensure_valid_wsdl_generation(controller, expected_namespace) 103 | wsdl = controller.generate_wsdl 104 | ensure_valid_wsdl(controller, wsdl, expected_namespace) 105 | end 106 | 107 | def ensure_valid_wsdl(controller, wsdl, expected_namespace) 108 | definitions = WSDL::Parser.new.parse(wsdl) 109 | assert(definitions.is_a?(WSDL::Definitions)) 110 | definitions.bindings.each do |binding| 111 | assert(binding.name.name.index(':').nil?) 112 | end 113 | definitions.services.each do |service| 114 | service.ports.each do |port| 115 | assert(port.name.name.index(':').nil?) 116 | end 117 | end 118 | types = definitions.collect_complextypes.map{|x| x.name} 119 | types.each do |type| 120 | assert(type.namespace == expected_namespace) 121 | end 122 | location = definitions.services[0].ports[0].soap_address.location 123 | if controller.is_a?(DelegatedController) 124 | assert_match %r{http://test.host/dispatcher_test/delegated/test_service$}, location 125 | elsif controller.is_a?(DirectController) 126 | assert_match %r{http://test.host/dispatcher_test/direct/api$}, location 127 | end 128 | definitions.collect_complextypes 129 | end 130 | 131 | def ensure_valid_wsdl_action(controller) 132 | test_request = ActionController::TestRequest.new 133 | test_request.action = 'wsdl' 134 | test_response = ActionController::TestResponse.new 135 | wsdl = controller.process(test_request, test_response).body 136 | ensure_valid_wsdl(controller, wsdl, DispatcherTest::WsdlNamespace) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/dispatcher_action_controller_xmlrpc_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_dispatcher' 2 | 3 | class TC_DispatcherActionControllerXmlRpc < Test::Unit::TestCase 4 | include DispatcherTest 5 | include DispatcherCommonTests 6 | 7 | def setup 8 | @direct_controller = DirectController.new 9 | @delegated_controller = DelegatedController.new 10 | @layered_controller = LayeredController.new 11 | @virtual_controller = VirtualController.new 12 | @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@direct_controller) 13 | end 14 | 15 | def test_layered_dispatching 16 | mt_cats = do_method_call(@layered_controller, 'mt.getCategories') 17 | assert_equal(["mtCat1", "mtCat2"], mt_cats) 18 | blogger_cats = do_method_call(@layered_controller, 'blogger.getCategories') 19 | assert_equal(["bloggerCat1", "bloggerCat2"], blogger_cats) 20 | end 21 | 22 | def test_multicall 23 | response = do_method_call(@layered_controller, 'system.multicall', [ 24 | {'methodName' => 'mt.getCategories'}, 25 | {'methodName' => 'blogger.getCategories'}, 26 | {'methodName' => 'mt.bool'}, 27 | {'methodName' => 'blogger.str', 'params' => ['2000']}, 28 | {'methodName' => 'mt.alwaysFail'}, 29 | {'methodName' => 'blogger.alwaysFail'}, 30 | {'methodName' => 'mt.blah'}, 31 | {'methodName' => 'blah.blah'}, 32 | {'methodName' => 'mt.person'} 33 | ]) 34 | assert_equal [ 35 | [["mtCat1", "mtCat2"]], 36 | [["bloggerCat1", "bloggerCat2"]], 37 | [true], 38 | ["2500"], 39 | {"faultCode" => 3, "faultString" => "MT AlwaysFail"}, 40 | {"faultCode" => 3, "faultString" => "Blogger AlwaysFail"}, 41 | {"faultCode" => 4, "faultMessage" => "no such method 'blah' on API DispatcherTest::MTAPI"}, 42 | {"faultCode" => 4, "faultMessage" => "no such web service 'blah'"}, 43 | [{"name"=>"person1", "id"=>1}] 44 | ], response 45 | end 46 | 47 | protected 48 | def exception_message(xmlrpc_fault_exception) 49 | xmlrpc_fault_exception.faultString 50 | end 51 | 52 | def is_exception?(obj) 53 | obj.is_a?(XMLRPC::FaultException) 54 | end 55 | 56 | def service_name(container) 57 | container.is_a?(DelegatedController) ? 'test_service' : 'api' 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/fixtures/db_definitions/mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` int(11) NOT NULL auto_increment, 3 | `name` varchar(30) default NULL, 4 | `active` tinyint(4) default NULL, 5 | `balance` decimal(5, 2) default NULL, 6 | `created_on` date default NULL, 7 | PRIMARY KEY (`id`) 8 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 9 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | user1: 2 | id: 1 3 | name: Kent 4 | active: 1 5 | balance: 12.2 6 | created_on: <%= Date.today %> 7 | user2: 8 | id: 2 9 | name: David 10 | active: 1 11 | balance: 16.4 12 | created_on: <%= Date.today %> 13 | -------------------------------------------------------------------------------- /test/gencov: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rcov -x '.*_test\.rb,rubygems,abstract_,/run,/apis' ./run 4 | -------------------------------------------------------------------------------- /test/invocation_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module InvocationTest 4 | class API < ActionWebService::API::Base 5 | api_method :add, :expects => [:int, :int], :returns => [:int] 6 | api_method :transmogrify, :expects_and_returns => [:string] 7 | api_method :fail_with_reason 8 | api_method :fail_generic 9 | api_method :no_before 10 | api_method :no_after 11 | api_method :only_one 12 | api_method :only_two 13 | end 14 | 15 | class Interceptor 16 | attr :args 17 | 18 | def initialize 19 | @args = nil 20 | end 21 | 22 | def intercept(*args) 23 | @args = args 24 | end 25 | end 26 | 27 | InterceptorClass = Interceptor.new 28 | 29 | class Service < ActionController::Base 30 | web_service_api API 31 | 32 | before_invocation :intercept_before, :except => [:no_before] 33 | after_invocation :intercept_after, :except => [:no_after] 34 | prepend_after_invocation :intercept_after_first, :except => [:no_after] 35 | prepend_before_invocation :intercept_only, :only => [:only_one, :only_two] 36 | after_invocation(:only => [:only_one]) do |*args| 37 | args[0].instance_variable_set('@block_invoked', args[1]) 38 | end 39 | after_invocation InterceptorClass, :only => [:only_one] 40 | 41 | attr_accessor :before_invoked 42 | attr_accessor :after_invoked 43 | attr_accessor :after_first_invoked 44 | attr_accessor :only_invoked 45 | attr_accessor :block_invoked 46 | attr_accessor :invocation_result 47 | 48 | def initialize 49 | @before_invoked = nil 50 | @after_invoked = nil 51 | @after_first_invoked = nil 52 | @only_invoked = nil 53 | @invocation_result = nil 54 | @block_invoked = nil 55 | end 56 | 57 | def add(a, b) 58 | a + b 59 | end 60 | 61 | def transmogrify(str) 62 | str.upcase 63 | end 64 | 65 | def fail_with_reason 66 | end 67 | 68 | def fail_generic 69 | end 70 | 71 | def no_before 72 | 5 73 | end 74 | 75 | def no_after 76 | end 77 | 78 | def only_one 79 | end 80 | 81 | def only_two 82 | end 83 | 84 | protected 85 | def intercept_before(name, args) 86 | @before_invoked = name 87 | return [false, "permission denied"] if name == :fail_with_reason 88 | return false if name == :fail_generic 89 | end 90 | 91 | def intercept_after(name, args, result) 92 | @after_invoked = name 93 | @invocation_result = result 94 | end 95 | 96 | def intercept_after_first(name, args, result) 97 | @after_first_invoked = name 98 | end 99 | 100 | def intercept_only(name, args) 101 | raise "Interception error" unless name == :only_one || name == :only_two 102 | @only_invoked = name 103 | end 104 | end 105 | end 106 | 107 | class TC_Invocation < Test::Unit::TestCase 108 | include ActionWebService::Invocation 109 | 110 | def setup 111 | @service = InvocationTest::Service.new 112 | end 113 | 114 | def test_invocation 115 | assert(perform_invocation(:add, 5, 10) == 15) 116 | assert(perform_invocation(:transmogrify, "hello") == "HELLO") 117 | assert_raises(NoMethodError) do 118 | perform_invocation(:nonexistent_method_xyzzy) 119 | end 120 | end 121 | 122 | def test_interceptor_registration 123 | assert(InvocationTest::Service.before_invocation_interceptors.length == 2) 124 | assert(InvocationTest::Service.after_invocation_interceptors.length == 4) 125 | assert_equal(:intercept_only, InvocationTest::Service.before_invocation_interceptors[0]) 126 | assert_equal(:intercept_after_first, InvocationTest::Service.after_invocation_interceptors[0]) 127 | end 128 | 129 | def test_interception 130 | assert(@service.before_invoked.nil?) 131 | assert(@service.after_invoked.nil?) 132 | assert(@service.only_invoked.nil?) 133 | assert(@service.block_invoked.nil?) 134 | assert(@service.invocation_result.nil?) 135 | perform_invocation(:add, 20, 50) 136 | assert(@service.before_invoked == :add) 137 | assert(@service.after_invoked == :add) 138 | assert(@service.invocation_result == 70) 139 | end 140 | 141 | def test_interception_canceling 142 | reason = nil 143 | perform_invocation(:fail_with_reason){|r| reason = r} 144 | assert(@service.before_invoked == :fail_with_reason) 145 | assert(@service.after_invoked.nil?) 146 | assert(@service.invocation_result.nil?) 147 | assert(reason == "permission denied") 148 | reason = true 149 | @service.before_invoked = @service.after_invoked = @service.invocation_result = nil 150 | perform_invocation(:fail_generic){|r| reason = r} 151 | assert(@service.before_invoked == :fail_generic) 152 | assert(@service.after_invoked.nil?) 153 | assert(@service.invocation_result.nil?) 154 | assert(reason == true) 155 | end 156 | 157 | def test_interception_except_conditions 158 | perform_invocation(:no_before) 159 | assert(@service.before_invoked.nil?) 160 | assert(@service.after_first_invoked == :no_before) 161 | assert(@service.after_invoked == :no_before) 162 | assert(@service.invocation_result == 5) 163 | @service.before_invoked = @service.after_invoked = @service.invocation_result = nil 164 | perform_invocation(:no_after) 165 | assert(@service.before_invoked == :no_after) 166 | assert(@service.after_invoked.nil?) 167 | assert(@service.invocation_result.nil?) 168 | end 169 | 170 | def test_interception_only_conditions 171 | assert(@service.only_invoked.nil?) 172 | perform_invocation(:only_one) 173 | assert(@service.only_invoked == :only_one) 174 | assert(@service.block_invoked == :only_one) 175 | assert(InvocationTest::InterceptorClass.args[1] == :only_one) 176 | @service.only_invoked = nil 177 | perform_invocation(:only_two) 178 | assert(@service.only_invoked == :only_two) 179 | end 180 | 181 | private 182 | def perform_invocation(method_name, *args, &block) 183 | @service.perform_invocation(method_name, args, &block) 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'test/unit' 3 | $:.unshift(File.dirname(__FILE__) + '/../lib') 4 | args = Dir[File.join(File.dirname(__FILE__), '*_test.rb')] + Dir[File.join(File.dirname(__FILE__), 'ws/*_test.rb')] 5 | (r = Test::Unit::AutoRunner.new(true)).process_args(args) 6 | exit r.run 7 | -------------------------------------------------------------------------------- /test/scaffolded_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | ActionController::Routing::Routes.draw do |map| 4 | map.connect '', :controller => 'scaffolded' 5 | map.connect ':controller/:action/:id' 6 | end 7 | 8 | ActionController::Base.view_paths = [ '.' ] 9 | 10 | class ScaffoldPerson < ActionWebService::Struct 11 | member :id, :int 12 | member :name, :string 13 | member :birth, :date 14 | 15 | def ==(other) 16 | self.id == other.id && self.name == other.name 17 | end 18 | end 19 | 20 | class ScaffoldedControllerTestAPI < ActionWebService::API::Base 21 | api_method :hello, :expects => [{:integer=>:int}, :string], :returns => [:bool] 22 | api_method :hello_struct_param, :expects => [{:person => ScaffoldPerson}], :returns => [:bool] 23 | api_method :date_of_birth, :expects => [ScaffoldPerson], :returns => [:string] 24 | api_method :bye, :returns => [[ScaffoldPerson]] 25 | api_method :date_diff, :expects => [{:start_date => :date}, {:end_date => :date}], :returns => [:int] 26 | api_method :time_diff, :expects => [{:start_time => :time}, {:end_time => :time}], :returns => [:int] 27 | api_method :base64_upcase, :expects => [:base64], :returns => [:base64] 28 | end 29 | 30 | class ScaffoldedController < ActionController::Base 31 | web_service_api ScaffoldedControllerTestAPI 32 | web_service_scaffold :scaffold_invoke 33 | 34 | def hello(int, string) 35 | 0 36 | end 37 | 38 | def hello_struct_param(person) 39 | 0 40 | end 41 | 42 | def date_of_birth(person) 43 | person.birth.to_s 44 | end 45 | 46 | def bye 47 | [ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")] 48 | end 49 | 50 | def rescue_action(e) 51 | raise e 52 | end 53 | 54 | def date_diff(start_date, end_date) 55 | end_date - start_date 56 | end 57 | 58 | def time_diff(start_time, end_time) 59 | end_time - start_time 60 | end 61 | 62 | def base64_upcase(data) 63 | data.upcase 64 | end 65 | end 66 | 67 | class ScaffoldedControllerTest < ActionController::TestCase 68 | # def setup 69 | # @controller = ScaffoldedController.new 70 | # @request = ActionController::TestRequest.new 71 | # @response = ActionController::TestResponse.new 72 | # end 73 | 74 | def test_scaffold_invoke 75 | get :scaffold_invoke 76 | assert_template 'methods.html.erb' 77 | end 78 | 79 | def test_scaffold_invoke_method_params 80 | get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Hello' 81 | assert_template 'parameters.html.erb' 82 | end 83 | 84 | def test_scaffold_invoke_method_params_with_struct 85 | get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'HelloStructParam' 86 | assert_template 'parameters.html.erb' 87 | assert_tag :tag => 'form' 88 | assert_tag :tag => 'input', :attributes => {:name => "method_params[0][name]"} 89 | end 90 | 91 | def test_scaffold_invoke_submit_hello 92 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Hello', :method_params => {'0' => '5', '1' => 'hello world'} 93 | assert_template 'result.html.erb' 94 | assert_equal false, @controller.instance_eval{ @method_return_value } 95 | end 96 | 97 | def test_scaffold_invoke_submit_bye 98 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Bye' 99 | assert_template 'result.html.erb' 100 | persons = [ScaffoldPerson.new(:id => 1, :name => "leon"), ScaffoldPerson.new(:id => 2, :name => "paul")] 101 | assert_equal persons, @controller.instance_eval{ @method_return_value } 102 | end 103 | 104 | def test_scaffold_date_params 105 | get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'DateDiff' 106 | (0..1).each do |param| 107 | (1..3).each do |date_part| 108 | assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"}, 109 | :children => {:greater_than => 1, :only => {:tag => 'option'}} 110 | end 111 | end 112 | 113 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateDiff', 114 | :method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1'}, '1' => {'1' => '2006', '2' => '2', '3' => '2'}} 115 | assert_equal 1, @controller.instance_eval{ @method_return_value } 116 | end 117 | 118 | def test_scaffold_struct_date_params 119 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'DateOfBirth', 120 | :method_params => {'0' => {'birth' => {'1' => '2006', '2' => '2', '3' => '1'}, 'id' => '1', 'name' => 'person'}} 121 | assert_equal '2006-02-01', @controller.instance_eval{ @method_return_value } 122 | end 123 | 124 | def test_scaffold_time_params 125 | get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'TimeDiff' 126 | (0..1).each do |param| 127 | (1..6).each do |date_part| 128 | assert_tag :tag => 'select', :attributes => {:name => "method_params[#{param}][#{date_part}]"}, 129 | :children => {:greater_than => 1, :only => {:tag => 'option'}} 130 | end 131 | end 132 | 133 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'TimeDiff', 134 | :method_params => {'0' => {'1' => '2006', '2' => '2', '3' => '1', '4' => '1', '5' => '1', '6' => '1'}, 135 | '1' => {'1' => '2006', '2' => '2', '3' => '2', '4' => '1', '5' => '1', '6' => '1'}} 136 | assert_equal 86400, @controller.instance_eval{ @method_return_value } 137 | end 138 | 139 | def test_scaffold_base64 140 | get :scaffold_invoke_method_params, :service => 'scaffolded', :method => 'Base64Upcase' 141 | assert_tag :tag => 'textarea', :attributes => {:name => 'method_params[0]'} 142 | 143 | post :scaffold_invoke_submit, :service => 'scaffolded', :method => 'Base64Upcase', :method_params => {'0' => 'scaffold'} 144 | assert_equal 'SCAFFOLD', @controller.instance_eval{ @method_return_value } 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/struct_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | 3 | module StructTest 4 | class Struct < ActionWebService::Struct 5 | member :id, Integer 6 | member :name, String 7 | member :items, [String] 8 | member :deleted, :bool 9 | member :emails, [:string] 10 | end 11 | end 12 | 13 | class TC_Struct < Test::Unit::TestCase 14 | include StructTest 15 | 16 | def setup 17 | @struct = Struct.new(:id => 5, 18 | :name => 'hello', 19 | :items => ['one', 'two'], 20 | :deleted => true, 21 | :emails => ['test@test.com']) 22 | end 23 | 24 | def test_members 25 | assert_equal(5, Struct.members.size) 26 | assert_equal(Integer, Struct.members[:id].type_class) 27 | assert_equal(String, Struct.members[:name].type_class) 28 | assert_equal(String, Struct.members[:items].element_type.type_class) 29 | assert_equal(TrueClass, Struct.members[:deleted].type_class) 30 | assert_equal(String, Struct.members[:emails].element_type.type_class) 31 | end 32 | 33 | def test_initializer_and_lookup 34 | assert_equal(5, @struct.id) 35 | assert_equal('hello', @struct.name) 36 | assert_equal(['one', 'two'], @struct.items) 37 | assert_equal(true, @struct.deleted) 38 | assert_equal(['test@test.com'], @struct.emails) 39 | assert_equal(5, @struct['id']) 40 | assert_equal('hello', @struct['name']) 41 | assert_equal(['one', 'two'], @struct['items']) 42 | assert_equal(true, @struct['deleted']) 43 | assert_equal(['test@test.com'], @struct['emails']) 44 | end 45 | 46 | def test_each_pair 47 | @struct.each_pair do |name, value| 48 | assert_equal @struct.__send__(name), value 49 | assert_equal @struct[name], value 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_invoke_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/abstract_unit' 2 | require 'action_web_service/test_invoke' 3 | 4 | class TestInvokeAPI < ActionWebService::API::Base 5 | api_method :null 6 | api_method :add, :expects => [:int, :int], :returns => [:int] 7 | end 8 | 9 | class TestInvokeService < ActionWebService::Base 10 | web_service_api TestInvokeAPI 11 | 12 | attr :invoked 13 | 14 | def add(a, b) 15 | @invoked = true 16 | a + b 17 | end 18 | 19 | def null 20 | end 21 | end 22 | 23 | class TestController < ActionController::Base 24 | def rescue_action(e); raise e; end 25 | end 26 | 27 | class TestInvokeDirectController < TestController 28 | web_service_api TestInvokeAPI 29 | 30 | attr :invoked 31 | 32 | def add 33 | @invoked = true 34 | @method_params[0] + @method_params[1] 35 | end 36 | 37 | def null 38 | end 39 | end 40 | 41 | class TestInvokeDelegatedController < TestController 42 | web_service_dispatching_mode :delegated 43 | web_service :service, TestInvokeService.new 44 | end 45 | 46 | class TestInvokeLayeredController < TestController 47 | web_service_dispatching_mode :layered 48 | web_service(:one) { @service_one ||= TestInvokeService.new } 49 | web_service(:two) { @service_two ||= TestInvokeService.new } 50 | end 51 | 52 | class TestInvokeTest < ActiveSupport::TestCase 53 | def setup 54 | @request = ActionController::TestRequest.new 55 | @response = ActionController::TestResponse.new 56 | end 57 | 58 | def test_direct_add 59 | @controller = TestInvokeDirectController.new 60 | assert_equal nil, @controller.invoked 61 | result = invoke :add, 25, 25 62 | assert_equal 50, result 63 | assert_equal true, @controller.invoked 64 | end 65 | 66 | def test_delegated_add 67 | @controller = TestInvokeDelegatedController.new 68 | assert_equal nil, @controller.web_service_object(:service).invoked 69 | result = invoke_delegated :service, :add, 100, 50 70 | assert_equal 150, result 71 | assert_equal true, @controller.web_service_object(:service).invoked 72 | end 73 | 74 | def test_layered_add 75 | [:soap, :xmlrpc].each do |protocol| 76 | @protocol = protocol 77 | [:one, :two].each do |service| 78 | @controller = TestInvokeLayeredController.new 79 | assert_equal nil, @controller.web_service_object(service).invoked 80 | result = invoke_layered service, :add, 200, -50 81 | assert_equal 150, result 82 | assert_equal true, @controller.web_service_object(service).invoked 83 | end 84 | end 85 | end 86 | 87 | def test_layered_fail_with_wrong_number_of_arguments 88 | [:soap, :xmlrpc].each do |protocol| 89 | @protocol = protocol 90 | [:one, :two].each do |service| 91 | @controller = TestInvokeLayeredController.new 92 | assert_raise(ArgumentError) { invoke_layered service, :add, 1 } 93 | end 94 | end 95 | end 96 | 97 | def test_delegated_fail_with_wrong_number_of_arguments 98 | @controller = TestInvokeDelegatedController.new 99 | assert_raise(ArgumentError) { invoke_delegated :service, :add, 1 } 100 | end 101 | 102 | def test_direct_fail_with_wrong_number_of_arguments 103 | @controller = TestInvokeDirectController.new 104 | assert_raise(ArgumentError) { invoke :add, 1 } 105 | end 106 | 107 | def test_with_no_parameters_declared 108 | @controller = TestInvokeDirectController.new 109 | assert_nil invoke(:null) 110 | end 111 | 112 | end 113 | --------------------------------------------------------------------------------