From 03d37a2d68c1940f65d5a65a51ae747955a5b075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Sun, 5 Mar 2006 18:59:58 +0000 Subject: [PATCH] Added new infrastructure support for REST webservices. By default application/xml posts are handled by creating a XmlNode object with the same name as the root element of the submitted xml. M$ ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| node = REXML::Document.new(post) { node.root.name => node.root } end XmlSimple and Yaml web services were retired, ActionController::Base.param_parsers carries an example which shows how to get this functio$ request.[formatted_post?, xml_post?, yaml_post? and post_format] were all deprecated in favor of request.content_type [Tobias Luetke] Closes #4081 git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3777 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 11 ++ actionpack/lib/action_controller.rb | 2 + actionpack/lib/action_controller/base.rb | 35 ++++- .../action_controller/cgi_ext/cgi_methods.rb | 8 +- .../lib/action_controller/cgi_process.rb | 4 +- .../deprecated_request_methods.rb | 34 ++++ actionpack/lib/action_controller/request.rb | 49 ++---- .../lib/action_controller/vendor/xml_node.rb | 98 ++++++++++++ actionpack/test/abstract_unit.rb | 4 +- actionpack/test/controller/webservice_test.rb | 146 ++++++++++++++++++ 10 files changed, 345 insertions(+), 46 deletions(-) create mode 100644 actionpack/lib/action_controller/deprecated_request_methods.rb create mode 100644 actionpack/lib/action_controller/vendor/xml_node.rb create mode 100644 actionpack/test/controller/webservice_test.rb diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 49fd59c72e..58c75c44c5 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,16 @@ *SVN* +* Added new infrastructure support for REST webservices. + By default application/xml posts are handled by creating a XmlNode object with the same name as the root element of the submitted xml. More handlers can easily be registered like this + + ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| + node = REXML::Document.new(post) + { node.root.name => node.root } + end + + XmlSimple and Yaml web services were retired, ActionController::Base.param_parsers carries an example which shows how to get this functionality back. + request.[formatted_post?, xml_post?, yaml_post? and post_format] were all deprecated in favor of request.content_type [Tobias Luetke] + * Fixed Effect.Appear in effects.js to work with floats in Safari #3524, #3813, #3044 [Thomas Fuchs] * Fixed that default image extension was not appended when using a full URL with AssetTagHelper#image_tag #4032, #3728 [rubyonrails@beautifulpixel.com] diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 80772f7581..b51e5b45bc 100755 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -36,6 +36,8 @@ end require 'action_controller/base' require 'action_controller/deprecated_redirects' +require 'action_controller/request' +require 'action_controller/deprecated_request_methods' require 'action_controller/rescue' require 'action_controller/benchmarking' require 'action_controller/flash' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index da27edd68c..ab8cc1802c 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -260,10 +260,41 @@ module ActionController #:nodoc: @@allow_concurrency = false cattr_accessor :allow_concurrency + # Modern REST web services often need to submit complex data to the web application. + # the param_parsers hash lets you register handlers wich will process the http body and add parameters to the + # @params hash. These handlers are invoked for post and put requests. + # + # By default application/xml is enabled. a XmlNode class with the same param name as the root + # will be instanciated in the @params. + # + # Example: + # + # ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| + # node = REXML::Document.new(post) + # { node.root.name => node.root } + # end + # + # Note: Up until release 1.1 rails would default to using XmlSimple for such requests. To get the old + # behavior you can re-register XmlSimple as application/xml handler and enable application/x-yaml like + # this: + # + # ActionController::Base.param_parsers['application/xml'] = Proc.new do |data| + # XmlSimple.xml_in(data, 'ForceArray' => false) + # end + # + # ActionController::Base.param_parsers['application/x-yaml'] = Proc.new do |data| + # |post| YAML.load(post) + # end + # + @@param_parsers = { + 'application/xml' => Proc.new { |post| node = XmlNode.from_xml(post); { node.node_name => node } }, + } + cattr_accessor :param_parsers + # Template root determines the base from which template references will be made. So a call to render("test/template") # will be converted to "#{template_root}/test/template.rhtml". class_inheritable_accessor :template_root - + # The logger is used for generating information on the action run-time (including benchmarking) if available. # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers. cattr_accessor :logger @@ -302,7 +333,7 @@ module ActionController #:nodoc: # Returns the name of the action this controller is processing. attr_accessor :action_name - + class << self # Factory for the standard create, process loop where the controller is discarded after processing. def process(request, response) #:nodoc: diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb index a2ba601662..89f982f56f 100755 --- a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb +++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb @@ -1,5 +1,6 @@ require 'cgi' require 'action_controller/vendor/xml_simple' +require 'action_controller/vendor/xml_node' # Static methods for parsing the query and request parameters that can be used in # a CGI extension class or testing in isolation. @@ -58,12 +59,7 @@ class CGIMethods #:nodoc: end def self.parse_formatted_request_parameters(format, raw_post_data) - case format - when :xml - return XmlSimple.xml_in(raw_post_data, 'ForceArray' => false) - when :yaml - return YAML.load(raw_post_data) - end + ActionController::Base.param_parsers[format].call(raw_post_data) || {} rescue Object => e { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace, "raw_post_data" => raw_post_data, "format" => format } diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb index 3e02fbada6..508dc016ac 100644 --- a/actionpack/lib/action_controller/cgi_process.rb +++ b/actionpack/lib/action_controller/cgi_process.rb @@ -64,8 +64,8 @@ module ActionController #:nodoc: end def request_parameters - if formatted_post? - CGIMethods.parse_formatted_request_parameters(post_format, @env['RAW_POST_DATA']) + if ActionController::Base.param_parsers.has_key?(content_type) + CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA']) else CGIMethods.parse_request_parameters(@cgi.params) end diff --git a/actionpack/lib/action_controller/deprecated_request_methods.rb b/actionpack/lib/action_controller/deprecated_request_methods.rb new file mode 100644 index 0000000000..0364831873 --- /dev/null +++ b/actionpack/lib/action_controller/deprecated_request_methods.rb @@ -0,0 +1,34 @@ +module ActionController + class AbstractRequest + # Determine whether the body of a HTTP call is URL-encoded (default) + # or matches one of the registered param_parsers. + # + # For backward compatibility, the post format is extracted from the + # X-Post-Data-Format HTTP header if present. + def post_format + case content_type + when 'application/xml' + :xml + when 'application/x-yaml' + :yaml + else + :url_encoded + end + end + + # Is this a POST request formatted as XML or YAML? + def formatted_post? + post? && (post_format == :yaml || post_format == :xml) + end + + # Is this a POST request formatted as XML? + def xml_post? + post? && post_format == :xml + end + + # Is this a POST request formatted as YAML? + def yaml_post? + post? && post_format == :yaml + end + end +end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 0daa229448..06b6601fb4 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -42,44 +42,25 @@ module ActionController method == :head end - # Determine whether the body of a POST request is URL-encoded (default), - # XML, or YAML by checking the Content-Type HTTP header: - # - # Content-Type Post Format - # application/xml :xml - # text/xml :xml - # application/x-yaml :yaml - # text/x-yaml :yaml - # * :url_encoded + # Determine whether the body of a HTTP call is URL-encoded (default) + # or matches one of the registered param_parsers. # # For backward compatibility, the post format is extracted from the # X-Post-Data-Format HTTP header if present. - def post_format - @post_format ||= - if @env['HTTP_X_POST_DATA_FORMAT'] - @env['HTTP_X_POST_DATA_FORMAT'].downcase.to_sym - else - case @env['CONTENT_TYPE'].to_s.downcase - when 'application/xml', 'text/xml' then :xml - when 'application/x-yaml', 'text/x-yaml' then :yaml - else :url_encoded - end - end - end + def content_type + return @content_type if @content_type - # Is this a POST request formatted as XML or YAML? - def formatted_post? - post? && (post_format == :xml || post_format == :yaml) - end - - # Is this a POST request formatted as XML? - def xml_post? - post? && post_format == :xml - end - - # Is this a POST request formatted as YAML? - def yaml_post? - post? && post_format == :yaml + @content_type = @env['CONTENT_TYPE'].to_s.downcase + + if @env['HTTP_X_POST_DATA_FORMAT'] + case @env['HTTP_X_POST_DATA_FORMAT'].downcase.to_sym + when :yaml + @content_type = 'application/x-yaml' + when :xml + @content_type = 'application/xml' + end + end + @content_type end # Returns true if the request's "X-Requested-With" header contains diff --git a/actionpack/lib/action_controller/vendor/xml_node.rb b/actionpack/lib/action_controller/vendor/xml_node.rb new file mode 100644 index 0000000000..c4e2f98fbf --- /dev/null +++ b/actionpack/lib/action_controller/vendor/xml_node.rb @@ -0,0 +1,98 @@ +require 'rexml/document' + +# SimpleXML like xml parser. Written by leon breet from the ruby on rails Mailing list +# +class XmlNode + attr :node + + def initialize(node, options = {}) + @node = node + @children = {} + @raise_errors = options[:raise_errors] + end + + def self.from_xml(xml_or_io) + document = REXML::Document.new(xml_or_io) + if document.root + XmlNode.new(document.root) + else + XmlNode.new(document) + end + end + + def node_encoding + @node.encoding + end + + def node_name + @node.name + end + + def node_value + @node.text + end + + def node_value=(value) + @node.text = value + end + + def xpath(expr) + matches = nil + REXML::XPath.each(@node, expr) do |element| + matches ||= XmlNodeList.new + matches << (@children[element] ||= XmlNode.new(element)) + end + matches + end + + def method_missing(name, *args) + name = name.to_s + nodes = nil + @node.each_element(name) do |element| + nodes ||= XmlNodeList.new + nodes << (@children[element] ||= XmlNode.new(element)) + end + nodes + end + + def <<(node) + if node.is_a? REXML::Node + child = node + elsif node.respond_to? :node + child = node.node + end + @node.add_element child + @children[child] ||= XmlNode.new(child) + end + + def [](name) + @node.attributes[name.to_s] + end + + def []=(name, value) + @node.attributes[name.to_s] = value + end + + def to_s + @node.to_s + end + + def to_i + to_s.to_i + end +end + +class XmlNodeList < Array + def [](i) + i.is_a?(String) ? super(0)[i] : super(i) + end + + def []=(i, value) + i.is_a?(String) ? self[0][i] = value : super(i, value) + end + + def method_missing(name, *args) + name = name.to_s + self[0].__send__(name, *args) + end +end \ No newline at end of file diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index eaf70322c3..94fcae4caf 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -2,6 +2,7 @@ $:.unshift(File.dirname(__FILE__) + '/../lib') $:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') +require 'yaml' require 'test/unit' require 'action_controller' require 'breakpoint' @@ -10,5 +11,4 @@ require 'action_controller/test_process' ActionController::Base.logger = nil ActionController::Base.ignore_missing_templates = false -ActionController::Routing::Routes.reload rescue nil - +ActionController::Routing::Routes.reload rescue nil \ No newline at end of file diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb new file mode 100644 index 0000000000..844ac723ba --- /dev/null +++ b/actionpack/test/controller/webservice_test.rb @@ -0,0 +1,146 @@ +require File.dirname(__FILE__) + '/../abstract_unit' +require 'stringio' + +class WebServiceTest < Test::Unit::TestCase + + class MockCGI < CGI #:nodoc: + attr_accessor :stdinput, :stdoutput, :env_table + + def initialize(env, data = '') + self.env_table = env + self.stdinput = StringIO.new(data) + self.stdoutput = StringIO.new + super() + end + end + + + class TestController < ActionController::Base + session :off + + def assign_parameters + render :text => (@params.keys - ['controller', 'action']).sort.join(", ") + end + + def rescue_action(e) raise end + end + + def setup + @controller = TestController.new + end + + def test_check_parameters + process('GET') + assert_equal '', @controller.response.body + end + + def test_post_xml + process('POST', 'application/xml', 'content...') + + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'content...', @controller.params["entry"].summary.node_value + assert_equal 'true', @controller.params["entry"]['attributed'] + end + + def test_put_xml + process('PUT', 'application/xml', 'content...') + + assert_equal 'entry', @controller.response.body + assert @controller.params.has_key?(:entry) + assert_equal 'content...', @controller.params["entry"].summary.node_value + assert_equal 'true', @controller.params["entry"]['attributed'] + end + + def test_register_and_use_yaml + ActionController::Base.param_parsers['application/x-yaml'] = Proc.new { |d| YAML.load(d) } + process('POST', 'application/x-yaml', {"entry" => "loaded from yaml"}.to_yaml) + assert @controller.params.has_key?(:entry) + assert_equal 'loaded from yaml', @controller.params["entry"] + ensure + ActionController::Base.param_parsers['application/x-yaml'] = nil + end + + + def test_deprecated_request_methods + process('POST', 'application/x-yaml') + assert_equal 'application/x-yaml', @controller.request.content_type + assert_equal true, @controller.request.post? + assert_equal :yaml, @controller.request.post_format + assert_equal true, @controller.request.yaml_post? + assert_equal false, @controller.request.xml_post? + end + + + private + + def process(verb, content_type = 'application/x-www-form-urlencoded', data = '') + + cgi = MockCGI.new({ + 'REQUEST_METHOD' => verb, + 'CONTENT_TYPE' => content_type, + 'QUERY_STRING' => "action=assign_parameters&controller=webservicetest/test", + "REQUEST_URI" => "/", + "HTTP_HOST" => 'testdomain.com', + "CONTENT_LENGTH" => data.size, + "SERVER_PORT" => "80", + "HTTPS" => "off"}, data) + + @controller.send(:process, ActionController::CgiRequest.new(cgi, {}), ActionController::CgiResponse.new(cgi)) + end + +end + + +class XmlNodeTest < Test::Unit::TestCase + def test_all + xn = XmlNode.from_xml(%{ + + + With O'Reilly and Adaptive Path + + + Staying at the Savoy + + + + + + + + + } + ) + assert_equal 'UTF-8', xn.node.document.encoding + assert_equal '1.0', xn.node.document.version + assert_equal 'true', xn['success'] + assert_equal 'response', xn.node_name + assert_equal 'Ajax Summit', xn.page['title'] + assert_equal '1133', xn.page['id'] + assert_equal "With O'Reilly and Adaptive Path", xn.page.description.node_value + assert_equal nil, xn.nonexistent + assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip + assert_equal 'Technology', xn.page.tags.tag[0]['name'] + assert_equal 'Travel', xn.page.tags.tag[1][:name] + matches = xn.xpath('//@id').map{ |id| id.to_i } + assert_equal [4, 5, 1020, 1133], matches.sort + matches = xn.xpath('//tag').map{ |tag| tag['name'] } + assert_equal ['Technology', 'Travel'], matches.sort + assert_equal "Ajax Summit", xn.page['title'] + xn.page['title'] = 'Ajax Summit V2' + assert_equal "Ajax Summit V2", xn.page['title'] + assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip + xn.page.notes.note.node_value = "Staying at the Ritz" + assert_equal "Staying at the Ritz", xn.page.notes.note.node_value.strip + assert_equal '5', xn.page.tags.tag[1][:id] + xn.page.tags.tag[1]['id'] = '7' + assert_equal '7', xn.page.tags.tag[1]['id'] + end + + + def test_small_entry + node = XmlNode.from_xml('hi') + assert_equal 'hi', node.node_value + end + +end