mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
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
This commit is contained in:
parent
4f00c70580
commit
03d37a2d68
10 changed files with 345 additions and 46 deletions
|
@ -1,5 +1,16 @@
|
||||||
*SVN*
|
*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 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]
|
* Fixed that default image extension was not appended when using a full URL with AssetTagHelper#image_tag #4032, #3728 [rubyonrails@beautifulpixel.com]
|
||||||
|
|
|
@ -36,6 +36,8 @@ end
|
||||||
|
|
||||||
require 'action_controller/base'
|
require 'action_controller/base'
|
||||||
require 'action_controller/deprecated_redirects'
|
require 'action_controller/deprecated_redirects'
|
||||||
|
require 'action_controller/request'
|
||||||
|
require 'action_controller/deprecated_request_methods'
|
||||||
require 'action_controller/rescue'
|
require 'action_controller/rescue'
|
||||||
require 'action_controller/benchmarking'
|
require 'action_controller/benchmarking'
|
||||||
require 'action_controller/flash'
|
require 'action_controller/flash'
|
||||||
|
|
|
@ -260,6 +260,37 @@ module ActionController #:nodoc:
|
||||||
@@allow_concurrency = false
|
@@allow_concurrency = false
|
||||||
cattr_accessor :allow_concurrency
|
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")
|
# 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".
|
# will be converted to "#{template_root}/test/template.rhtml".
|
||||||
class_inheritable_accessor :template_root
|
class_inheritable_accessor :template_root
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
require 'cgi'
|
require 'cgi'
|
||||||
require 'action_controller/vendor/xml_simple'
|
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
|
# Static methods for parsing the query and request parameters that can be used in
|
||||||
# a CGI extension class or testing in isolation.
|
# a CGI extension class or testing in isolation.
|
||||||
|
@ -58,12 +59,7 @@ class CGIMethods #:nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_formatted_request_parameters(format, raw_post_data)
|
def self.parse_formatted_request_parameters(format, raw_post_data)
|
||||||
case format
|
ActionController::Base.param_parsers[format].call(raw_post_data) || {}
|
||||||
when :xml
|
|
||||||
return XmlSimple.xml_in(raw_post_data, 'ForceArray' => false)
|
|
||||||
when :yaml
|
|
||||||
return YAML.load(raw_post_data)
|
|
||||||
end
|
|
||||||
rescue Object => e
|
rescue Object => e
|
||||||
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
|
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
|
||||||
"raw_post_data" => raw_post_data, "format" => format }
|
"raw_post_data" => raw_post_data, "format" => format }
|
||||||
|
|
|
@ -64,8 +64,8 @@ module ActionController #:nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_parameters
|
def request_parameters
|
||||||
if formatted_post?
|
if ActionController::Base.param_parsers.has_key?(content_type)
|
||||||
CGIMethods.parse_formatted_request_parameters(post_format, @env['RAW_POST_DATA'])
|
CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA'])
|
||||||
else
|
else
|
||||||
CGIMethods.parse_request_parameters(@cgi.params)
|
CGIMethods.parse_request_parameters(@cgi.params)
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -42,44 +42,25 @@ module ActionController
|
||||||
method == :head
|
method == :head
|
||||||
end
|
end
|
||||||
|
|
||||||
# Determine whether the body of a POST request is URL-encoded (default),
|
# Determine whether the body of a HTTP call is URL-encoded (default)
|
||||||
# XML, or YAML by checking the Content-Type HTTP header:
|
# or matches one of the registered param_parsers.
|
||||||
#
|
|
||||||
# Content-Type Post Format
|
|
||||||
# application/xml :xml
|
|
||||||
# text/xml :xml
|
|
||||||
# application/x-yaml :yaml
|
|
||||||
# text/x-yaml :yaml
|
|
||||||
# * :url_encoded
|
|
||||||
#
|
#
|
||||||
# For backward compatibility, the post format is extracted from the
|
# For backward compatibility, the post format is extracted from the
|
||||||
# X-Post-Data-Format HTTP header if present.
|
# X-Post-Data-Format HTTP header if present.
|
||||||
def post_format
|
def content_type
|
||||||
@post_format ||=
|
return @content_type if @content_type
|
||||||
|
|
||||||
|
@content_type = @env['CONTENT_TYPE'].to_s.downcase
|
||||||
|
|
||||||
if @env['HTTP_X_POST_DATA_FORMAT']
|
if @env['HTTP_X_POST_DATA_FORMAT']
|
||||||
@env['HTTP_X_POST_DATA_FORMAT'].downcase.to_sym
|
case @env['HTTP_X_POST_DATA_FORMAT'].downcase.to_sym
|
||||||
else
|
when :yaml
|
||||||
case @env['CONTENT_TYPE'].to_s.downcase
|
@content_type = 'application/x-yaml'
|
||||||
when 'application/xml', 'text/xml' then :xml
|
when :xml
|
||||||
when 'application/x-yaml', 'text/x-yaml' then :yaml
|
@content_type = 'application/xml'
|
||||||
else :url_encoded
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
@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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if the request's "X-Requested-With" header contains
|
# Returns true if the request's "X-Requested-With" header contains
|
||||||
|
|
98
actionpack/lib/action_controller/vendor/xml_node.rb
vendored
Normal file
98
actionpack/lib/action_controller/vendor/xml_node.rb
vendored
Normal file
|
@ -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
|
|
@ -2,6 +2,7 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
|
||||||
$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support')
|
$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support')
|
||||||
$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
|
$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
|
||||||
|
|
||||||
|
require 'yaml'
|
||||||
require 'test/unit'
|
require 'test/unit'
|
||||||
require 'action_controller'
|
require 'action_controller'
|
||||||
require 'breakpoint'
|
require 'breakpoint'
|
||||||
|
@ -11,4 +12,3 @@ require 'action_controller/test_process'
|
||||||
ActionController::Base.logger = nil
|
ActionController::Base.logger = nil
|
||||||
ActionController::Base.ignore_missing_templates = false
|
ActionController::Base.ignore_missing_templates = false
|
||||||
ActionController::Routing::Routes.reload rescue nil
|
ActionController::Routing::Routes.reload rescue nil
|
||||||
|
|
||||||
|
|
146
actionpack/test/controller/webservice_test.rb
Normal file
146
actionpack/test/controller/webservice_test.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue