Ox Ruby XML Parser and Object Marshaller

repository·develop·Indexed 21 days ago

https://github.com/ohler55/ox

Ox is a high-performance XML parser and Object marshaller for Ruby, designed as a faster alternative to Nokogiri for XML/HTML parsing and a human-readable alternative to Ruby's native Marshal for object serialization. It supports generic XML writing and parsing via Ox::Document and Ox::Element, high-performance stream parsing using a SAX API (Ox::Sax), and the ability to convert XML into Ruby Hashes. Ox also provides a simplified path-based node location system and an 'easy' API for navigating XML structures.

Tokens
6.9K
Snippets
33
Records
38
Agent score
75%

What's inside Ox

  1. Access and manipulate XML attributes with HasAttrs

    develop

    The Ox::HasAttrs module provides an 'easy' API for interacting with XML attributes. Objects including this module treat attributes as a Hash where keys can be either String or Symbol.

    Key capabilities include:

    • Direct Access: Use [] to retrieve values by name.
    • Dynamic Access: Use method_missing to access attributes as if they were method calls (e.g., node.id).
    • Bulk Access: Use attributes to get all attributes as a Hash.
    • Modification: Use []= to set or add attributes.

    Note: When setting attributes via []=, the value is automatically converted to a String.

    # Assuming an object 'node' includes Ox::HasAttrs
    
    # 1. Accessing attributes
    val = node[:id]          # Using Symbol
    val = node['id']        # Using String
    val = node.id          # Using method-style access
    
    # 2. Setting attributes
    node[:class] = 'container'
    node['type'] = 'header'
    
    # 3. Getting all attributes
    all_attrs = node.attributes # Returns a Hash
  2. Use the Ox::Element 'easy' API for XML navigation

    develop

    For simple, regularly formatted XML, you can navigate the document tree by calling methods named after the element or attribute names directly on the Ox::Element object.

    • Elements: Access child elements by their name. If multiple elements with the same name exist, you can specify an index using parentheses, e.g., element.Name(1).
    • Attributes: Access attribute values by calling the attribute name as a method.
    • Text: Use the .text method to retrieve the first String node within an element's children.

    Note: This API raises a NoMethodError if the requested name is not found.

    doc = Ox.parse(%{
    <?xml?>
    <People>
      <Person age="58">
        <given>Peter</given>
        <surname>Ohler</surname>
      </Person>
      <Person>
        <given>Makie</given>
        <surname>Ohler</surname>
      </Person>
    </People>
    })
    
    # Accessing elements and attributes via method calls
    doc.People.Person.given.text  # => "Peter"
    doc.People.Person(1).given.text # => "Makie"
    doc.People.Person.age          # => "58"
  3. How Ox handles XML documents

    develop

    Ox operates in two distinct modes depending on your requirements:

    1. Object Marshalling: A fast Object/XML marshaller designed to replace Ruby's Marshal. It is optimized for speed when converting Ruby objects to XML and back.
    2. Generic XML Processing: A high-performance XML parser and writer. It is designed as a faster replacement for Nokogiri, specifically optimized for parsing and generating XML structures.
  4. Track position and line numbers in Ox::Sax handlers

    develop

    You can track the parser's position within the XML document by initializing specific instance variables in your Ox::Sax subclass's initialize method. The parser will automatically update these variables before each callback is invoked:

    • @line: Updated with the current XML line number.
    • @column: Updated with the column number where the current element or node starts.
    • @pos: If defined, updated with the number of bytes from the start of the document.
    class MySax < ::Ox::Sax
      def initialize
        @line = 0
        @column = 0
        @pos = 0
      end
    
      def start_element(name)
        puts "Starting element #{name} at line #{@line}, col #{@column}, byte #{@pos}"
      end
    end
  5. Handle Ox errors using Ox::Error

    develop

    All errors raised by the Ox gem inherit from Ox::Error. You can use this as a catch-all rescue clause to handle any library-specific exceptions.

    begin
      # Ox operation
    rescue Ox::Error => e
      puts "An Ox error occurred: #{e.message}"
    end
  6. How SAX parsing works in Ox

    develop

    Ox provides a SAX (Simple API for XML) parser for event-based XML parsing, which is ideal for processing very large files or IO streams without loading the entire document into memory.

    To use it, you must create a subclass of Ox::Sax and implement the specific callback methods you wish to trigger. These methods must be public in your subclass; if they remain private, the parser will not call them.

    Argument Types:

    • name arguments are passed as Symbol.
    • str arguments are passed as String.
    • value arguments are passed as Ox::Sax::Value objects.

    Callback Overlap Rules:

    • If you define or make value(value) public, the text(str) method will be ignored for that element.
    • Similarly, if you define or make attr_value(name, value) public, the attr(name, str) method will be ignored.
    • The attrs_done() callback is invoked once all attributes for an element have been read.
    require 'ox'
    
    class MySax < ::Ox::Sax
      def initialize
        @element_names = []
      end
    
      def start_element(name)
        @element_names << name
      end
    end
    
    any = MySax.new()
    File.open('any.xml', 'r') do |f|
      Ox.sax_parse(any, f)
    end
  7. Use Ox::Bag for attribute storage

    develop

    The Ox::Bag class is a generic container used by the Ox storage system to hold attributes for auto-generated classes. It is designed to be read-only; instance variables are added via instance_variable_set, but no explicit setters are provided.

    Attributes can be accessed as if they were methods (without the @ prefix). For example, if an instance variable is named :@x, you can access it by calling .x on the object.

    Note: The initialize method accepting a hash of instance variable symbols is intended for testing purposes only.

    # For testing purposes: creating a bag with specific instance variables
    bag = Ox::Bag.new(:@x => 42, :@y => 57)
    puts bag.x # => 42
    puts bag.y # => 57
  8. Configure Ox for HTML parsing

    develop

    Because HTML is often non-conforming, you should adjust the Ox.default_options to use :generic mode with :tolerant effort and smart: true to handle loose HTML structures.

    Ox.default_options = {
        mode:   :generic,
        effort: :tolerant,
        smart:  true
    }
  9. Use Ox as an XML-RPC parser

    develop

    You can use Ox::StreamParser as a high-performance alternative to the standard library's XML-RPC parser. It is based on REXMLStreamParser but utilizes the Ox engine for parsing. To use it, require both xmlrpc/client and ox/xmlrpc_adapter, then pass a new instance of Ox::StreamParser to your XML-RPC client using set_parser.

    require 'xmlrpc/client'
    require 'ox/xmlrpc_adapter'
    
    client = XMLRPC::Client.new2('http://some_server/rpc')
    client.set_parser(Ox::StreamParser.new)
  10. Perform generic XML writing and parsing

    develop

    Ox provides a way to build XML documents programmatically using Ox::Document, Ox::Element, and Ox::Instruct. You can append elements to each other and add special nodes like Ox::CData, Ox::Comment, or Ox::Raw (for direct injection).

    require 'ox'
    
    doc = Ox::Document.new
    
    instruct = Ox::Instruct.new(:xml)
    instruct[:version] = '1.0'
    instruct[:encoding] = 'UTF-8'
    instruct[:standalone] = 'yes'
    doc << instruct
    
    top = Ox::Element.new('top')
    top[:name] = 'sample'
    doc << top
    
    mid = Ox::Element.new('middle')
    mid[:name] = 'second'
    top << mid
    
    bot = Ox::Element.new('bottom')
    bot[:name] = 'third'
    bot << 'text at bottom'
    mid << bot
    
    other_elements = Ox::Element.new('otherElements')
    other_elements << Ox::CData.new('<sender>John Smith</sender>')
    other_elements << Ox::Comment.new("Director's commentary")
    # other_elements << Ox::DocType.new('content')
    other_elements << Ox::Raw.new('<warning>Be carefull with this! Direct inject into XML!</warning>')
    top << other_elements
    
    xml = Ox.dump(doc)
  11. Serialize and deserialize Ruby objects with Ox

    develop

    Ox can be used as a fast alternative to Ruby's Marshal for object serialization. It uses human-readable XML instead of a binary format. Use Ox.dump(obj) to convert an object to an XML string and Ox.parse_obj(xml) to convert the XML string back into a Ruby object.

    require 'ox'
    
    class Sample
      attr_accessor :a, :b, :c
    
      def initialize(a, b, c)
        @a = a
        @b = b
        @c = c
      end
    end
    
    # Create Object
    obj = Sample.new(1, "bee", ['x', :y, 7.0])
    # Now dump the Object to an XML String.
    xml = Ox.dump(obj)
    # Convert the object back into a Sample Object.
    obj2 = Ox.parse_obj(xml)