module XmlMapper::ClassMethods

Public Instance Methods

after_parse(&block) click to toggle source

Register a new after_parse callback, given as a block.

@yield [object] Yields the newly-parsed object to the block after parsing.

Sub-objects will be already populated.
# File lib/xmlmapper.rb, line 199
def after_parse(&block)
  after_parse_callbacks.push(block)
end
after_parse_callbacks() click to toggle source

The list of registered after_parse callbacks.

# File lib/xmlmapper.rb, line 190
def after_parse_callbacks
  @after_parse_callbacks ||= []
end
attribute(name, type, options={}) click to toggle source

The xml has the following attributes defined.

@example

"<country code='de'>Germany</country>"

# definition of the 'code' attribute within the class
attribute :code, String

@param [Symbol] name the name of the accessor that is created @param [String,Class] type the class name of the name of the class whcih

the object will be converted upon parsing

@param [Hash] options additional parameters to send to the relationship

# File lib/xmlmapper.rb, line 55
def attribute(name, type, options={})
  attribute = Attribute.new(name, type, options)
  @attributes[name] = attribute
  attr_accessor attribute.method_name.intern
end
attributes() click to toggle source

The elements defined through {#attribute}.

@return [Array<Attribute>] a list of the attributes defined for this class;

an empty array is returned when there have been no attributes defined.
# File lib/xmlmapper.rb, line 67
def attributes
  @attributes.values
end
content(name, type=String, options={}) click to toggle source

The value stored in the text node of the current element.

@example

"<firstName>Michael Jackson</firstName>"

# definition of the 'firstName' text node within the class

content :first_name, String

@param [Symbol] name the name of the accessor that is created @param [String,Class] type the class name of the name of the class whcih

the object will be converted upon parsing. By Default String class will be taken.

@param [Hash] options additional parameters to send to the relationship

# File lib/xmlmapper.rb, line 143
def content(name, type=String, options={})
  @content = TextNode.new(name, type, options)
  attr_accessor @content.method_name.intern
end
element(name, type, options={}) click to toggle source

An element defined in the XML that is parsed.

@example

"<address location='home'>
   <city>Oldenburg</city>
 </address>"

# definition of the 'city' element within the class

element :city, String

@param [Symbol] name the name of the accessor that is created @param [String,Class] type the class name of the name of the class whcih

the object will be converted upon parsing

@param [Hash] options additional parameters to send to the relationship

# File lib/xmlmapper.rb, line 110
def element(name, type, options={})
  element = Element.new(name, type, options)
  @elements[name] = element
  attr_accessor element.method_name.intern
end
elements() click to toggle source

The elements defined through {#element}, {#has_one}, and {#has_many}.

@return [Array<Element>] a list of the elements contained defined for this

class; an empty array is returned when there have been no elements
defined.
# File lib/xmlmapper.rb, line 123
def elements
  @elements.values
end
has_many(name, type, options={}) click to toggle source

The object has many of these elements in the XML.

@param [Symbol] name the name of accessor that is created @param [String,Class] type the class name or the name of the class which

the object will be converted upon parsing.

@param [Hash] options additional parameters to send to the relationship

@see element

# File lib/xmlmapper.rb, line 183
def has_many(name, type, options={})
  element name, type, {:single => false}.merge(options)
end
has_one(name, type, options={}) click to toggle source

The object has one of these elements in the XML. If there are multiple, the last one will be set to this value.

@param [Symbol] name the name of the accessor that is created @param [String,Class] type the class name of the name of the class whcih

the object will be converted upon parsing

@param [Hash] options additional parameters to send to the relationship

@see element

# File lib/xmlmapper.rb, line 169
def has_one(name, type, options={})
  element name, type, {:single => true}.merge(options)
end
has_xml_content() click to toggle source

Sets the object to have xml content, this will assign the XML contents that are parsed to the attribute accessor xml_content. The object will respond to the method xml_content and will return the XML data that it has parsed.

# File lib/xmlmapper.rb, line 154
def has_xml_content
  attr_accessor :xml_content
end
namespace(namespace = nil) click to toggle source

Specify a namespace if a node and all its children are all namespaced elements. This is simpler than passing the :namespace option to each defined element.

@param [String] namespace the namespace to set as default for the class

element.
# File lib/xmlmapper.rb, line 211
def namespace(namespace = nil)
  @namespace = namespace if namespace
  @namespace
end
nokogiri_config_callback() click to toggle source

The callback defined through {.with_nokogiri_config}.

@return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.

# File lib/xmlmapper.rb, line 268
def nokogiri_config_callback
  @nokogiri_config_callback
end
parse(xml, options = {}) { |part| ... } click to toggle source

@param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML

contents to convert into Object.

@param [Hash] options additional information for parsing. :single => true

if requesting a single object, otherwise it defaults to retuning an
array of multiple items. :xpath information where to start the parsing
:namespace is the namespace to use for additional information.
# File lib/xmlmapper.rb, line 289
def parse(xml, options = {})

  # create a local copy of the objects namespace value for this parse execution
  namespace = @namespace

  # If the XML specified is an Node then we have what we need.
  if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
    node = xml
  else

    # If xml is an XML document select the root node of the document
    if xml.is_a?(Nokogiri::XML::Document)
      node = xml.root
    else

      # Attempt to parse the xml value with Nokogiri XML as a document
      # and select the root element
      xml = Nokogiri::XML(
        xml, nil, nil,
        Nokogiri::XML::ParseOptions::STRICT,
        &nokogiri_config_callback
      )
      node = xml.root
    end

    # if the node name is equal to the tag name then the we are parsing the
    # root element and that is important to record so that we can apply
    # the correct xpath on the elements of this document.

    root = node.name == tag_name
  end

  # if any namespaces have been provied then we should capture those and then
  # merge them with any namespaces found on the xml node and merge all that
  # with any namespaces that have been registered on the object

  namespaces = options[:namespaces] || {}
  namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
  namespaces = namespaces.merge(@registered_namespaces)

  # if a namespace has been provided then set the current namespace to it
  # or set the default namespace to the one defined under 'xmlns'
  # or set the default namespace to the namespace that matches 'xmlmapper's

  if options[:namespace]
    namespace = options[:namespace]
  elsif namespaces.has_key?("xmlns")
    namespace ||= DEFAULT_NS
    namespaces[DEFAULT_NS] = namespaces.delete("xmlns")
  elsif namespaces.has_key?(DEFAULT_NS)
    namespace ||= DEFAULT_NS
  end

  # from the options grab any nodes present and if none are present then
  # perform the following to find the nodes for the given class

  nodes = options.fetch(:nodes) do

    # when at the root use the xpath '/' otherwise use a more gready './/'
    # unless an xpath has been specified, which should overwrite default
    # and finally attach the current namespace if one has been defined
    #

    xpath  = (root ? '/' : './/')
    xpath  = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
    xpath += "#{namespace}:" if namespace

    nodes = []

    # when finding nodes, do it in this order:
    # 1. specified tag if one has been provided
    # 2. name of element
    # 3. tag_name (derived from class name by default)

    # If a tag has been provided we need to search for it.

    if options.key?(:tag)
      begin
        nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
      rescue
        # This exception takes place when the namespace is often not found
        # and we should continue on with the empty array of nodes.
      end
    else

      # This is the default case when no tag value is provided.
      # First we use the name of the element `items` in `has_many items`
      # Second we use the tag name which is the name of the class cleaned up

      [options[:name], tag_name].compact.each do |xpath_ext|
        begin
          nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
        rescue
          break
          # This exception takes place when the namespace is often not found
          # and we should continue with the empty array of nodes or keep looking
        end
        break if nodes && !nodes.empty?
      end

    end

    nodes
  end

  # Nothing matching found, we can go ahead and return
  return ( ( options[:single] || root ) ? nil : [] ) if nodes.size == 0

  # If the :limit option has been specified then we are going to slice
  # our node results by that amount to allow us the ability to deal with
  # a large result set of data.

  limit = options[:in_groups_of] || nodes.size

  # If the limit of 0 has been specified then the user obviously wants
  # none of the nodes that we are serving within this batch of nodes.

  return [] if limit == 0

  collection = []

  nodes.each_slice(limit) do |slice|

    part = slice.map do |n|

      # If an existing XmlMapper object is provided, update it with the
      # values from the xml being parsed.  Otherwise, create a new object

      obj = options[:update] ? options[:update] : new

      attributes.each do |attr|
        value = attr.from_xml_node(n, namespace, namespaces)
        value = attr.default if value.nil?
        obj.send("#{attr.method_name}=", value)
      end

      elements.each do |elem|
        obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
      end

      if @content
        obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces))
      end

      # If the XmlMapper class has the method #xml_value=,
      # attr_writer :xml_value, or attr_accessor :xml_value then we want to
      # assign the current xml that we just parsed to the xml_value

      if obj.respond_to?('xml_value=')
        # n.namespaces.each {|name,path| n[name] = path }

        obj.xml_value = n.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0, n.document.collect_namespaces.keys.map { |name| name.split(":").last })
        # obj.xml_value = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
      end

      if obj.respond_to?('xml_node=')
        obj.xml_node = n
      end

      # If the XmlMapper class has the method #xml_content=,
      # attr_write :xml_content, or attr_accessor :xml_content then we want to
      # assign the child xml that we just parsed to the xml_content

      if obj.respond_to?('xml_content=')
        n = n.children if n.respond_to?(:children)
        obj.xml_content = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
      end

      # Call any registered after_parse callbacks for the object's class

      obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }

      # collect the object that we have created

      obj
    end

    # If a block has been provided and the user has requested that the objects
    # be handled in groups then we should yield the slice of the objects to them
    # otherwise continue to lump them together

    if block_given? and options[:in_groups_of]
      yield part
    else
      collection += part
    end

  end

  # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
  nodes = nil

  # If the :single option has been specified or we are at the root element
  # then we are going to return the first item in the collection. Otherwise
  # the return response is going to be an entire array of items.

  if options[:single] or root
    collection.first
  else
    collection
  end
end
register_namespace(namespace, ns) click to toggle source

Register a namespace that is used to persist the object namespace back to XML.

@example

register_namespace 'prefix', 'http://www.unicornland.com/prefix'

# the output will contain the namespace defined

"<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
...
</outputXML>"

@param [String] namespace the xml prefix @param [String] ns url for the xml namespace

# File lib/xmlmapper.rb, line 88
def register_namespace(namespace, ns)
  @registered_namespaces.merge!({namespace => ns})
end
tag(new_tag_name) click to toggle source

@param [String] new_tag_name the name for the tag

# File lib/xmlmapper.rb, line 219
def tag(new_tag_name)
  @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
end
tag_name() click to toggle source

The name of the tag

@return [String] the name of the tag as a string, downcased

# File lib/xmlmapper.rb, line 228
def tag_name
  @tag_name ||= to_s.split('::')[-1].downcase
end
with_nokogiri_config(&blk) click to toggle source

Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse(). See nokogiri.org/Nokogiri/XML/Document.html#method-c-parse

@param [Proc] the proc to pass to Nokogiri to setup parse options

# File lib/xmlmapper.rb, line 277
def with_nokogiri_config(&blk)
  @nokogiri_config_callback = blk
end
wrap(name, &blk) click to toggle source

There is an XML tag that needs to be known for parsing and should be generated during a to_xml. But it doesn't need to be a class and the contained elements should be made available on the parent class

@param [String] name the name of the element that is just a place holder @param [Proc] blk the element definitions inside the place holder tag

# File lib/xmlmapper.rb, line 239
def wrap(name, &blk)
  # Get an anonymous XmlMapper that has 'name' as its tag and defined
  # in '&blk'.  Then save that to a class instance variable for later use
  wrapper = AnonymousWrapperClassFactory.get(name, &blk)
  @wrapper_anonymous_classes[wrapper.inspect] = wrapper

  # Create getter/setter for each element and attribute defined on the anonymous XmlMapper
  # onto this class. They get/set the value by passing thru to the anonymous class.
  passthrus = wrapper.attributes + wrapper.elements
  passthrus.each do |item|
    class_eval %{
      def #{item.method_name}
        @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
        @#{name}.#{item.method_name}
      end
      def #{item.method_name}=(value)
        @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
        @#{name}.#{item.method_name} = value
      end
    }
  end

  has_one name, wrapper
end