XmlFu
Convert Ruby Hashes to XML
A hash is meant to be a structured set of data. So is XML. The two are very similar in that they have the capability of nesting information within a tree structure. With XML you have nodes. With Hashes, you have key/value pairs. The value of an XML node is referenced by its parent's name. A hash value is referenced by its key. This basic lesson tells the majority of what you need to know about creating XML via Hashes in Ruby using the XmlFu gem.
Installation
Add this line to your application's Gemfile:
gem 'xml-fu'
And then execute:
$ bundle
Or install it yourself as:
$ gem install xml-fu
Hash Keys
Hash keys are translated into XML nodes (whether it be a document node or attribute node).
Translation
With Ruby, a hash key may be a string or a symbol. XmlFu will convert the symbols into an XML safe name by lower camel-casing them. So :foo_bar will become "fooBar". You may change the conversion algorithm to your liking by setting the XmlFu.symbol_conversion_algorithm to a lambda or proc of your liking.
Built-In Algorithms
- :lower_camelcase (default)
- :camelcase
- :none (result of :sym.to_s)
- :snake_case (alias for :none)
# Built-in Algorithm
XmlFu::Node.symbol_conversion_algorithm = :camelcase
XmlFu.xml( :foo => "bar" ) #=> "<foo>bar</foo>"
XmlFu.xml( "foo" => "bar" ) #=> "<foo>bar</foo>"
# :foo and "foo" both translate to <foo>
# Custom Algorithm
XmlFu.symbol_conversion_algorithm {|sym| sym.downcase }
Types of Nodes
Because there are multiple types of XML nodes, there are also multiple types of keys to denote them.
Self-Closing Nodes (key/)
By default, XmlFu assumes that all XML nodes will contain closing tags. However, if you want to explicitly create a self-closing node, use the following syntax when you define the key.
XmlFu.xml("foo/" => "bar") #=> <foo/>
One thing to take note of this syntax is that XmlFu will ignore ANY value you throw at it if the key syntax denotes a self-closing tag. This is because a self-closing tag cannot have any contents (hence the use for a self-closing tag).
Unescaped Content Nodes (key!)
By default, if you pass a pure string as a value, special characters will be escaped to keep the XML compliant. If you know that the string is valid XML and can be trusted, you can add the exclamation point to the end of the key name to denote that XmlFu should NOT escape special characters in the value.
# Default Functionality (Escaped Characters)
XmlFu.xml("foo" => "<bar/>") #=> "<foo><bar/></foo>"
# Unescaped Characters
XmlFu.xml("foo!" => "<bar/>") #=> "<foo><bar/></foo>"
Attribute Node (@key)
Yes, the attributes of an XML node are nodes themselves, so we need a way of defining them. Since XPath syntax uses @ to denote an attribute, so does XmlFu.
XmlFu.xml(:agent => {
"@id" => "007",
"FirstName" => "James",
"LastName" => "Bond"
})
#=> <agent id="007"><FirstName>James</FirstName><LastName>Bond</LastName></agent>
Hash Values
The value in a key/value pair describes the key/node. Different value types determine the extent of this description.
Simple Values
Simple value types describe the contents of the XML node.
Strings
XmlFu.xml( :foo => "bar" ) #=> "<foo>bar</foo>"
XmlFu.xml( "foo" => "bar" ) #=> "<foo>bar</foo>"
Numbers
XmlFu.xml( :foo => 0 ) #=> "<foo>0</foo>"
XmlFu.xml( :pi => 3.14159 ) #=> "<pi>3.14159</pi>"
Nil
XmlFu.xml( :foo => nil ) #=> "<foo xsi:nil=\"true\"/>"
Hashes
Hash are parsed for their translated values prior to returning a XmlFu value.
XmlFu.xml(:foo => {:bar => {:biz => "bang"} })
#=> "<foo><bar><biz>bang</biz></bar></foo>"
Content in Hash (=)
Should you require setting node attributes as well as setting the value of the XML node, you may use the "=" key in a nested hash to denote explicit content.
XmlFu.xml(:agent => {"@id" => "007", "=" => "James Bond"})
#=> "<agent id=\"007\">James Bond</agent>"
This key will not get around the self-closing node rule. The only nodes that will be used in this case will be attribute nodes and additional content will be ignored.
XmlFu.xml("foo/" => {"@id" => "123", "=" => "You can't see me."})
#=> "<foo id=\"123\"/>"
Arrays
Since the value in a key/value pair is (for the most part) used as the contents of a key/node, there are some assumptions that XmlFu makes when dealing with Array values.
- For a typical key, the contents of the array are considered to be nodes to be contained within the
node.
Array of Hashes
XmlFu.xml( "SecretAgents" => [
{ "agent/" => { "@id"=>"006", "@name"=>"Alec Trevelyan" } },
{ "agent/" => { "@id"=>"007", "@name"=>"James Bond" } }
])
#=> "<SecretAgents><agent name=\"Alec Trevelyan\" id=\"006\"/><agent name=\"James Bond\" id=\"007\"/></SecretAgents>"
Alternate Array of Hashes (key*)
There comes a time that you may want to declare the contents of an array as a collection of items denoted by the key name. Using the asterisk (also known for multiplication --- hence multiple keys) we denote that we want a collection of <key> nodes.
XmlFu.xml( "person*" => ["Bob", "Sheila"] )
#=> "<person>Bob</person><person>Sheila</person>"
In this case, the value of "person*" is an array of two names. These names are to be the contents of multiple <person> nodes and the result is a set of sibling XML nodes with no parent.
How about a more complex example:
XmlFu.xml(
"person*" => {
"@foo" => "bar",
"=" => [
{"@foo" => "nope", "=" => "Bob"},
"Sheila"
]
}
)
#=> "<person foo=\"nope\">Bob</person><person foo=\"bar\">Sheila</person>"
This is getting interesting, isn't it? In this example, we are setting a default "foo" attribute on each of the items in the collection of <person> nodes. However, you'll notice that we overwrote the default "foo" with Bob.
Array of Arrays
Array values are flattened prior to translation, to reduce the need to iterate over nested arrays.
XmlFu.xml(
:foo => [
[{"a/" => nil}, {"b/" => nil}],
{"c/" => nil},
[
[{"d/" => nil}, {"e/" => nil}],
{"f/" => nil}
]
]
)
#=> "<foo><a/><b/><c/><d/><e/><f/></foo>"
:foo in this case, is the parent node of it's contents
Array of Mixed Types
Since with simple values, you cannot infer the value of their node container purely on their value, simple values are currently ignored in arrays and only Hashes are translated.
"foo" => [
{:bar => "biz"},
nil, # ignored
true, # ignored
false, # ignored
42, # ignored
3.14, # ignored
"simple string", # ignored
['another','array','of','values'] # ignored
]
#=> "<foo><bar>biz</bar></foo>"
Options
- :instruct => true
- Adds <xml version="1.0" encoding="UTF-8"?> to generated XML
Cheat Sheet
Key
- if key denotes self-closing node (key/)
- attributes are preserved with Hash values
- value and "=" values are ignored
- if key denotes collection (key*) with Array value
- Array is flattened
- Only Hash and Simple values are translated
- Hashes may override default attributes set by parent
- (applies to Array values only)
- if key denotes contents (key) with Array value
- Array is flattened
- Only Hash items in array are translated
Value
- if value is Hash:
- "@" keys are attributes of the node
- "=" key can be used in conjunction with any "@" keys to specify content of node
- if value is simple value:
- it is content of
node - unless: key denotes a self-closing node
- it is content of
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Added some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request