aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG11
-rwxr-xr-xactionpack/lib/action_controller.rb2
-rwxr-xr-xactionpack/lib/action_controller/base.rb35
-rwxr-xr-xactionpack/lib/action_controller/cgi_ext/cgi_methods.rb8
-rw-r--r--actionpack/lib/action_controller/cgi_process.rb4
-rw-r--r--actionpack/lib/action_controller/deprecated_request_methods.rb34
-rwxr-xr-xactionpack/lib/action_controller/request.rb49
-rw-r--r--actionpack/lib/action_controller/vendor/xml_node.rb98
-rw-r--r--actionpack/test/abstract_unit.rb4
-rw-r--r--actionpack/test/controller/webservice_test.rb146
10 files changed, 345 insertions, 46 deletions
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', '<entry attributed="true"><summary>content...</summary></entry>')
+
+ 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', '<entry attributed="true"><summary>content...</summary></entry>')
+
+ 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(%{<?xml version="1.0" encoding="UTF-8"?>
+ <response success='true'>
+ <page title='Ajax Summit' id='1133' email_address='ry87ib@backpackit.com'>
+ <description>With O'Reilly and Adaptive Path</description>
+ <notes>
+ <note title='Hotel' id='1020' created_at='2005-05-14 16:41:11'>
+ Staying at the Savoy
+ </note>
+ </notes>
+ <tags>
+ <tag name='Technology' id='4' />
+ <tag name='Travel' id='5' />
+ </tags>
+ </page>
+ </response>
+ }
+ )
+ 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('<entry>hi</entry>')
+ assert_equal 'hi', node.node_value
+ end
+
+end