diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2006-06-16 10:07:13 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2006-06-16 10:07:13 +0000 |
commit | 36dc94a6a1e742848c5a80975b8bf5d216f54022 (patch) | |
tree | 4e22f73746eded676c8109c36683380418b893df | |
parent | 7c326a3b54aaeb572a69dabb62fdc453d4814cf3 (diff) | |
download | rails-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-x | actionpack/lib/action_controller/cgi_ext/cgi_methods.rb | 208 | ||||
-rw-r--r-- | actionpack/test/controller/webservice_test.rb | 7 | ||||
-rw-r--r-- | activesupport/CHANGELOG | 13 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/hash/conversions.rb | 70 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/string/inflections.rb | 2 | ||||
-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.rb | 79 |
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(/</, "<"). - gsub(/>/, ">"). - gsub(/"/, '"'). - gsub(/'/, "'"). - gsub(/&/, "&") - 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(/</, "<"). + gsub(/>/, ">"). + gsub(/"/, '"'). + gsub(/'/, "'"). + gsub(/&/, "&") + 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 |