require 'active_support/xml_mini' require 'active_support/time' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/reverse_merge' 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 # # => # # # # # # 1 # # 2 # # # # 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 :root. # # * If +value+ is an array there's a recursive call with +key+ as :root, # and +key+ singularized as :children. # # * 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 :root, and +key+ singularized as second argument. The # callable can add nodes by using options[:builder]. # # 'foo'.to_xml(lambda { |options, key| options[:builder].b(key) }) # # => "foo" # # * If +value+ responds to +to_xml+ the method is invoked with +key+ as :root. # # class Foo # def to_xml(options) # options[:builder].bar 'fooing!' # end # end # # { foo: Foo.new }.to_xml(skip_instruct: true) # # => "fooing!" # # * 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 :skip_types 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 :root option. # # The default XML builder is a fresh instance of Builder::XmlMarkup. You can # configure your own builder with the :builder option. The method also accepts # options like :dasherize 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.tag!(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: # } } into { files: # } 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