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'classHash# 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.defto_xml(options={})require'active_support/builder'unlessdefined?(Builder)options=options.dupoptions[:indent]||=2options[:root]||='hash'options[:builder]||=Builder::XmlMarkup.new(indent: options[:indent])builder=options[:builder]builder.instruct!unlessoptions.delete(:skip_instruct)root=ActiveSupport::XmlMini.rename_key(options[:root].to_s,options)builder.tag!(root)doeach{|key,value|ActiveSupport::XmlMini.to_tag(key,value,options)}yieldbuilderifblock_given?endendclass<<self# Returns a Hash containing a collection of pairs when the key is the node name and the value is# its content## xml = <<-XML# <?xml version="1.0" encoding="UTF-8"?># <hash># <foo type="integer">1</foo># <bar type="integer">2</bar># </hash># XML## hash = Hash.from_xml(xml)# # => {"hash"=>{"foo"=>1, "bar"=>2}}## DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.deffrom_xml(xml,disallowed_types=nil)ActiveSupport::XMLConverter.new(xml,disallowed_types).to_hend# Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.deffrom_trusted_xml(xml)from_xmlxml,[]endendendmoduleActiveSupportclassXMLConverter# :nodoc:classDisallowedType<StandardErrordefinitialize(type)super"Disallowed type attribute: #{type.inspect}"endendDISALLOWED_TYPES=%w(symbol yaml)definitialize(xml,disallowed_types=nil)@xml=normalize_keys(XmlMini.parse(xml))@disallowed_types=disallowed_types||DISALLOWED_TYPESenddefto_hdeep_to_h(@xml)endprivatedefnormalize_keys(params)caseparamswhenHashHash[params.map{|k,v|[k.to_s.tr('-','_'),normalize_keys(v)]}]whenArrayparams.map{|v|normalize_keys(v)}elseparamsendenddefdeep_to_h(value)casevaluewhenHashprocess_hash(value)whenArrayprocess_array(value)whenStringvalueelseraise"can't typecast #{value.class.name} - #{value.inspect}"endenddefprocess_hash(value)ifvalue.include?('type')&&!value['type'].is_a?(Hash)&&@disallowed_types.include?(value['type'])raiseDisallowedType,value['type']endifbecome_array?(value)_,entries=Array.wrap(value.detect{|k,v|notv.is_a?(String)})ifentries.nil?||value['__content__'].try(:empty?)[]elsecaseentrieswhenArrayentries.collect{|v|deep_to_h(v)}whenHash[deep_to_h(entries)]elseraise"can't typecast #{entries.inspect}"endendelsifbecome_content?(value)process_content(value)elsifbecome_empty_string?(value)''elsifbecome_hash?(value)xml_value=Hash[value.map{|k,v|[k,deep_to_h(v)]}]# Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with# how multipart uploaded files from HTML appearxml_value['file'].is_a?(StringIO)?xml_value['file']:xml_valueendenddefbecome_content?(value)value['type']=='file'||(value['__content__']&&(value.keys.size==1||value['__content__'].present?))enddefbecome_array?(value)value['type']=='array'enddefbecome_empty_string?(value)# {"string" => true}# No tests fail when the second term is removed.value['type']=='string'&&value['nil']!='true'enddefbecome_hash?(value)!nothing?(value)&&!garbage?(value)enddefnothing?(value)# blank or nil parsed values are represented by nilvalue.blank?||value['nil']=='true'enddefgarbage?(value)# 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)value['type']&&!value['type'].is_a?(::Hash)&&value.size==1enddefprocess_content(value)content=value['__content__']ifparser=ActiveSupport::XmlMini::PARSING[value['type']]parser.arity==1?parser.call(content):parser.call(content,value)elsecontentendenddefprocess_array(value)value.map!{|i|deep_to_h(i)}value.length>1?value:value.firstendendend