Decontaminate is a tool for extracting information from large, potentially nested XML documents. It provides a simple Ruby DSL for selecting values from Nokogiri objects and storing the results in JSON-like Ruby hashes and arrays.
Installation
Add this line to your application's Gemfile:
gem 'decontaminate'
And then execute:
$ bundle
Or install it yourself as:
$ gem install decontaminate
Usage
Decontaminate provides a DSL for creating decontaminators, which, when instantiated, accept XML nodes or documents and produce a hash as a result. To start, create a class that inherits from Decontaminate::Decontaminator
:
class MyDecontaminator < Decontaminate::Decontaminator
end
If parsing an entire document, you should specify the name of the root element:
class MyDecontaminator < Decontaminate::Decontaminator
self.root = 'User'
end
Scalar Values
To select values from the XML document, use the scalar
class method:
class MyDecontaminator < Decontaminate::Decontaminator
self.root = 'User'
scalar 'Name'
scalar 'Age', type: :integer
scalar 'DateRegistered', key: 'registered_at'
end
This might produce a result like the following:
=> MyDecontaminator.new(xml_document).as_json
{
'name' => 'Jane Smith',
'age' => 28,
'registered_at' => '2013-08-16T20:51:34.236Z'
}
The first argument to scalar
is the name of the node to extract data from. In fact, this can be any XPath string relative to the document root. By default, the resulting JSON key is inferred from the provided path, but it can also be overridden with the key:
argument. Additionally, the type of the scalar can be specified with the type:
argument, which defaults to :string
.
Attributes can be specified with XPath syntax by prepending an @
sign:
scalar '@id', type: :integer
Scalar Transformers
In addition to customization of the parser using the type:
keyword argument, scalar
can be provided with a block that will allow custom transformation of the value. It will be supplied with the value as parsed according to the provided type, and the return value will be the result stored in the output.
scalar 'RatingPercentage', key: 'rating_ratio', type: :float do |percentage|
percentage && percentage / 100.0
end
Transformer blocks are evaluated in the context of the decontaminator instance, so instance methods can be called. Additionally, it is possible to call instance methods as transformers directly without needing to pass a block by passing the name of the method as the transformer:
keyword argument.
scalar 'RatingPercentage',
key: 'rating_ratio',
type: :float,
transformer: :percentage_to_ratio
def percentage_to_ratio(percentage)
percentage && percentage / 100.0
end
Nested Values
It's also possible to specify nested or even deeply nested hashes with the hash
class method:
hash 'UserProfile', key: 'profile' do
scalar 'Description'
hash 'Specialization' do
scalar 'Area'
scalar 'Expertise', type: :float
end
end
The hash
method accepts a block, which works just like the class body, but all paths are scoped to the path passed to hash
. The key
argument is optional, just like with scalar
.
Sometimes it may be useful to create an additional hash in the output as an organizational tool, even though there is no equivalent nesting in the input XML. In this case, the XPath argument may be omitted, specifying only key:
.
hash key: 'info' do
scalar 'Email'
end
This will fetch a value from the Email
node on the root, but it will be stored in a property within a separate hash, keyed in the result with 'info'
.
Array Data
In addition to the scalar
and hash
methods, there are plural forms which allow parsing and extracting data that appears many times within a single document. These are named scalars
and hashes
, respectively. They work much like their singular counterparts, but the provided path should match multiple elements.
For example, given the following decontaminator:
class ArticlesDecontaminator < Decontaminate::Decontaminator
hashes 'Articles' do
scalar 'Name'
scalars 'Tags'
end
end
And given the following XML document:
<Articles>
<Article>
<Name>Article A</Name>
<Tags>
<Tag>News</Tag>
<Tag>Technology</Tag>
</Tags>
</Article>
<Article>
<Name>Article B</Name>
<Tags>
<Tag>Sports</Tag>
<Tag>Recreation</Tag>
</Tags>
</Article>
</Articles>
The resulting object will have the following structure:
{
'articles' => [
{
'name' => 'Article A',
'tags' => ['News', 'Technology']
},
{
'name' => 'Article B',
'tags' => ['Sports', 'Recreation']
}
]
}
There are some special things to note in the above example:
-
The name of the individual elements is inferred from the parent key.
In both cases, the parent element was the plural form of its children (
Articles
/Article
andTags
/Tag
). Since this is common, the plural forms automatically perform this name inference.Since this behavior is sometimes unwanted, it can be disabled by passing the path as an explicit
path:
keyword argument.scalars path: 'Tags/TagName', key: 'tags' # Performs no name inference
-
No
root
element was specified since the root element is a plural.When using name inference for a plural element at the root, specifying the root element is an error. By using the explicit
path:
form mentioned above,root
could still be specified.self.root = 'Articles' hashes path: 'Article', key: 'articles' do; ...; end
Tuple Data
Complementing scalar
and hash
is tuple
, which accepts multiple paths and returns a fixed-length array containing an element for each path.
tuple ['Height/text()', 'Height/@units'], key: 'height_with_units'
The tuple
method is most useful when supplied with a block, which works like scalar
's value transformer, but is supplied with an argument for each path. This allows values to be parsed from multiple values in the source document.
tuple ['Height/text()', 'Height/@units'], key: 'height_cm' do |height, units|
convert_units height.to_f, from: units, to: 'cm'
end
Tuples also support the shorthand transformer:
argument that scalar
and scalars
support.
Flattening nested data
Since source data is sometimes more nested than is desired, the with
method is a helper for scoping decontamination directives to a given XML element without increasing the nesting depth of the resulting object. Like hash
, it accepts an XPath and a block, but the attributes created from within the block will not be wrapped in a hash.
with 'Some/Nested/Data' do
scalar 'Value'
end
There is no plural form for with
since it would, by necessity, create duplicate keys.