Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first pass of web service scaffolding. add ability to quickly generat…

…e an

action pack request for a protocol, add missing log_error when we fail to parse
protocol messages. add RDoc for scaffolding and functional testing.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1037 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit b94bd32f3116b469b48400382dbc964bf17994d1 1 parent 715715a
@bitserf bitserf authored
Showing with 451 additions and 28 deletions.
  1. +4 −0 actionwebservice/CHANGELOG
  2. +45 −0 actionwebservice/README
  3. +5 −1 actionwebservice/TODO
  4. +2 −0  actionwebservice/lib/action_web_service.rb
  5. +21 −0 actionwebservice/lib/action_web_service/api/base.rb
  6. +1 −0  actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb
  7. +54 −0 actionwebservice/lib/action_web_service/protocol/abstract.rb
  8. +7 −1 actionwebservice/lib/action_web_service/protocol/soap_protocol.rb
  9. +1 −1  actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb
  10. +181 −0 actionwebservice/lib/action_web_service/scaffolding.rb
  11. +65 −0 actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml
  12. +6 −0 actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml
  13. +20 −0 actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml
  14. +26 −0 actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml
  15. +0 −3  actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb
  16. +7 −3 actionwebservice/test/abstract_dispatcher.rb
  17. +4 −0 actionwebservice/test/api_test.rb
  18. +1 −10 actionwebservice/test/dispatcher_action_controller_soap_test.rb
  19. +1 −9 actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb
View
4 actionwebservice/CHANGELOG
@@ -1,7 +1,11 @@
*0.7.0* (Unreleased)
+* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser
+
* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC)
+* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC
+
*0.6.2* (27th March, 2005)
View
45 actionwebservice/README
@@ -197,6 +197,51 @@ For this example, a remote call for a method with a name like
method on the <tt>:mt</tt> service.
+== Testing your APIs
+
+
+=== Functional testing
+
+You can perform testing of your APIs by creating a functional test for the
+controller dispatching the API, and calling #invoke in the test case to
+perform the invocation.
+
+Example:
+
+ class PersonApiControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = PersonController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ end
+
+ def test_add
+ result = invoke :remove, 1
+ assert_equal true, result
+ end
+ end
+
+This example invokes the API method <tt>test</tt>, defined on
+the PersonController, and returns the result.
+
+
+=== Scaffolding
+
+You can also test your APIs with a web browser by attaching scaffolding
+to the controller.
+
+Example:
+
+ class PersonController
+ web_service_scaffold :invocation
+ end
+
+This creates an action named <tt>invocation</tt> on the PersonController.
+
+Navigating to this action lets you select the method to invoke, supply the parameters,
+and view the result of the invocation.
+
+
== Using the client support
Action Web Service includes client classes that can use the same API
View
6 actionwebservice/TODO
@@ -1,6 +1,10 @@
= 0.7.0
- WS Dynamic Scaffolding
- - WS Scaffolding Generators
+ * add protocol selection ability
+ * test with XML-RPC (namespaced method name support)
+ * support structured types as input parameters with the input field helper
+
+ - update manual for scaffolding and functional testing
= 0.8.0
- Consumption of WSDL services
View
2  actionwebservice/lib/action_web_service.rb
@@ -46,6 +46,7 @@
require 'action_web_service/protocol'
require 'action_web_service/struct'
require 'action_web_service/dispatcher'
+require 'action_web_service/scaffolding'
ActionWebService::Base.class_eval do
include ActionWebService::Container::Direct
@@ -61,4 +62,5 @@
include ActionWebService::Container::ActionController
include ActionWebService::Dispatcher
include ActionWebService::Dispatcher::ActionController
+ include ActionWebService::Scaffolding
end
View
21 actionwebservice/lib/action_web_service/api/base.rb
@@ -284,10 +284,31 @@ def cast_returns(marshaler, return_value)
marshaler.cast_inbound_recursive(return_value, @returns[0])
end
+ # String representation of this method
+ def to_s
+ fqn = ""
+ fqn << (@returns ? (friendly_param(@returns[0], nil) + " ") : "void ")
+ fqn << "#{@public_name}("
+ if @expects
+ i = 0
+ fqn << @expects.map{ |p| friendly_param(p, i+= 1) }.join(", ")
+ end
+ fqn << ")"
+ fqn
+ end
+
private
def response_name(encoder)
encoder.is_a?(WS::Encoding::SoapRpcEncoding) ? (@public_name + "Response") : @public_name
end
+
+ def friendly_param(spec, i)
+ name = param_name(spec, i)
+ type = param_type(spec)
+ spec = spec.values.first if spec.is_a?(Hash)
+ type = spec.is_a?(Array) ? (type.to_s + "[]") : type.to_s
+ i ? (type + " " + name) : type
+ end
end
end
end
View
1  actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb
@@ -63,6 +63,7 @@ def dispatch_web_service_request
end
else
exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
+ log_error(exception) unless logger.nil?
send_web_service_error_response(request, exception)
end
rescue Exception => e
View
54 actionwebservice/lib/action_web_service/protocol/abstract.rb
@@ -7,10 +7,64 @@ class AbstractProtocol
attr :marshaler
attr :encoder
+ def unmarshal_request(ap_request)
+ end
+
def marshal_response(method, return_value)
body = method.encode_rpc_response(marshaler, encoder, return_value)
Response.new(body, 'text/xml')
end
+
+ def protocol_client(api, protocol_name, endpoint_uri, options)
+ end
+
+ def create_action_pack_request(service_name, public_method_name, raw_body, options={})
+ klass = options[:request_class] || SimpleActionPackRequest
+ request = klass.new
+ request.request_parameters['action'] = service_name.to_s
+ request.env['RAW_POST_DATA'] = raw_body
+ request.env['REQUEST_METHOD'] = 'POST'
+ request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
+ request
+ end
+ end
+
+ class SimpleActionPackRequest < ActionController::AbstractRequest
+ def initialize
+ @env = {}
+ @qparams = {}
+ @rparams = {}
+ @cookies = {}
+ reset_session
+ end
+
+ def query_parameters
+ @qparams
+ end
+
+ def request_parameters
+ @rparams
+ end
+
+ def env
+ @env
+ end
+
+ def host
+ ''
+ end
+
+ def cookies
+ @cookies
+ end
+
+ def session
+ @session
+ end
+
+ def reset_session
+ @session = {}
+ end
end
class Request # :nodoc:
View
8 actionwebservice/lib/action_web_service/protocol/soap_protocol.rb
@@ -20,11 +20,17 @@ def unmarshal_request(ap_request)
Request.new(self, method_name, params, service_name)
end
- def protocol_client(api, protocol_name, endpoint_uri, options)
+ def protocol_client(api, protocol_name, endpoint_uri, options={})
return nil unless protocol_name == :soap
ActionWebService::Client::Soap.new(api, endpoint_uri, options)
end
+ def create_action_pack_request(service_name, public_method_name, raw_body, options={})
+ request = super
+ request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name]
+ request
+ end
+
private
def has_valid_soap_action?(request)
return nil unless request.method == :post
View
2  actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb
@@ -18,7 +18,7 @@ def unmarshal_request(ap_request)
Request.new(self, method_name, params, service_name)
end
- def protocol_client(api, protocol_name, endpoint_uri, options)
+ def protocol_client(api, protocol_name, endpoint_uri, options={})
return nil unless protocol_name == :xmlrpc
ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options)
end
View
181 actionwebservice/lib/action_web_service/scaffolding.rb
@@ -0,0 +1,181 @@
+require 'ostruct'
+require 'uri'
+
+module ActionWebService
+ module Scaffolding # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The
+ # generated scaffold actions have default views to let you enter the method parameters and view the
+ # results.
+ #
+ # Example:
+ #
+ # class ApiController < ActionController
+ # web_service_scaffold :invoke
+ # end
+ #
+ # This example generates an +invoke+ action in the +ApiController+ that you can navigate to from
+ # your browser, select the API method, enter its parameters, and perform the invocation.
+ #
+ # If you want to customize the default views, create the following views in "app/views":
+ #
+ # * <tt>action_name/methods.rhtml</tt>
+ # * <tt>action_name/parameters.rhtml</tt>
+ # * <tt>action_name/result.rhtml</tt>
+ # * <tt>action_name/layout.rhtml</tt>
+ #
+ # Where <tt>action_name</tt> is the name of the action you gave to ClassMethods#web_service_scaffold.
+ #
+ # You can use the default views in <tt>RAILS_DIR/lib/action_web_service/templates/scaffolds</tt> as
+ # a guide.
+ module ClassMethods
+ # Generates web service invocation scaffolding for the current controller. The given action name
+ # can then be used as the entry point for invoking API methods from a web browser.
+ def web_service_scaffold(action_name)
+ add_template_helper(Helpers)
+ module_eval <<-END, __FILE__, __LINE__
+ def #{action_name}
+ if @request.method == :get
+ setup_#{action_name}_assigns
+ render_#{action_name}_scaffold 'methods'
+ end
+ end
+
+ def #{action_name}_method_params
+ if @request.method == :get
+ setup_#{action_name}_assigns
+ render_#{action_name}_scaffold 'parameters'
+ end
+ end
+
+ def #{action_name}_submit
+ if @request.method == :post
+ setup_#{action_name}_assigns
+ protocol_name = @params['protocol'] ? @params['protocol'].to_sym : :soap
+ case protocol_name
+ when :soap
+ protocol = Protocol::Soap::SoapProtocol.new
+ when :xmlrpc
+ protocol = Protocol::XmlRpc::XmlRpcProtocol.new
+ end
+ @method_request_xml = @scaffold_method.encode_rpc_call(protocol.marshaler, protocol.encoder, @params['method_params'].dup)
+ cgi = @request.cgi
+ @request = protocol.create_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml)
+ dispatch_web_service_request
+ @method_response_xml = @response.body
+ @method_return_value = protocol.marshaler.unmarshal(protocol.encoder.decode_rpc_response(@method_response_xml)[1]).value
+ add_instance_variables_to_assigns
+ @response = ::ActionController::CgiResponse.new(cgi)
+ @performed_render = false
+ render_#{action_name}_scaffold 'result'
+ end
+ end
+
+ private
+ def setup_#{action_name}_assigns
+ @scaffold_class = self.class
+ @scaffold_action_name = "#{action_name}"
+ @scaffold_container = WebServiceModel::Container.new(self)
+ if @params['service'] && @params['method']
+ @scaffold_service = @scaffold_container.services.find{ |x| x.name == @params['service'] }
+ @scaffold_method = @scaffold_service.api_methods[@params['method']]
+ end
+ add_instance_variables_to_assigns
+ end
+
+ def render_#{action_name}_scaffold(action)
+ customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
+ default_template = scaffold_path(action)
+ @content_for_layout = template_exists?(customized_template) ? @template.render_file(customized_template) : @template.render_file(default_template, false)
+ self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout"))
+ end
+
+ def scaffold_path(template_name)
+ File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
+ end
+ END
+ end
+ end
+
+ module Helpers # :nodoc:
+ def method_parameter_input_fields(method, param_spec, i)
+ klass = method.param_class(param_spec)
+ unless WS::BaseTypes.base_type?(klass)
+ name = method.param_name(param_spec, i)
+ raise "Parameter #{name}: Structured/array types not supported in scaffolding input fields yet"
+ end
+ type_name = method.param_type(param_spec)
+ field_name = "method_params[]"
+ case type_name
+ when :int
+ text_field_tag field_name
+ when :string
+ text_field_tag field_name
+ when :bool
+ radio_button_tag field_name, "True"
+ radio_button_tag field_name, "False"
+ when :float
+ text_field_tag field_name
+ when :time
+ select_datetime Time.now, 'name' => field_name
+ when :date
+ select_date Date.today, 'name' => field_name
+ end
+ end
+
+ def service_method_list(service)
+ action = @scaffold_action_name + '_method_params'
+ methods = service.api_methods_full.map do |desc, name|
+ content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name))
+ end
+ content_tag("ul", methods.join("\n"))
+ end
+ end
+
+ module WebServiceModel # :nodoc:
+ class Container # :nodoc:
+ attr :services
+
+ def initialize(real_container)
+ @real_container = real_container
+ @services = []
+ if @real_container.class.web_service_dispatching_mode == :direct
+ @services << Service.new(@real_container.controller_name, @real_container)
+ else
+ @real_container.class.web_services.each do |name|
+ @services << Service.new(name, @real_container.instance_eval{ web_service_object(name) })
+ end
+ end
+ end
+ end
+
+ class Service # :nodoc:
+ attr :name
+ attr :object
+ attr :api
+ attr :api_methods
+ attr :api_methods_full
+
+ def initialize(name, real_service)
+ @name = name.to_s
+ @object = real_service
+ @api = @object.class.web_service_api
+ @api_methods = {}
+ @api_methods_full = []
+ @api.api_methods.each do |name, method|
+ @api_methods[method.public_name.to_s] = method
+ @api_methods_full << [method.to_s, method.public_name.to_s]
+ end
+ end
+
+ def to_s
+ self.name.camelize
+ end
+ end
+ end
+ end
+end
View
65 actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml
@@ -0,0 +1,65 @@
+<html>
+<head>
+ <title><%= @scaffold_class.wsdl_service_name %> Web Service</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+
+ .fieldWithErrors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+ }
+
+ #ErrorExplanation {
+ width: 400px;
+ border: 2px solid #red;
+ padding: 7px;
+ padding-bottom: 12px;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+ }
+
+ #ErrorExplanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ background-color: #c00;
+ color: #fff;
+ }
+
+ #ErrorExplanation p {
+ color: #333;
+ margin-bottom: 0;
+ padding: 5px;
+ }
+
+ #ErrorExplanation ul li {
+ font-size: 12px;
+ list-style: square;
+ }
+ </style>
+</head>
+<body>
+
+<%= @content_for_layout %>
+
+</body>
+</html>
View
6 actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml
@@ -0,0 +1,6 @@
+<% @scaffold_container.services.each do |service| %>
+
+ <h4>API Methods for <%= service %></h4>
+ <%= service_method_list(service) %>
+
+<% end %>
View
20 actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml
@@ -0,0 +1,20 @@
+<h4>Method Parameters for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
+
+<%= form_tag :action => @scaffold_action_name + '_submit' %>
+<%= hidden_field_tag "service", @scaffold_service.name %>
+<%= hidden_field_tag "method", @scaffold_method.public_name %>
+
+<% i = 0 %>
+<% @scaffold_method.expects.each do |spec| %>
+ <p>
+ <label for="method_params[]"><%= @scaffold_method.param_name(spec, i).camelize %></label><br />
+ <%= method_parameter_input_fields(@scaffold_method, spec, i) %>
+ </p>
+ <% i += 1 %>
+<% end %>
+
+<%= submit_tag "Invoke" %>
+
+<p>
+<%= link_to "Back", :action => @scaffold_action_name %>
+</p>
View
26 actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml
@@ -0,0 +1,26 @@
+<h4>Method Invocation Result for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
+
+<p>
+<strong>Return Value:</strong><br />
+<pre>
+<%= h @method_return_value.inspect %>
+</pre>
+</p>
+
+<p>
+<strong>Request XML:</strong><br />
+<pre>
+<%= h @method_request_xml %>
+</pre>
+</p>
+
+<p>
+<strong>Response XML:</strong><br />
+<pre>
+<%= h @method_response_xml %>
+</pre>
+</p>
+
+<p>
+<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %>
+</p>
View
3  actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb
@@ -2,9 +2,6 @@
module WS
module Encoding
- class XmlRpcError < WSError
- end
-
class XmlRpcEncoding < AbstractEncoding
def encode_rpc_call(method_name, params)
XMLRPC::Marshal.dump_call(method_name, *params)
View
10 actionwebservice/test/abstract_dispatcher.rb
@@ -301,7 +301,8 @@ def test_garbage_request
[@direct_controller, @delegated_controller].each do |controller|
controller.class.web_service_exception_reporting = true
send_garbage_request = lambda do
- request = create_ap_request(controller, 'invalid request body', 'xxx')
+ service_name = service_name(controller)
+ request = @protocol.create_action_pack_request(service_name, 'broken, method, name!', 'broken request body', :request_class => ActionController::TestRequest)
response = ActionController::TestResponse.new
controller.process(request, response)
# puts response.body
@@ -378,22 +379,25 @@ def do_method_call(container, public_method_name, *params)
mode = container.web_service_dispatching_mode
case mode
when :direct
+ service_name = service_name(container)
api = container.class.web_service_api
when :delegated
- api = container.web_service_object(service_name(container)).class.web_service_api
+ service_name = service_name(container)
+ api = container.web_service_object(service_name).class.web_service_api
when :layered
service_name = nil
if public_method_name =~ /^([^\.]+)\.(.*)$/
service_name = $1
end
api = container.web_service_object(service_name.to_sym).class.web_service_api
+ service_name = self.service_name(container)
end
method = api.public_api_method_instance(public_method_name)
method ||= api.dummy_public_api_method_instance(public_method_name)
# we turn off strict so we can test our own handling of incorrectly typed parameters
body = method.encode_rpc_call(@marshaler, @encoder, params.dup, :strict => false)
# puts body
- ap_request = create_ap_request(container, body, public_method_name, *params)
+ ap_request = protocol.create_action_pack_request(service_name, public_method_name, body, :request_class => ActionController::TestRequest)
ap_response = ActionController::TestResponse.new
container.process(ap_request, ap_response)
# puts ap_response.body
View
4 actionwebservice/test/api_test.rb
@@ -73,4 +73,8 @@ def test_api_errors
end
end
end
+
+ def test_to_s
+ assert_equal 'void Expects(int p1, bool p2)', APITest::API.api_methods[:expects].to_s
+ end
end
View
11 actionwebservice/test/dispatcher_action_controller_soap_test.rb
@@ -28,6 +28,7 @@ def setup
@direct_controller = DirectController.new
@delegated_controller = DelegatedController.new
@virtual_controller = VirtualController.new
+ @protocol = ActionWebService::Protocol::Soap::SoapProtocol.new
end
def test_wsdl_generation
@@ -70,16 +71,6 @@ def is_exception?(obj)
obj.detail.cause.is_a?(Exception)
end
- def create_ap_request(container, body, public_method_name, *args)
- test_request = ActionController::TestRequest.new
- test_request.request_parameters['action'] = service_name(container)
- test_request.env['REQUEST_METHOD'] = "POST"
- test_request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
- test_request.env['HTTP_SOAPACTION'] = "/soap/#{service_name(container)}/#{public_method_name}"
- test_request.env['RAW_POST_DATA'] = body
- test_request
- end
-
def service_name(container)
container.is_a?(DelegatedController) ? 'test_service' : 'api'
end
View
10 actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb
@@ -5,6 +5,7 @@ class TC_DispatcherActionControllerXmlRpc < Test::Unit::TestCase
include DispatcherCommonTests
def setup
+ @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new
@encoder = WS::Encoding::XmlRpcEncoding.new
@marshaler = WS::Marshaling::XmlRpcMarshaler.new
@direct_controller = DirectController.new
@@ -29,15 +30,6 @@ def is_exception?(obj)
obj.is_a?(XMLRPC::FaultException)
end
- def create_ap_request(container, body, public_method_name, *args)
- test_request = ActionController::TestRequest.new
- test_request.request_parameters['action'] = service_name(container)
- test_request.env['REQUEST_METHOD'] = "POST"
- test_request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
- test_request.env['RAW_POST_DATA'] = body
- test_request
- end
-
def service_name(container)
container.is_a?(DelegatedController) ? 'test_service' : 'api'
end
Please sign in to comment.
Something went wrong with that request. Please try again.