Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 4453

Show
Ignore:
Timestamp:
06/16/06 10:07:13 (4 years ago)
Author:
david
Message:

Added Hash.create_from_xml(string) which will create a hash from a XML string and even typecast if possible [DHH]

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb

    r4410 r4453  
    11require 'cgi' 
    2 require 'action_controller/vendor/xml_simple' 
    32require 'action_controller/vendor/xml_node' 
    43 
     
    65# a CGI extension class or testing in isolation. 
    76class CGIMethods #:nodoc: 
    8   public 
     7  class << self 
    98    # Returns a hash with the pairs from the query string. The implicit hash construction that is done in 
    109    # parse_request_params is not done here. 
    11     def CGIMethods.parse_query_parameters(query_string) 
     10    def parse_query_parameters(query_string) 
    1211      parsed_params = {} 
    1312   
     
    4241    # "Somewhere cool!" are translated into a full hash hierarchy, like 
    4342    # { "customer" => { "address" => { "street" => "Somewhere cool!" } } } 
    44     def CGIMethods.parse_request_parameters(params) 
     43    def parse_request_parameters(params) 
    4544      parsed_params = {} 
    4645 
     
    6059    end 
    6160 
    62     def self.parse_formatted_request_parameters(mime_type, raw_post_data) 
    63       params = case strategy = ActionController::Base.param_parsers[mime_type] 
     61    def parse_formatted_request_parameters(mime_type, raw_post_data) 
     62      case strategy = ActionController::Base.param_parsers[mime_type] 
    6463        when Proc 
    6564          strategy.call(raw_post_data) 
    6665        when :xml_simple 
    67           raw_post_data.blank? ? nil : 
    68             typecast_xml_value(XmlSimple.xml_in(raw_post_data, 
    69               'forcearray'   => false, 
    70               'forcecontent' => true, 
    71               'keeproot'     => true, 
    72               'contentkey'   => '__content__')) 
     66          raw_post_data.blank? ? {} : Hash.create_from_xml(raw_post_data) 
    7367        when :yaml 
    7468          YAML.load(raw_post_data) 
     
    7771          { node.node_name => node } 
    7872      end 
    79        
    80       dasherize_keys(params || {}) 
    8173    rescue Object => e 
    8274      { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,  
     
    8476    end 
    8577 
    86     def self.typecast_xml_value(value) 
    87       case value 
    88       when Hash 
    89         if value.has_key?("__content__") 
    90           content = translate_xml_entities(value["__content__"]
    91           case value["type"] 
    92           when "integer"  then content.to_i 
    93           when "boolean"  then content == "true" 
    94           when "datetime" then Time.parse(content
    95           when "date"     then Date.parse(content) 
    96           else                 content 
    97           end 
     78    private 
     79      # Splits the given key into several pieces. Example keys are 'name', 'person[name]', 
     80      # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned. 
     81      # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', ''] 
     82      def split_key(key
     83        if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key 
     84          keys = [$1] 
     85         
     86          keys.concat($2[1..-2].split('][')
     87          keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings 
     88         
     89          keys 
    9890        else 
    99           value.empty? ? nil : value.inject({}) do |h,(k,v)| 
    100             h[k] = typecast_xml_value(v) 
    101             h 
    102           end 
     91          [key] 
    10392        end 
    104       when Array 
    105         value.map! { |i| typecast_xml_value(i) } 
    106         case value.length 
    107         when 0 then nil 
    108         when 1 then value.first 
    109         else value 
    110         end 
    111       else 
    112         raise "can't typecast #{value.inspect}" 
    11393      end 
    114     end 
     94     
     95      def get_typed_value(value) 
     96        # test most frequent case first 
     97        if value.is_a?(String) 
     98          value 
     99        elsif value.respond_to?(:content_type) && ! value.content_type.blank? 
     100          # Uploaded file 
     101          unless value.respond_to?(:full_original_filename) 
     102            class << value 
     103              alias_method :full_original_filename, :original_filename 
    115104 
    116   private 
    117  
    118     def self.translate_xml_entities(value) 
    119       value.gsub(/&lt;/,   "<"). 
    120             gsub(/&gt;/,   ">"). 
    121             gsub(/&quot;/, '"'). 
    122             gsub(/&apos;/, "'"). 
    123             gsub(/&amp;/,  "&") 
    124     end 
    125  
    126     def self.dasherize_keys(params) 
    127       case params.class.to_s 
    128       when "Hash" 
    129         params.inject({}) do |h,(k,v)| 
    130           h[k.to_s.tr("-", "_")] = dasherize_keys(v) 
    131           h 
    132         end 
    133       when "Array" 
    134         params.map { |v| dasherize_keys(v) } 
    135       else 
    136         params 
    137       end 
    138     end 
    139  
    140     # Splits the given key into several pieces. Example keys are 'name', 'person[name]', 
    141     # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned. 
    142     # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', ''] 
    143     def CGIMethods.split_key(key) 
    144       if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key 
    145         keys = [$1] 
    146          
    147         keys.concat($2[1..-2].split('][')) 
    148         keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings 
    149          
    150         keys 
    151       else 
    152         [key] 
    153       end 
    154     end 
    155      
    156     def CGIMethods.get_typed_value(value) 
    157       # test most frequent case first 
    158       if value.is_a?(String) 
    159         value 
    160       elsif value.respond_to?(:content_type) && ! value.content_type.blank? 
    161         # Uploaded file 
    162         unless value.respond_to?(:full_original_filename) 
    163           class << value 
    164             alias_method :full_original_filename, :original_filename 
    165  
    166             # Take the basename of the upload's original filename. 
    167             # This handles the full Windows paths given by Internet Explorer 
    168             # (and perhaps other broken user agents) without affecting 
    169             # those which give the lone filename. 
    170             # The Windows regexp is adapted from Perl's File::Basename. 
    171             def original_filename 
    172               if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename) 
    173                 md.captures.first 
    174               else 
    175                 File.basename full_original_filename 
     105              # Take the basename of the upload's original filename. 
     106              # This handles the full Windows paths given by Internet Explorer 
     107              # (and perhaps other broken user agents) without affecting 
     108              # those which give the lone filename. 
     109              # The Windows regexp is adapted from Perl's File::Basename. 
     110              def original_filename 
     111                if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename) 
     112                  md.captures.first 
     113                else 
     114                  File.basename full_original_filename 
     115                end 
    176116              end 
    177117            end 
    178118          end 
     119 
     120          # Return the same value after overriding original_filename. 
     121          value 
     122 
     123        elsif value.respond_to?(:read) 
     124          # Value as part of a multipart request 
     125          result = value.read 
     126          value.rewind 
     127          result 
     128        elsif value.class == Array 
     129          value.collect { |v| get_typed_value(v) } 
     130        else 
     131          # other value (neither string nor a multipart request) 
     132          value.to_s 
    179133        end 
     134      end 
     135   
     136      PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/ 
     137      def get_levels(key) 
     138        all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a 
     139        if main.nil? 
     140          [] 
     141        elsif trailing 
     142          [key] 
     143        elsif bracketed 
     144          [main] + bracketed.slice(1...-1).split('][') 
     145        else 
     146          [main] 
     147        end 
     148      end 
    180149 
    181         # Return the same value after overriding original_filename. 
    182         value 
    183  
    184       elsif value.respond_to?(:read) 
    185         # Value as part of a multipart request 
    186         result = value.read 
    187         value.rewind 
    188         result 
    189       elsif value.class == Array 
    190         value.collect { |v| CGIMethods.get_typed_value(v) } 
    191       else 
    192         # other value (neither string nor a multipart request) 
    193         value.to_s 
     150      def build_deep_hash(value, hash, levels) 
     151        if levels.length == 0 
     152          value 
     153        elsif hash.nil? 
     154          { levels.first => build_deep_hash(value, nil, levels[1..-1]) } 
     155        else 
     156          hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) }) 
     157        end 
    194158      end 
    195     end 
    196    
    197     PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/ 
    198     def CGIMethods.get_levels(key) 
    199       all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a 
    200       if main.nil? 
    201         [] 
    202       elsif trailing 
    203         [key] 
    204       elsif bracketed 
    205         [main] + bracketed.slice(1...-1).split('][') 
    206       else 
    207         [main] 
    208       end 
    209     end 
    210  
    211     def CGIMethods.build_deep_hash(value, hash, levels) 
    212       if levels.length == 0 
    213         value 
    214       elsif hash.nil? 
    215         { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) } 
    216       else 
    217         hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) }) 
    218       end 
    219     end 
     159  end 
    220160end 
  • trunk/actionpack/test/controller/webservice_test.rb

    r3936 r4453  
    147147    XML 
    148148    assert_equal %(<foo "bar's" & friends>), @controller.params[:data] 
    149   end 
    150  
    151   def test_dasherized_keys_as_yaml 
    152     ActionController::Base.param_parsers[Mime::YAML] = :yaml 
    153     process('POST', 'application/x-yaml', "---\nfirst-key:\n  sub-key: ...\n", true) 
    154     assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body 
    155     assert_equal "...", @controller.params[:first_key][:sub_key] 
    156149  end 
    157150 
  • trunk/activesupport/CHANGELOG

    r4452 r4453  
    11*SVN* 
     2 
     3* Added Hash.create_from_xml(string) which will create a hash from a XML string and even typecast if possible [DHH]. Example: 
     4 
     5    Hash.create_from_xml <<-EOT 
     6      <note> 
     7        <title>This is a note</title> 
     8        <created-at type="date">2004-10-10</created-at> 
     9      </note> 
     10    EOT 
     11   
     12  ...would return: 
     13   
     14    { :note => { :title => "This is a note", :created_at => Date.new(2004, 10, 10) } } 
    215 
    316* 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] 
  • trunk/activesupport/lib/active_support/core_ext/hash/conversions.rb

    r4432 r4453  
    11require 'date' 
     2require 'xml_simple' 
    23 
    34module ActiveSupport #:nodoc: 
     
    2021          "binary"   => Proc.new { |binary| Base64.encode64(binary) } 
    2122        } 
     23 
     24        def self.included(klass) 
     25          klass.extend(ClassMethods) 
     26        end 
    2227 
    2328        def to_xml(options = {}) 
     
    7176 
    7277        end 
     78 
     79        module ClassMethods 
     80          def create_from_xml(xml) 
     81            # TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple 
     82            undasherize_keys(typecast_xml_value(XmlSimple.xml_in(xml, 
     83              'forcearray'   => false, 
     84              'forcecontent' => true, 
     85              'keeproot'     => true, 
     86              'contentkey'   => '__content__') 
     87            )) 
     88          end 
     89 
     90          private 
     91            def typecast_xml_value(value) 
     92              case value.class.to_s 
     93                when "Hash" 
     94                  if value.has_key?("__content__") 
     95                    content = translate_xml_entities(value["__content__"]) 
     96                    case value["type"] 
     97                      when "integer"  then content.to_i 
     98                      when "boolean"  then content == "true" 
     99                      when "datetime" then ::Time.parse(content).utc 
     100                      when "date"     then ::Date.parse(content) 
     101                      else                 content 
     102                    end 
     103                  else 
     104                    value.empty? ? nil : value.inject({}) do |h,(k,v)| 
     105                      h[k] = typecast_xml_value(v) 
     106                      h 
     107                    end 
     108                  end 
     109                when "Array" 
     110                  value.map! { |i| typecast_xml_value(i) } 
     111                  case value.length 
     112                    when 0 then nil 
     113                    when 1 then value.first 
     114                    else value 
     115                  end 
     116                else 
     117                  raise "can't typecast #{value.inspect}" 
     118              end 
     119            end 
     120 
     121            def translate_xml_entities(value) 
     122              value.gsub(/&lt;/,   "<"). 
     123                    gsub(/&gt;/,   ">"). 
     124                    gsub(/&quot;/, '"'). 
     125                    gsub(/&apos;/, "'"). 
     126                    gsub(/&amp;/,  "&") 
     127            end 
     128 
     129            def undasherize_keys(params) 
     130              case params.class.to_s 
     131                when "Hash" 
     132                  params.inject({}) do |h,(k,v)| 
     133                    h[k.to_s.tr("-", "_")] = undasherize_keys(v) 
     134                    h 
     135                  end 
     136                when "Array" 
     137                  params.map { |v| undasherize_keys(v) } 
     138                else 
     139                  params 
     140              end 
     141            end 
     142        end 
    73143      end 
    74144    end 
  • trunk/activesupport/lib/active_support/core_ext/string/inflections.rb

    r4428 r4453  
    11require File.dirname(__FILE__) + '/../../inflector' unless defined? Inflector 
     2 
    23module ActiveSupport #:nodoc: 
    34  module CoreExtensions #:nodoc: 
     
    78      #   "ScaleScore".tableize => "scale_scores" 
    89      module Inflections 
    9          
    1010        # Returns the plural form of the word in the string. 
    1111        # 
  • trunk/activesupport/test/core_ext/hash_ext_test.rb

    r4413 r4453  
    281281  end 
    282282 
     283  def test_single_record_from_xml 
     284    topic_xml = <<-EOT 
     285      <topic> 
     286        <title>The First Topic</title> 
     287        <author-name>David</author-name> 
     288        <id type="integer">1</id> 
     289        <approved type="boolean">false</approved> 
     290        <replies-count type="integer">0</replies-count> 
     291        <written-on type="date">2003-07-16</written-on> 
     292        <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at> 
     293        <content>Have a nice day</content> 
     294        <author-email-address>david@loudthinking.com</author-email-address> 
     295        <parent-id></parent-id> 
     296      </topic> 
     297    EOT 
     298     
     299    expected_topic_hash = { 
     300      :title => "The First Topic", 
     301      :author_name => "David", 
     302      :id => 1, 
     303      :approved => false, 
     304      :replies_count => 0, 
     305      :written_on => Date.new(2003, 7, 16), 
     306      :viewed_at => Time.utc(2003, 7, 16, 9, 28), 
     307      :content => "Have a nice day", 
     308      :author_email_address => "david@loudthinking.com", 
     309      :parent_id => nil 
     310    }.stringify_keys 
     311     
     312    assert_equal expected_topic_hash, Hash.create_from_xml(topic_xml)["topic"] 
     313  end 
     314 
     315  def test_multiple_records_from_xml 
     316    topics_xml = <<-EOT 
     317      <topics> 
     318        <topic> 
     319          <title>The First Topic</title> 
     320          <author-name>David</author-name> 
     321          <id type="integer">1</id> 
     322          <approved type="boolean">false</approved> 
     323          <replies-count type="integer">0</replies-count> 
     324          <written-on type="date">2003-07-16</written-on> 
     325          <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at> 
     326          <content>Have a nice day</content> 
     327          <author-email-address>david@loudthinking.com</author-email-address> 
     328          <parent-id></parent-id> 
     329        </topic> 
     330        <topic> 
     331          <title>The Second Topic</title> 
     332          <author-name>Jason</author-name> 
     333          <id type="integer">1</id> 
     334          <approved type="boolean">false</approved> 
     335          <replies-count type="integer">0</replies-count> 
     336          <written-on type="date">2003-07-16</written-on> 
     337          <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at> 
     338          <content>Have a nice day</content> 
     339          <author-email-address>david@loudthinking.com</author-email-address> 
     340          <parent-id></parent-id> 
     341        </topic> 
     342      </topics> 
     343    EOT 
     344     
     345    expected_topic_hash = { 
     346      :title => "The First Topic", 
     347      :author_name => "David", 
     348      :id => 1, 
     349      :approved => false, 
     350      :replies_count => 0, 
     351      :written_on => Date.new(2003, 7, 16), 
     352      :viewed_at => Time.utc(2003, 7, 16, 9, 28), 
     353      :content => "Have a nice day", 
     354      :author_email_address => "david@loudthinking.com", 
     355      :parent_id => nil 
     356    }.stringify_keys 
     357     
     358    assert_equal expected_topic_hash, Hash.create_from_xml(topics_xml)["topics"]["topic"].first 
     359  end 
    283360end