Nori

CI Gem Version Coverage Status

Really simple XML parsing ripped from Crack, which ripped it from Merb.

Nori supports pluggable parsers and ships with both REXML and Nokogiri implementations.
It defaults to Nokogiri since v2.0.0, but you can change it to use REXML via:

Nori.new(:parser => :rexml)  # or :nokogiri

Make sure Nokogiri is in your LOAD_PATH when parsing XML, because Nori tries to load it when it's needed.

Examples

Nori.new.parse("<tag>This is the content</tag>")
# => {"tag"=>"This is the content"}

Nori.new.parse('<foo />')
#=> {"foo"=>nil}

Nori.new.parse('<foo bar />')
#=> {}

Nori.new.parse('<foo bar="baz"/>')
#=> {"foo"=>{"@bar"=>"baz"}}

Nori.new.parse('<foo bar="baz">Content</foo>')
#=> {"foo"=>"Content"}

Nori::StringWithAttributes

You can access a string node's attributes via attributes.

result = Nori.new.parse('<foo bar="baz">Content</foo>')
#=> {"foo"=>"Content"}

result["foo"].class
# => Nori::StringWithAttributes

result["foo"].attributes
# => {"bar"=>"baz"}

Because StringWithAttributes hides the attributes on an accessor, they are lost on a plain to_json and callers have to type-check each value. The serializable profile returns a plain Hash instead and will probably be the default in 3.0.

advanced_typecasting

Nori can automatically convert string values to TrueClass, FalseClass, Time, Date, and DateTime:

# "true" and "false" String values are converted to `TrueClass` and `FalseClass`.
Nori.new.parse("<value>true</value>")
# => {"value"=>true}

# String values matching xs:time, xs:date and xs:dateTime are converted to `Time`, `Date` and `DateTime` objects.
Nori.new.parse("<value>09:33:55.7Z</value>")
# => {"value"=>2022-09-29 09:33:55.7 UTC

# disable with advanced_typecasting: false
Nori.new(advanced_typecasting: false).parse("<value>true</value>")
# => {"value"=>"true"}

strip_namespaces

Nori can strip the namespaces from your XML tags. This feature is disabled by default.

Nori.new.parse('<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"></soap:Envelope>')
# => {"soap:Envelope"=>{"@xmlns:soap"=>"http://schemas.xmlsoap.org/soap/envelope/"}}

Nori.new(:strip_namespaces => true).parse('<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"></soap:Envelope>')
# => {"Envelope"=>{"@xmlns:soap"=>"http://schemas.xmlsoap.org/soap/envelope/"}}

convert_tags_to

Nori lets you specify a custom formula to convert XML tags to Hash keys using convert_tags_to.

Nori.new.parse('<userResponse><accountStatus>active</accountStatus></userResponse>')
# => {"userResponse"=>{"accountStatus"=>"active"}}

parser = Nori.new(:convert_tags_to => lambda { |tag| Nori::StringUtils.snakecase(tag).to_sym })
parser.parse('<userResponse><accountStatus>active</accountStatus></userResponse>')
# => {:user_response=>{:account_status=>"active"}}

convert_dashes_to_underscores

By default, Nori will automatically convert dashes in tag names to underscores.

Nori.new.parse('<any-tag>foo bar</any-tag>')
# => {"any_tag"=>"foo bar"}

# disable with convert_dashes_to_underscores
parser = Nori.new(:convert_dashes_to_underscores => false)
parser.parse('<any-tag>foo bar</any-tag>')
# => {"any-tag"=>"foo bar"}

empty_tag_value

By default, an empty tag becomes nil. The empty_tag_value option replaces that value.

Nori.new.parse('<foo/>')
# => {"foo"=>nil}

Nori.new(:empty_tag_value => "").parse('<foo/>')
# => {"foo"=>""}

The option does not apply to empty tags with attributes. They become a hash of their prefixed attributes instead.

Nori.new(:empty_tag_value => "").parse('<foo bar="baz"/>')
# => {"foo"=>{"@bar"=>"baz"}}

consistent_empty_tags

With consistent_empty_tags, every empty tag (with or without attributes) becomes the empty_tag_value.

Nori.new(:consistent_empty_tags => true).parse('<foo bar="baz"/>')
# => {"foo"=>nil}

A string empty_tag_value keeps the attributes accessible on the value, and an explicit xsi:nil="true" always becomes nil.

parser = Nori.new(:consistent_empty_tags => true, :empty_tag_value => "")

result = parser.parse('<foo bar="baz"/>')
# => {"foo"=>""}

result["foo"].attributes
# => {"bar"=>"baz"}

parser.parse('<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>')
# => {"foo"=>nil}

standards

standards is a profile that turns on Nori's spec-correct parsing as a group, rather than one flag at a time. It is opt-in on the 2.x line and becomes the default in Nori 3.0.

Nori.new(:standards => true)

Members of the profile:

  • xml:space honoring (XML 1.0 §2.10). Whitespace-only text under xml:space="preserve" is kept instead of stripped. The nearest ancestor that sets xml:space wins, and xml:space="default" resets to the stripping behavior.

    Nori.new(:standards => true).parse('<name xml:space="preserve">   </name>')
    # => {"name"=>"   "}
    
  • the XML string-value model for empty elements. The profile implies consistent_empty_tags: true with empty_tag_value: "". Both remain overridable, so passing either option explicitly wins over the profile.

    Nori.new(:standards => true).parse('<foo bar="baz"/>')
    # => {"foo"=>""}
    
  • no schema-less typing. Without a schema, character data is just text, so the profile implies advanced_typecasting: false (overridable) and ignores the bare type= and nil= attribute conventions that Nori inherited from Rails' Hash.from_xml. A type="integer" no longer casts, a type="array" no longer folds children into an Array, and both stay visible as ordinary attributes. Only the prefixed xsi:nil="true" still marks an element nil, because that convention comes from XML Schema Instance itself.

    Nori.new(:standards => true).parse('<id type="integer">123</id>')
    # => {"id"=>"123"}    (with "type" accessible via #attributes)
    

serializable

serializable is a profile that makes Nori return plain, directly-serializable data with no custom value classes. It is opt-in on the 2.x line and will probably become the default in 3.0.

Nori.new(:serializable => true)

Members of the profile:

  • text nodes with attributes become a Hash. A tag with both text content and attributes maps to the XML JSON convention ({"#text" => content} merged with the @-prefixed attributes) instead of a Nori::StringWithAttributes. This is the same @-keyed shape element nodes already use, so the attributes survive to_json, to_yaml, and Marshal. A text node without attributes stays a plain String. Typecast values keep their attributes the same way ({"#text" => true, "@source" => "ldap"}) – attributes are never silently dropped under the profile. The #text key goes through convert_tags_to like every other key, so a key-converting formula can reshape it (or drop the # entirely).

    Nori.new(:serializable => true).parse('<foo bar="baz">Content</foo>')
    # => {"foo"=>{"#text"=>"Content", "@bar"=>"baz"}}
    
    Nori.new(:serializable => true).parse('<foo>Content</foo>')
    # => {"foo"=>"Content"}
    
  • type="file" nodes are not decoded. Without the profile, a node carrying type="file" is base64-decoded into a Nori::StringIOFile with its filename and content type. The profile skips that. The node folds into text and attributes like any other, leaving the base64 content as a plain String for the caller to decode. This type= decoding is a Rails/ActiveSupport Hash.from_xml convention Nori inherited through crack and merb, not part of any XML specification, so the plain-data profile leaves it behind.

    Nori.new(:serializable => true).parse('<doc type="file" name="x.pdf">aGVsbG8=</doc>')
    # => {"doc"=>{"#text"=>"aGVsbG8=", "@type"=>"file", "@name"=>"x.pdf"}}