Xml Dsl
Xml Dsl adds DSL for defining easy parsers (or mappers) for XML via nokogiri XML objects.
Install
Add xml_dsl
gem to Gemfile
:
gem 'xml_dsl', '~> 0.1.1'
Usage
Let's pretend we have an XML output of a following structure
<root>
<offer>
<id>703134</id>
<distance>10</distance>
<house>38a</house>
<rooms>
<room>1</room>
</rooms>
<prices>
<price>2200000</price>
</prices>
<currency>RUR</currency>
<floor>7</floor>
<nfloor>17</nfloor>
<areas>
<area type="total">37</area>
<area type="live">0</area>
<area type="kitchen">10</area>
</areas>
</offer>
<offer>
<id>703102</id>
<distance>25</distance>
<house>7</house>
<rooms>
<room>1</room>
</rooms>
<prices>
<price>2650000</price>
</prices>
<currency>RUR</currency>
<floor>1</floor>
<nfloor>5</nfloor>
<areas>
<area type="total">30</area>
<area type="live">17</area>
<area type="kitchen">7</area>
</areas>
<magick>woodoo</magick>
</offer>
</root>
Then we can define our mapper as following
class Parser
attr_accessor :xml, :external
def initialize(xml, external)
# Normal iteration goes from xml instance_variable
self.xml = xml
self.external = external
end
#
# Here we definig parser, which will put attributes to Hash, and look for <offer> nodes inside <root>
# any length of root path can be provided, also attributes full path is also a choise
# define_xml_parser arbitrary_class, :root, "offer[type=uniq]"
# You can pass as first argument not only Hash, but any Ruby class capable to set attributes defined in fields
# define_xml_parser OpenStruct
# define_xml_parser NiftyActiveRecordModel
define_xml_parser Hash, :root, :offer do
# Here is basic validation blocks
# They MUST return some bool (if block)
before_parse? do |node|
!node.search('magick').empty?
end
# Or just check for must_have key (or path to it)
before_parse? :jim
before_parse? [:areas, 'area[type=hobby]']
# We can define a block to call every time an XmlDsl::ParseError os raised
# it is raised if null: true is passed to field declaration, or manually via raise XmlDsl::ParseError
error_handle do |e, node|
external.notify node
end
# Field declaration
field :id, :id, matcher: :to_i
# We can pass getter to call on Nokogiri::Xml::Element (default :text) and matcher (default :to_s)
field :minutes, :distance, matcher: :to_i
# If null: true (default false) is passed then if field is empty - it will raise XmlDsl::ParseError
# and then triggers error_handle blocks
field :magick, :magick, null: true
# We can pass long xpath to find proper node inside our XML
# like this:
field :amount, [:prices, :price], matcher: :to_i
# or even like this:
field :total_area, [:areas, 'area[type=total]'], matcher: :to_i
# If our logic is more complex we can define it inside block
field :full_area, null: true do |instance, node|
if !instance[:area]
node.search('areas').reduce(0) { |acc,n| acc + n.text.to_i }
else
0
end
end
# If something goes wrong - just raise XmlDsl::ParseError manually
field :fuu do |_, node|
raise XmlDsl::ParseError, 'FUUU' if node.search(:areas).length > 5
end
end
end
Then you can use your newly defined parser as you wish:
parser = Parser.new(nifty_xml, some_logger_eg)
# just iterate with block
parser.iterate do |instance|
do_stuff_with_newly_mapped_instance_whatever(instance)
end
# or you can pass some accumulator for your instances to be put into
parser.iterate acc: []
# or event call a method on instances without getting them actually
parser.iterate after_method: :save
Contributing
Feel free to request for some features or fork - implement - pul request.