├── 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 |
--------------------------------------------------------------------------------