提交 b94bd32f 编写于 作者: L Leon Breedt

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
上级 715715ae
*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)
......
......@@ -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
......
= 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
......
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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:
......
......@@ -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
......
......@@ -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
......
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
<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>
<% @scaffold_container.services.each do |service| %>
<h4>API Methods for <%= service %></h4>
<%= service_method_list(service) %>
<% end %>
<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>
<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>
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册