Skip to content
This repository
tree: 83555a5398
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 158 lines (148 sloc) 6.12 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
require 'active_support/xml_mini'
require 'active_support/time'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'

class Hash
  # Returns a string containing an XML representation of its receiver:
  #
  # {"foo" => 1, "bar" => 2}.to_xml
  # # =>
  # # <?xml version="1.0" encoding="UTF-8"?>
  # # <hash>
  # # <foo type="integer">1</foo>
  # # <bar type="integer">2</bar>
  # # </hash>
  #
  # To do so, the method loops over the pairs and builds nodes that depend on
  # the _values_. Given a pair +key+, +value+:
  #
  # * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
  #
  # * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
  # and +key+ singularized as <tt>:children</tt>.
  #
  # * If +value+ is a callable object it must expect one or two arguments. Depending
  # on the arity, the callable is invoked with the +options+ hash as first argument
  # with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
  # callable can add nodes by using <tt>options[:builder]</tt>.
  #
  # "foo".to_xml(lambda { |options, key| options[:builder].b(key) })
  # # => "<b>foo</b>"
  #
  # * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
  #
  # class Foo
  # def to_xml(options)
  # options[:builder].bar "fooing!"
  # end
  # end
  #
  # {:foo => Foo.new}.to_xml(:skip_instruct => true)
  # # => "<hash><bar>fooing!</bar></hash>"
  #
  # * Otherwise, a node with +key+ as tag is created with a string representation of
  # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
  # Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
  # added as well according to the following mapping:
  #
  # XML_TYPE_NAMES = {
  # "Symbol" => "symbol",
  # "Fixnum" => "integer",
  # "Bignum" => "integer",
  # "BigDecimal" => "decimal",
  # "Float" => "float",
  # "TrueClass" => "boolean",
  # "FalseClass" => "boolean",
  # "Date" => "date",
  # "DateTime" => "datetime",
  # "Time" => "datetime"
  # }
  #
  # By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
  #
  # The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
  # configure your own builder with the <tt>:builder</tt> option. The method also accepts
  # options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
  def to_xml(options = {})
    require 'active_support/builder' unless defined?(Builder)

    options = options.dup
    options[:indent] ||= 2
    options[:root] ||= "hash"
    options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])

    builder = options[:builder]
    builder.instruct! unless options.delete(:skip_instruct)

    root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)

    builder.__send__(:method_missing, root) do
      each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
      yield builder if block_given?
    end
  end

  class << self
    def from_xml(xml)
      typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml)))
    end

    private
      def typecast_xml_value(value)
        case value.class.to_s
          when 'Hash'
            if value['type'] == 'array'
              _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
              if entries.nil? || (c = value['__content__'] && c.blank?)
                []
              else
                case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
                when "Array"
                  entries.collect { |v| typecast_xml_value(v) }
                when "Hash"
                  [typecast_xml_value(entries)]
                else
                  raise "can't typecast #{entries.inspect}"
                end
              end
            elsif value['type'] == 'file' ||
               (value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
              content = value["__content__"]
              if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
                parser.arity == 1 ? parser.call(content) : parser.call(content, value)
              else
                content
              end
            elsif value['type'] == 'string' && value['nil'] != 'true'
              ""
            # blank or nil parsed values are represented by nil
            elsif value.blank? || value['nil'] == 'true'
              nil
            # If the type is the only element which makes it then
            # this still makes the value nil, except if type is
            # a XML node(where type['value'] is a Hash)
            elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
              nil
            else
              xml_value = Hash[value.map { |k,v| [k, typecast_xml_value(v)] }]

              # Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
              # how multipart uploaded files from HTML appear
              xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
            end
          when 'Array'
            value.map! { |i| typecast_xml_value(i) }
            value.length > 1 ? value : value.first
          when 'String'
            value
          else
            raise "can't typecast #{value.class.name} - #{value.inspect}"
        end
      end

      def unrename_keys(params)
        case params.class.to_s
          when "Hash"
            Hash[params.map { |k,v| [k.to_s.tr("-", "_"), unrename_keys(v)] } ]
          when "Array"
            params.map { |v| unrename_keys(v) }
          else
            params
        end
      end
  end
end
Something went wrong with that request. Please try again.