How to convert an array of hashes into XML in Rails? - ruby-on-rails

I have an array of database objects, #configs, that I want to convert to the XML format but the output is not the expected. Every entry gets enclosed in a <map> tag instead of a <entry> tag, I only wanted <tag> to be the XML root. How do I build the XML with the <tag> root and put all the entries in a <entry> tag?
Thank you in advance for your help and time!
Here is my code:
entries = Array.new
entry = Hash.new
conf = Hash.new
#configs.each do |config|
entry.store('string', config.key)
conf.store('value', config.value)
conf.store('comment', config.comment)
entry.store('com.mirth.connect.util.ConfigurationProperty', conf)
entries << entry
end
pp entries.to_xml(:root => 'map', :indent => 0, :skip_types => true)
And the result is:
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<map>
<map>
<string>PNB_ALERTLOG_RECEIVER_CHANNEL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>PNB_ALERTLOG_RECEIVER</value>
<comment>Canal que irá receber tudo o que for logged com Warning e Error</comment>
</com.mirth.connect.util.ConfigurationProperty>
</map>
<map>
<string>PNB_CFG_FILE_ACCESS_CONTROL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/pnbAccessControl.json</value>
<comment>Este ficheiro permite configurar Autenticação e Controlo de Acessos.</comment>
</com.mirth.connect.util.ConfigurationProperty>
</map>
<map>
<string>PNB_CFG_FILE_CONNECTION_POOLS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/pnbConnectionPools.json</value>
<comment>Configuração de Oracle Universal Connection Pools usadas pelo PNB (PEM, RCU2)</comment>
</com.mirth.connect.util.ConfigurationProperty>
</map>
<map>
<string>PNB_CFG_FILE_CSP_MC_EXCLUSIONS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/medCronExclusions/mcExclCurrentRevision.json</value>
<comment>N/A</comment>
</com.mirth.connect.util.ConfigurationProperty>
</map>
<map>
<string>PNB_CFG_FILE_FACILITIES_ALIAS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/snsFacilitiesAlias.json</value>
<comment>Mapa de alias do codigo das instituicoes do SNS.</comment>
</com.mirth.connect.util.ConfigurationProperty>
</map>
</map>
What I wanted:
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<map>
<entry>
<string>PNB_ALERTLOG_RECEIVER_CHANNEL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>PNB_ALERTLOG_RECEIVER</value>
<comment>Canal que irá receber tudo o que for logged com Warning e Error</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
<entry>
<string>PNB_CFG_FILE_ACCESS_CONTROL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/pnbAccessControl.json</value>
<comment>Este ficheiro permite configurar Autenticação e Controlo de Acessos.</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
<entry>
<string>PNB_CFG_FILE_CONNECTION_POOLS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/pnbConnectionPools.json</value>
<comment>Configuração de Oracle Universal Connection Pools usadas pelo PNB (PEM, RCU2)</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
<entry>
<string>PNB_CFG_FILE_CSP_MC_EXCLUSIONS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/medCronExclusions/mcExclCurrentRevision.json</value>
<comment>N/A</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
<entry>
<string>PNB_CFG_FILE_FACILITIES_ALIAS</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/snsFacilitiesAlias.json</value>
<comment>entrya de alias do codigo das instituicoes do SNS.</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
</map>

try this:
pp entries.to_xml(:root => 'map', :children => 'entry', :indent => 0, :skip_types => true)
source: http://apidock.com/rails/Array/to_xml

Suppose entry is the following hash:
entry = {
a: “hello”,
b: “goodbye”,
}
If you write:
entries = []
entries << entry
p entries
then the output is:
[{:a => “hello”, {:b => “goodbye”}]
So if you then write:
p entries.to_xml
how do you suppose the word “entry” will ever appear in the output? That's sort of like expecting the output of:
x = 10
y = 20
puts x+y
to include the letters "x" and "y" somewhere.
According to the to_xml() docs for arrays:
Returns a string ... by invoking to_xml on each element.
The options hash is passed downwards.
http://apidock.com/rails/Array/to_xml
The fact that the options hash is passed downwards means that when you specify {root: map} for the to_xml() call on the array, then <map> will become the root of the xml, and when to_xml() is called on each array element the method will be called with the option {root: “map”}, which will cause each array element to be wrapped in a <map> tag. For instance:
puts [{a: 10, b: 20}, {a: 100, b: 200}].to_xml({root: "map"})
--output:--
<?xml version="1.0" encoding="UTF-8"?>
<map type="array">
<map>
<a type="integer">10</a>
<b type="integer">20</b>
</map>
<map>
<a type="integer">100</a>
<b type="integer">200</b>
</map>
</map>
The nested <map> tags are a side effect of a feature built into the to_xml() method: if you specify a plural name for the :root option when calling to_xml() on an array, e.g. “maps”, then when rails turns around and calls to_xml() on each element of the array, rails will specify the singular “map” for the :root option. That makes some sense because if you call to_xml() on an array and you specify the :root option to be “maps” then naturally each array element would probably be a "map". Of course, that isn’t what you want.
Luckily, as mr_sudaca pointed out, there is this:
By default name of the node for the children of root is
root.singularize. You can change it with the :children option.
http://apidock.com/rails/Array/to_xml
As a result, this code:
require 'ostruct'
configs = [
OpenStruct.new(
key: "PNB_ALERTLOG_RECEIVER_CHANNEL",
value: "PNB_ALERTLOG_RECEIVER",
comment: "Canal que...",
),
OpenStruct.new(
key: "PNB_CFG_FILE_ACCESS_CONTROL",
value: "resources/configPnbDev/pnbAccessControl.json",
comment: "Este ficheiro...",
)
]
entries = []
configs.each do |config|
entry = {}
conf = {}
entry.store('string', config.key)
conf.store('value', config.value)
conf.store('comment', config.comment)
entry.store('com.mirth.connect.util.ConfigurationProperty', conf)
entries << entry
end
p entries
puts entries.to_xml(:root => 'map', children: "entry", :skip_types => true)
produces the output:
<?xml version="1.0" encoding="UTF-8"?>
<map>
<entry>
<string>PNB_ALERTLOG_RECEIVER_CHANNEL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>PNB_ALERTLOG_RECEIVER</value>
<comment>Canal que...</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
<entry>
<string>PNB_CFG_FILE_ACCESS_CONTROL</string>
<com.mirth.connect.util.ConfigurationProperty>
<value>resources/configPnbDev/pnbAccessControl.json</value>
<comment>Este ficheiro...</comment>
</com.mirth.connect.util.ConfigurationProperty>
</entry>
</map>
It looks to me like you also have some problems with your entry and conf hashes as every element in the entries array will refer to the same entry and conf hash, and because your loop keeps changing those hashes, each entry in the array will refer to a hash that contains the last key/values set in the loop.

Related

How to sort XML file using Ruby nokogiri

I want to sort this XML such that same type of demographics show first like all staty_type="REACH" appear on top, then all clicks and so on.
Here is an example object:
<?xml version="1.0"?>
<properties date="2020-06-23">
<property>
<order start="2020-06-23" end="2020-06-23">52658</order>
<demographics demographic="Age" stat_type="REACH">
<value category="18-24">36</value>
<value category="25-34">149</value>
</demographics>
<demographics demographic="Age" stat_type="CLICK">
<value category="18-24">6</value>
<value category="25-34">37</value>
</demographics>
<demographics demographic="Gender" stat_type="REACH">
<value category="female">402</value>
<value category="male">188</value>
</demographics>
<demographics demographic="Gender" stat_type="CLICK">
<value category="female">107</value>
<value category="male">44</value>
</demographics>
</property>
</properties>
I'm able to iterate XML. However, unable to perform sorting.
#doc = Nokogiri::XML(File.open("public/test.xml"))
builder = #doc.xpath("//property")
builder.search('./demographics').sort_by{|t| puts t['stat_type']}.each do |table|
puts table.to_s
end
I need the final XML in this form.
<?xml version="1.0"?>
<properties date="2020-06-23">
<property>
<order start="2020-06-23" end="2020-06-23">PBNI152658</order>
<demographics demographic="Age" stat_type="REACH">
<value category="18-24">36</value>
<value category="25-34">149</value>
</demographics>
<demographics demographic="Gender" stat_type="REACH">
<value category="female">402</value>
<value category="male">188</value>
</demographics>
<demographics demographic="Age" stat_type="CLICK">
<value category="18-24">6</value>
<value category="25-34">37</value>
</demographics>
<demographics demographic="Gender" stat_type="CLICK">
<value category="female">107</value>
<value category="male">44</value>
</demographics>
</property>
</properties>
When you do things like builder.search('./demographics') you just create a new nodeset with some nodes filtered from the initial XML document. Even if you sort this new nodeset you don't affect the initial document itself.
To sort the nodes of the initial document you have to rebuild the children of the node in question (<property> in your case). And here comes a tiny additional challenge - there are more nodes parsed by Nokogiri to take into account, not only the ones to sort:
pry(main)> #doc.at_xpath("//property").children.map(&:node_name)
=> ["text", "order", "text", "demographics", "text", "demographics", "text", "demographics", "text", "demographics", "text"]
So, what we have to do is to sort demographics nodes only and keep everything else untouched. One of the ways to do this is:
property_node = #doc.at_xpath("//property")
nodes_to_sort = property_node.children.dup
# My sorting logic is dumb here, apply your own as necessary
sorted_demographics = nodes_to_sort.select { |n| n.node_name == "demographics" }.sort_by { |n| n.attr("stat_type") }.reverse
# Create an empty nodeset. There should be a more idiomatic and readable way but this trick works too
new_nodeset = nodes_to_sort - nodes_to_sort
nodes_to_sort.each do |n|
case n.node_name
when "demographics"
new_nodeset << sorted_demographics.shift
else
new_nodeset << n
end
end
property_node.children = new_nodeset
And voila! - we are sorted now:
pry(main)> puts #doc
<?xml version="1.0"?>
<properties date="2020-06-23">
<property>
<order start="2020-06-23" end="2020-06-23">52658</order>
<demographics demographic="Gender" stat_type="REACH">
<value category="female">402</value>
<value category="male">188</value>
</demographics>
<demographics demographic="Age" stat_type="REACH">
<value category="18-24">36</value>
<value category="25-34">149</value>
</demographics>
<demographics demographic="Gender" stat_type="CLICK">
<value category="female">107</value>
<value category="male">44</value>
</demographics>
<demographics demographic="Age" stat_type="CLICK">
<value category="18-24">6</value>
<value category="25-34">37</value>
</demographics>
</property>
</properties>
NB. Take the solution above with a grain of salt - I don't know nokogiri's XML building capabilities well, so chances are there are some ways to achieve the same result with less code/in a more idiomatic way.

Nokogiri : NoMethodError (undefined method `inner_html' for nil:NilClass)

I'm trying to parse a simple XML data with nokogiri.
this is my XML:
POST /.... HTTP/1.1
Host: ....
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://...."
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="...." xmlns:xsd="...." xmlns:soap="....">
<soap:Body>
<WS_QueryOnSec xmlns="......">
<type>string</type>
<ID>string</ID>
</WS_QueryOnSec>
</soap:Body>
</soap:Envelope>
and this is my simle request:
require "nokogiri"
#doc = Nokogiri::XML(request.body.read)
#something = #doc.at('type').inner_html
But Nokogiri can not find the Type or ID node.
When I change the data into this every thing works fine:
<soap:Body>
<type>string</type>
<ID>string</ID>
</soap:Body>
It seems the problem is the raw text above the data and the nods with xmlns or the other attributes!
What do you recommend to resolve this ?
The first "XML" isn't XML. It's text that contains XML. Remove the header information down to the blank line and try it again.
I think it'd help you to read the XML spec or to read some tutorials about creating XML which will help you understand how it's defined. XML is a tight specification and doesn't allow any deviation. The syntax is pretty flexible, but you have to play by its rules.
Consider these examples:
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
foo
<root>
<node />
</root>
EOT
doc.errors # => [#<Nokogiri::XML::SyntaxError: Start tag expected, '<' not found>]
Removing the text, which is outside the root tag results in a proper parse:
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<root>
<node />
</root>
EOT
doc.errors # => []
<root> isn't neccesarily the name of the "root" node, it's just the outermost tag:
doc = Nokogiri::XML(<<EOT)
<foo>
<node />
</foo>
EOT
doc.errors # => []
and still results in a valid DOM/internal representation of the document:
puts doc.to_html
# >> <foo>
# >> <node></node>
# >> </foo>
Your XML sample is using namespaces, which complicate matters somewhat. The Nokogiri documentation talks about how to deal with them, so you'll want to understand that part of parsing XML because you'll encounter it again. Here's the easy way of working with them:
require 'nokogiri'
doc = Nokogiri::XML(<<EOT)
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:xsi="...." xmlns:xsd="...." xmlns:soap="....">
<Body>
<WS_QueryOnSec xmlns="......">
<type>string</type>
<ID>string</ID>
</WS_QueryOnSec>
</Body>
</Envelope>
EOT
namespaces = doc.collect_namespaces
doc.at('type', namespaces).text # => "string"

How to convert XML to Hash in ruby?

I have a XML code which I want to convert into Hash
<meta_description><language id="1"></language><language id="2"></language></meta_description>
<meta_keywords><language id="1"></language><language id="2"></language></meta_keywords>
<meta_title><language id="1"></language><language id="2" ></language></meta_title>
<link_rewrite><language id="1" >konsult-500-krtim</language><language id="2" >konsult-500-krtim</language></link_rewrite>
<name><language id="1" >Konsult 500 kr/tim</language><language id="2" >Konsult 500 kr/tim</language></name>
<description><language id="1" ></language><language id="2" ></language></description>
<description_short><language id="1" ></language><language id="2" ></language></description_short>
<available_now><language id="1" ></language><language id="2" ></language></available_now>
<available_later><language id="1" ></language><language id="2" ></language></available_later>
<associations>
<categories nodeType="category" api="categories">
<category>
<id>2</id>
</category>
</categories>
<images nodeType="image" api="images"/>
<combinations nodeType="combination" api="combinations"/>
<product_option_values nodeType="product_option_value" api="product_option_values"/>
<product_features nodeType="product_feature" api="product_features"/>
<tags nodeType="tag" api="tags"/>
<stock_availables nodeType="stock_available" api="stock_availables">
<stock_available>
<id>111</id>
<id_product_attribute>0</id_product_attribute>
</stock_available>
</stock_availables>
<accessories nodeType="product" api="products"/>
<product_bundle nodeType="product" api="products"/>
</associations>
I want to convert this xml into Hash .
I try to find functions which convert this xml to h=Hash.new
How I do this?
There is ActiveSupport's Hash#from_xml method that you can use:
xml = File.open("data.xml").read # if your xml is in the 'data.xml' file
Hash.from_xml(xml)
If you are using Rails you can use the answer provided above, otherwise you can require the ActiveSuppport gem:
require 'active_support/core_ext/hash'
xml = '<foo>bar</foo>'
hash = Hash.from_xml(xml)
=>{"foo"=>"bar"}
Note this will only work with valid xml. See comments on op. Also note that using element attributes like id="1" won't convert back the same way for example:
xml = %q(
<root>
<foo id="1"></foo>
<bar id="2"></bar>
</root>).strip
hash = Hash.from(xml)
=>{"root"=>{"foo"=>{"id"=>"1"}, "bar"=>{"id"=>"2"}}}
puts hash.to_xml
# will output
<?xml version="1.0" encoding="UTF-8"?>
<hash>
<root>
<foo>
<id>1</id>
</foo>
<bar>
<id>2</id>
</bar>
</root>
</hash>
Use nokogiri to parse XML response to ruby hash. It's pretty fast.
require 'active_support/core_ext/hash' #from_xml
require 'nokogiri'
doc = Nokogiri::XML(response_body)
Hash.from_xml(doc.to_s)

Selecting the attribute value of XML file with Nokogiri

I am working on a project to parse a xml file into a certain table structure with nokogiri. At the moment I got this in my controller:
def new
doc = Nokogiri::HTML(open('sample3.xml'))
#home = doc.xpath('//match').map do |i|
{'title' => i.at('home')['name'], 'away' => i.at('away')['name']}
end
end
And this is the format of the XML file:
<league country="worldcup" cup="True" id="2889" name="World: World Cup" sub_id="63638038137">
<matches date="12.06.2014">
<match alternate_id="3844428" alternate_id_2="4013768" date="12.06.2014" id="3551903" status="20:00" time="20:00">
<home alternate_id="536380381512" id="2338917" name="Brazil"/>
<away alternate_id="536380381513" id="2340076" name="Croatia"/>
<odds>
<type id="766" name="1x2">
<bookmaker id="947" name="12Bet">
<odd name="1" value="1.27"/>
<odd name="2" value="9.56"/>
<odd name="X" value="5.32"/>
</bookmaker>
<type id="767" name="Home/Away">
<bookmaker id="821" name="188Bet">
<odd name="1" value="1.04"/>
<odd name="2" value="8.50"/>
</bookmaker>
</type>
</odds>
</match>
</matches>
</league>
My codes above are able to select the home team and away team. But how can write the code that select the odd value of type name="1x2"?
Thanks.
Regards,
Yam
try this. it may useful for you
f = File.open("sample3.xml")
=> #<File:sample3.xml>
>> doc = Nokogiri::XML(f)
root = doc.root
>> # again here you'll see the complete XML document output to the console.
>> root["id"]
=> "2889"
Documentation

Rails XML Feed: ID as node attribute

I set up a simple XML feed for a vendor we're using (who refuses to read JSON).
<recipes type="array">
<recipe>
<id type="integer">1</id>
<name>
Hamburgers
</name>
<producturl>
http://test.com
</producturl>
...
</recipe>
...
<recipe>
However, the vendor requests that instead of having an id node, id is an attribute in the parent node. e.g.
<recipes type="array">
<recipe id="1">
<name>
Hamburgers
</name>
<producturl>
http://test.com
</producturl>
...
</recipe>
...
<recipe>
I'm building this with (basically)
xml_feed = []
recipes.each do |recipe|
xml_feed <<{id: recipe.id, name: recipe.name, ...}
end
...
render xml: xml_feed.to_xml(root: 'recipes')
But I'm unsure of how to include the id (or any field) as an attribute in the parent node like that. I googled around and couldn't find anything, nor were the http://api.rubyonrails.org/classes/ActiveRecord/Serialization.html docs very helpful
Thanks!
I would suggest you use the nokogiri gem. It provides all you can possible need for handling XML.
builder = Nokogiri::XML::Builder.new do |xml|
xml.root {
xml.objects {
xml.object.classy.thing!
}
}
end
puts builder.to_xml
<?xml version="1.0"?>
<root>
<objects>
<object class="classy" id="thing"/>
</objects>
</root>
The suggestion to use Nokogiri is fine. Just the sintax should be a little bit different to achive what you have requested:
builder = Nokogiri::XML::Builder.new do |xml|
xml.root {
xml.object('type' => 'Client') {
xml.name 'John'
}
}
end
puts builder.to_xml
<?xml version="1.0"?>
<root>
<object type="Client">
<name>John</name>
</object>
</root>

Resources