aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2006-06-16 10:07:13 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2006-06-16 10:07:13 +0000
commit36dc94a6a1e742848c5a80975b8bf5d216f54022 (patch)
tree4e22f73746eded676c8109c36683380418b893df
parent7c326a3b54aaeb572a69dabb62fdc453d4814cf3 (diff)
downloadrails-36dc94a6a1e742848c5a80975b8bf5d216f54022.tar.gz
rails-36dc94a6a1e742848c5a80975b8bf5d216f54022.tar.bz2
rails-36dc94a6a1e742848c5a80975b8bf5d216f54022.zip
Added Hash.create_from_xml(string) which will create a hash from a XML string and even typecast if possible [DHH]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4453 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rwxr-xr-xactionpack/lib/action_controller/cgi_ext/cgi_methods.rb208
-rw-r--r--actionpack/test/controller/webservice_test.rb7
-rw-r--r--activesupport/CHANGELOG13
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb70
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb2
-rw-r--r--activesupport/lib/active_support/vendor/xml_simple.rb (renamed from actionpack/lib/action_controller/vendor/xml_simple.rb)0
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb79
7 files changed, 236 insertions, 143 deletions
diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
index be5923e8de..1af837c39a 100755
--- a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
+++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
@@ -1,14 +1,13 @@
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.
class CGIMethods #:nodoc:
- public
+ class << self
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
- def CGIMethods.parse_query_parameters(query_string)
+ def parse_query_parameters(query_string)
parsed_params = {}
query_string.split(/[&;]/).each { |p|
@@ -41,7 +40,7 @@ class CGIMethods #:nodoc:
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
# "Somewhere cool!" are translated into a full hash hierarchy, like
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
- def CGIMethods.parse_request_parameters(params)
+ def parse_request_parameters(params)
parsed_params = {}
for key, value in params
@@ -59,162 +58,103 @@ class CGIMethods #:nodoc:
parsed_params
end
- def self.parse_formatted_request_parameters(mime_type, raw_post_data)
- params = case strategy = ActionController::Base.param_parsers[mime_type]
+ def parse_formatted_request_parameters(mime_type, raw_post_data)
+ case strategy = ActionController::Base.param_parsers[mime_type]
when Proc
strategy.call(raw_post_data)
when :xml_simple
- raw_post_data.blank? ? nil :
- typecast_xml_value(XmlSimple.xml_in(raw_post_data,
- 'forcearray' => false,
- 'forcecontent' => true,
- 'keeproot' => true,
- 'contentkey' => '__content__'))
+ raw_post_data.blank? ? {} : Hash.create_from_xml(raw_post_data)
when :yaml
YAML.load(raw_post_data)
when :xml_node
node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node }
end
-
- dasherize_keys(params || {})
rescue Object => e
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type }
end
- def self.typecast_xml_value(value)
- case value
- when Hash
- if value.has_key?("__content__")
- content = translate_xml_entities(value["__content__"])
- case value["type"]
- when "integer" then content.to_i
- when "boolean" then content == "true"
- when "datetime" then Time.parse(content)
- when "date" then Date.parse(content)
- else content
- end
- else
- value.empty? ? nil : value.inject({}) do |h,(k,v)|
- h[k] = typecast_xml_value(v)
- h
- end
- end
- when Array
- value.map! { |i| typecast_xml_value(i) }
- case value.length
- when 0 then nil
- when 1 then value.first
- else value
- end
- else
- raise "can't typecast #{value.inspect}"
- end
- end
-
- private
-
- def self.translate_xml_entities(value)
- value.gsub(/&lt;/, "<").
- gsub(/&gt;/, ">").
- gsub(/&quot;/, '"').
- gsub(/&apos;/, "'").
- gsub(/&amp;/, "&")
- end
-
- def self.dasherize_keys(params)
- case params.class.to_s
- when "Hash"
- params.inject({}) do |h,(k,v)|
- h[k.to_s.tr("-", "_")] = dasherize_keys(v)
- h
- end
- when "Array"
- params.map { |v| dasherize_keys(v) }
- else
- params
- end
- end
-
- # Splits the given key into several pieces. Example keys are 'name', 'person[name]',
- # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
- # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
- def CGIMethods.split_key(key)
- if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
- keys = [$1]
+ private
+ # Splits the given key into several pieces. Example keys are 'name', 'person[name]',
+ # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
+ # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
+ def split_key(key)
+ if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
+ keys = [$1]
- keys.concat($2[1..-2].split(']['))
- keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
+ keys.concat($2[1..-2].split(']['))
+ keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
- keys
- else
- [key]
+ keys
+ else
+ [key]
+ end
end
- end
- def CGIMethods.get_typed_value(value)
- # test most frequent case first
- if value.is_a?(String)
- value
- elsif value.respond_to?(:content_type) && ! value.content_type.blank?
- # Uploaded file
- unless value.respond_to?(:full_original_filename)
- class << value
- alias_method :full_original_filename, :original_filename
-
- # Take the basename of the upload's original filename.
- # This handles the full Windows paths given by Internet Explorer
- # (and perhaps other broken user agents) without affecting
- # those which give the lone filename.
- # The Windows regexp is adapted from Perl's File::Basename.
- def original_filename
- if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
- md.captures.first
- else
- File.basename full_original_filename
+ def get_typed_value(value)
+ # test most frequent case first
+ if value.is_a?(String)
+ value
+ elsif value.respond_to?(:content_type) && ! value.content_type.blank?
+ # Uploaded file
+ unless value.respond_to?(:full_original_filename)
+ class << value
+ alias_method :full_original_filename, :original_filename
+
+ # Take the basename of the upload's original filename.
+ # This handles the full Windows paths given by Internet Explorer
+ # (and perhaps other broken user agents) without affecting
+ # those which give the lone filename.
+ # The Windows regexp is adapted from Perl's File::Basename.
+ def original_filename
+ if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
+ md.captures.first
+ else
+ File.basename full_original_filename
+ end
end
end
end
- end
- # Return the same value after overriding original_filename.
- value
+ # Return the same value after overriding original_filename.
+ value
- elsif value.respond_to?(:read)
- # Value as part of a multipart request
- result = value.read
- value.rewind
- result
- elsif value.class == Array
- value.collect { |v| CGIMethods.get_typed_value(v) }
- else
- # other value (neither string nor a multipart request)
- value.to_s
+ elsif value.respond_to?(:read)
+ # Value as part of a multipart request
+ result = value.read
+ value.rewind
+ result
+ elsif value.class == Array
+ value.collect { |v| get_typed_value(v) }
+ else
+ # other value (neither string nor a multipart request)
+ value.to_s
+ end
end
- end
- PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
- def CGIMethods.get_levels(key)
- all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
- if main.nil?
- []
- elsif trailing
- [key]
- elsif bracketed
- [main] + bracketed.slice(1...-1).split('][')
- else
- [main]
+ PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
+ def get_levels(key)
+ all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
+ if main.nil?
+ []
+ elsif trailing
+ [key]
+ elsif bracketed
+ [main] + bracketed.slice(1...-1).split('][')
+ else
+ [main]
+ end
end
- end
- def CGIMethods.build_deep_hash(value, hash, levels)
- if levels.length == 0
- value
- elsif hash.nil?
- { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
- else
- hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
+ def build_deep_hash(value, hash, levels)
+ if levels.length == 0
+ value
+ elsif hash.nil?
+ { levels.first => build_deep_hash(value, nil, levels[1..-1]) }
+ else
+ hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) })
+ end
end
- end
+ end
end
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index 9874b2092b..e60e3a84cb 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -148,13 +148,6 @@ class WebServiceTest < Test::Unit::TestCase
assert_equal %(<foo "bar's" & friends>), @controller.params[:data]
end
- def test_dasherized_keys_as_yaml
- ActionController::Base.param_parsers[Mime::YAML] = :yaml
- process('POST', 'application/x-yaml', "---\nfirst-key:\n sub-key: ...\n", true)
- assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body
- assert_equal "...", @controller.params[:first_key][:sub_key]
- end
-
def test_typecast_as_yaml
ActionController::Base.param_parsers[Mime::YAML] = :yaml
process('POST', 'application/x-yaml', <<-YAML)
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index 9394fcfed5..5a5fc9d98e 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,5 +1,18 @@
*SVN*
+* Added Hash.create_from_xml(string) which will create a hash from a XML string and even typecast if possible [DHH]. Example:
+
+ Hash.create_from_xml <<-EOT
+ <note>
+ <title>This is a note</title>
+ <created-at type="date">2004-10-10</created-at>
+ </note>
+ EOT
+
+ ...would return:
+
+ { :note => { :title => "This is a note", :created_at => Date.new(2004, 10, 10) } }
+
* Added Jim Weirich's excellent FlexMock class to vendor (Copyright 2003, 2004 by Jim Weirich (jim@weriichhouse.org)) -- it's not automatically required, though, so require 'flexmock' is still necessary [DHH]
* Fixed that Module#alias_method_chain should work with both foo? foo! and foo at the same time #4954 [anna@wota.jp]
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index a3270c31ef..ac4a612e49 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -1,4 +1,5 @@
require 'date'
+require 'xml_simple'
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
@@ -20,6 +21,10 @@ module ActiveSupport #:nodoc:
"binary" => Proc.new { |binary| Base64.encode64(binary) }
}
+ def self.included(klass)
+ klass.extend(ClassMethods)
+ end
+
def to_xml(options = {})
options[:indent] ||= 2
options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
@@ -70,6 +75,71 @@ module ActiveSupport #:nodoc:
end
end
+
+ module ClassMethods
+ def create_from_xml(xml)
+ # TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
+ undasherize_keys(typecast_xml_value(XmlSimple.xml_in(xml,
+ 'forcearray' => false,
+ 'forcecontent' => true,
+ 'keeproot' => true,
+ 'contentkey' => '__content__')
+ ))
+ end
+
+ private
+ def typecast_xml_value(value)
+ case value.class.to_s
+ when "Hash"
+ if value.has_key?("__content__")
+ content = translate_xml_entities(value["__content__"])
+ case value["type"]
+ when "integer" then content.to_i
+ when "boolean" then content == "true"
+ when "datetime" then ::Time.parse(content).utc
+ when "date" then ::Date.parse(content)
+ else content
+ end
+ else
+ value.empty? ? nil : value.inject({}) do |h,(k,v)|
+ h[k] = typecast_xml_value(v)
+ h
+ end
+ end
+ when "Array"
+ value.map! { |i| typecast_xml_value(i) }
+ case value.length
+ when 0 then nil
+ when 1 then value.first
+ else value
+ end
+ else
+ raise "can't typecast #{value.inspect}"
+ end
+ end
+
+ def translate_xml_entities(value)
+ value.gsub(/&lt;/, "<").
+ gsub(/&gt;/, ">").
+ gsub(/&quot;/, '"').
+ gsub(/&apos;/, "'").
+ gsub(/&amp;/, "&")
+ end
+
+ def undasherize_keys(params)
+ case params.class.to_s
+ when "Hash"
+ params.inject({}) do |h,(k,v)|
+ h[k.to_s.tr("-", "_")] = undasherize_keys(v)
+ h
+ end
+ when "Array"
+ params.map { |v| undasherize_keys(v) }
+ else
+ params
+ end
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index ebae190052..7107bc6945 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -1,4 +1,5 @@
require File.dirname(__FILE__) + '/../../inflector' unless defined? Inflector
+
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
@@ -6,7 +7,6 @@ module ActiveSupport #:nodoc:
# For instance, you can figure out the name of a database from the name of a class.
# "ScaleScore".tableize => "scale_scores"
module Inflections
-
# Returns the plural form of the word in the string.
#
# Examples
diff --git a/actionpack/lib/action_controller/vendor/xml_simple.rb b/activesupport/lib/active_support/vendor/xml_simple.rb
index e6ce63a839..e6ce63a839 100644
--- a/actionpack/lib/action_controller/vendor/xml_simple.rb
+++ b/activesupport/lib/active_support/vendor/xml_simple.rb
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index f8ed54c881..65050de15a 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -280,4 +280,81 @@ class HashToXmlTest < Test::Unit::TestCase
assert xml.include?(%(<addresses><address><streets><street><name>))
end
-end
+ def test_single_record_from_xml
+ topic_xml = <<-EOT
+ <topic>
+ <title>The First Topic</title>
+ <author-name>David</author-name>
+ <id type="integer">1</id>
+ <approved type="boolean">false</approved>
+ <replies-count type="integer">0</replies-count>
+ <written-on type="date">2003-07-16</written-on>
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
+ <content>Have a nice day</content>
+ <author-email-address>david@loudthinking.com</author-email-address>
+ <parent-id></parent-id>
+ </topic>
+ EOT
+
+ expected_topic_hash = {
+ :title => "The First Topic",
+ :author_name => "David",
+ :id => 1,
+ :approved => false,
+ :replies_count => 0,
+ :written_on => Date.new(2003, 7, 16),
+ :viewed_at => Time.utc(2003, 7, 16, 9, 28),
+ :content => "Have a nice day",
+ :author_email_address => "david@loudthinking.com",
+ :parent_id => nil
+ }.stringify_keys
+
+ assert_equal expected_topic_hash, Hash.create_from_xml(topic_xml)["topic"]
+ end
+
+ def test_multiple_records_from_xml
+ topics_xml = <<-EOT
+ <topics>
+ <topic>
+ <title>The First Topic</title>
+ <author-name>David</author-name>
+ <id type="integer">1</id>
+ <approved type="boolean">false</approved>
+ <replies-count type="integer">0</replies-count>
+ <written-on type="date">2003-07-16</written-on>
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
+ <content>Have a nice day</content>
+ <author-email-address>david@loudthinking.com</author-email-address>
+ <parent-id></parent-id>
+ </topic>
+ <topic>
+ <title>The Second Topic</title>
+ <author-name>Jason</author-name>
+ <id type="integer">1</id>
+ <approved type="boolean">false</approved>
+ <replies-count type="integer">0</replies-count>
+ <written-on type="date">2003-07-16</written-on>
+ <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
+ <content>Have a nice day</content>
+ <author-email-address>david@loudthinking.com</author-email-address>
+ <parent-id></parent-id>
+ </topic>
+ </topics>
+ EOT
+
+ expected_topic_hash = {
+ :title => "The First Topic",
+ :author_name => "David",
+ :id => 1,
+ :approved => false,
+ :replies_count => 0,
+ :written_on => Date.new(2003, 7, 16),
+ :viewed_at => Time.utc(2003, 7, 16, 9, 28),
+ :content => "Have a nice day",
+ :author_email_address => "david@loudthinking.com",
+ :parent_id => nil
+ }.stringify_keys
+
+ assert_equal expected_topic_hash, Hash.create_from_xml(topics_xml)["topics"]["topic"].first
+ end
+end \ No newline at end of file