Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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
Leon Breedt 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
4 actionwebservice/CHANGELOG
View
@@ -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)
45 actionwebservice/README
View
@@ -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
6 actionwebservice/TODO
View
@@ -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
2  actionwebservice/lib/action_web_service.rb
View
@@ -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
21 actionwebservice/lib/action_web_service/api/base.rb
View
@@ -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
1  actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb
View
@@ -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
54 actionwebservice/lib/action_web_service/protocol/abstract.rb
View
@@ -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:
8 actionwebservice/lib/action_web_service/protocol/soap_protocol.rb
View
@@ -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
2  actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb
View
@@ -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
181 actionwebservice/lib/action_web_service/scaffolding.rb
View
@@ -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
65 actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml
View
@@ -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>
6 actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml
View
@@ -0,0 +1,6 @@
+<% @scaffold_container.services.each do |service| %>
+
+ <h4>API Methods for <%= service %></h4>
+ <%= service_method_list(service) %>
+
+<% end %>
20 actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml
View
@@ -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>
26 actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml
View
@@ -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>
3  actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb
View
@@ -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)
10 actionwebservice/test/abstract_dispatcher.rb
View
@@ -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
4 actionwebservice/test/api_test.rb
View
@@ -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
11 actionwebservice/test/dispatcher_action_controller_soap_test.rb
View
@@ -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
10 actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb
View
@@ -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.