From b94bd32f3116b469b48400382dbc964bf17994d1 Mon Sep 17 00:00:00 2001 From: Leon Breedt Date: Tue, 29 Mar 2005 12:31:39 +0000 Subject: [PATCH] first pass of web service scaffolding. add ability to quickly generate 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 --- actionwebservice/CHANGELOG | 4 + actionwebservice/README | 45 +++++ actionwebservice/TODO | 6 +- actionwebservice/lib/action_web_service.rb | 2 + .../lib/action_web_service/api/base.rb | 21 ++ .../action_controller_dispatcher.rb | 1 + .../action_web_service/protocol/abstract.rb | 54 ++++++ .../protocol/soap_protocol.rb | 8 +- .../protocol/xmlrpc_protocol.rb | 2 +- .../lib/action_web_service/scaffolding.rb | 181 ++++++++++++++++++ .../templates/scaffolds/layout.rhtml | 65 +++++++ .../templates/scaffolds/methods.rhtml | 6 + .../templates/scaffolds/parameters.rhtml | 20 ++ .../templates/scaffolds/result.rhtml | 26 +++ .../vendor/ws/encoding/xmlrpc_encoding.rb | 3 - actionwebservice/test/abstract_dispatcher.rb | 10 +- actionwebservice/test/api_test.rb | 4 + .../dispatcher_action_controller_soap_test.rb | 11 +- ...ispatcher_action_controller_xmlrpc_test.rb | 10 +- 19 files changed, 451 insertions(+), 28 deletions(-) create mode 100644 actionwebservice/lib/action_web_service/scaffolding.rb create mode 100644 actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml create mode 100644 actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml create mode 100644 actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml create mode 100644 actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml diff --git a/actionwebservice/CHANGELOG b/actionwebservice/CHANGELOG index ed57f6fa10..6fcdaf971e 100644 --- a/actionwebservice/CHANGELOG +++ b/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) diff --git a/actionwebservice/README b/actionwebservice/README index 1d93bde02f..d6bcea7534 100644 --- a/actionwebservice/README +++ b/actionwebservice/README @@ -197,6 +197,51 @@ For this example, a remote call for a method with a name like method on the :mt 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 test, 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 invocation 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 diff --git a/actionwebservice/TODO b/actionwebservice/TODO index af1d6ff5fc..cf5c11a795 100644 --- a/actionwebservice/TODO +++ b/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 diff --git a/actionwebservice/lib/action_web_service.rb b/actionwebservice/lib/action_web_service.rb index 61e451b704..d8dc132313 100644 --- a/actionwebservice/lib/action_web_service.rb +++ b/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 diff --git a/actionwebservice/lib/action_web_service/api/base.rb b/actionwebservice/lib/action_web_service/api/base.rb index e440a8b1bd..03e406cfc3 100644 --- a/actionwebservice/lib/action_web_service/api/base.rb +++ b/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 diff --git a/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb b/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb index 3c9a6e2c78..0140039c49 100644 --- a/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb +++ b/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 diff --git a/actionwebservice/lib/action_web_service/protocol/abstract.rb b/actionwebservice/lib/action_web_service/protocol/abstract.rb index ed50a6ffde..0ff4feef84 100644 --- a/actionwebservice/lib/action_web_service/protocol/abstract.rb +++ b/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: diff --git a/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb b/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb index 253812b5e2..5e56748ae3 100644 --- a/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb +++ b/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 diff --git a/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb b/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb index 37ef16c12f..f8ff12cfa3 100644 --- a/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb +++ b/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 diff --git a/actionwebservice/lib/action_web_service/scaffolding.rb b/actionwebservice/lib/action_web_service/scaffolding.rb new file mode 100644 index 0000000000..21790c101b --- /dev/null +++ b/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": + # + # * action_name/methods.rhtml + # * action_name/parameters.rhtml + # * action_name/result.rhtml + # * action_name/layout.rhtml + # + # Where action_name is the name of the action you gave to ClassMethods#web_service_scaffold. + # + # You can use the default views in RAILS_DIR/lib/action_web_service/templates/scaffolds 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 diff --git a/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml b/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml new file mode 100644 index 0000000000..1cd2e3042a --- /dev/null +++ b/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml @@ -0,0 +1,65 @@ + + + <%= @scaffold_class.wsdl_service_name %> Web Service + + + + +<%= @content_for_layout %> + + + diff --git a/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml b/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml new file mode 100644 index 0000000000..60dfe23f07 --- /dev/null +++ b/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml @@ -0,0 +1,6 @@ +<% @scaffold_container.services.each do |service| %> + +

API Methods for <%= service %>

+ <%= service_method_list(service) %> + +<% end %> diff --git a/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml b/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml new file mode 100644 index 0000000000..0516738da6 --- /dev/null +++ b/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml @@ -0,0 +1,20 @@ +

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

+ +<%= 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| %> +

+
+ <%= method_parameter_input_fields(@scaffold_method, spec, i) %> +

+ <% i += 1 %> +<% end %> + +<%= submit_tag "Invoke" %> + +

+<%= link_to "Back", :action => @scaffold_action_name %> +

diff --git a/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml b/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml new file mode 100644 index 0000000000..4475abff4a --- /dev/null +++ b/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml @@ -0,0 +1,26 @@ +

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

+ +

+Return Value:
+

+<%= h @method_return_value.inspect %>
+
+

+ +

+Request XML:
+

+<%= h @method_request_xml %>
+
+

+ +

+Response XML:
+

+<%= h @method_response_xml %>
+
+

+ +

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

diff --git a/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb b/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb index 122f62434f..4876c08705 100644 --- a/actionwebservice/lib/action_web_service/vendor/ws/encoding/xmlrpc_encoding.rb +++ b/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) diff --git a/actionwebservice/test/abstract_dispatcher.rb b/actionwebservice/test/abstract_dispatcher.rb index 21b9a0f5a9..53be05f9f2 100644 --- a/actionwebservice/test/abstract_dispatcher.rb +++ b/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 diff --git a/actionwebservice/test/api_test.rb b/actionwebservice/test/api_test.rb index 42b1dfbef2..4a61ae8de7 100644 --- a/actionwebservice/test/api_test.rb +++ b/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 diff --git a/actionwebservice/test/dispatcher_action_controller_soap_test.rb b/actionwebservice/test/dispatcher_action_controller_soap_test.rb index 400ab40dd6..76fc6094c2 100644 --- a/actionwebservice/test/dispatcher_action_controller_soap_test.rb +++ b/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 diff --git a/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb b/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb index 81ada70c9e..c92f270a1b 100644 --- a/actionwebservice/test/dispatcher_action_controller_xmlrpc_test.rb +++ b/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 -- GitLab