lib/hermod/xml_section.rb



require 'xml'
require 'hermod/xml_section_builder'
require 'hermod/sanitisation'

module Hermod
  # A representation of a section of XML sent to HMRC using the Government
  # Gateway
  class XmlSection
    include Sanitisation

    # Public: builds a new class using the XmlSectionBuilder DSL
    #
    # Returns the new Class
    def self.build(options = {}, &block)
      Class.new(XmlSection).tap do |new_class|
        options.each do |name, value|
          new_class.public_send "#{name}=", value
        end
        XmlSectionBuilder.new(new_class).build(&block)
      end
    end

    attr_reader :attributes

    # Public: turns the XmlSection into an XML::Node instance (from
    # libxml-ruby). This creates this as a node, adds any attributes (after
    # sanitising them according to HMRC's rules) and then adds child nodes in
    # the order they were defined in the DSL. Nodes that have been called multiple
    # times are added in the order they were called.
    #
    # Returns an XML::Node
    def to_xml
      XML::Node.new(self.class.xml_name).tap do |root_node|
        # Add attributes
        attributes.each do |attribute_name, attribute_value|
          sane_value = sanitise_attribute(attribute_value)
          root_node[attribute_name] = sane_value if sane_value.present?
        end
        # Add child nodes
        self.class.node_order.each do |node_name|
          nodes[node_name].each do |node|
            root_node << node.to_xml
          end
        end
      end
    end

    # Internal: creates an XmlSection. This shouldn't normally be called
    # directly, instead the subclasses call it as they define a useful
    # NODE_ORDER.
    #
    # name  - a Symbol that corresponds to the node name in NODE_ORDER
    # block - a Block that will be executed in the context of this class for
    #         setting up descendents.
    def initialize(attributes={}, &block)
      @attributes = attributes
      yield self if block_given?
    end

    class << self
      attr_writer :xml_name, :formats
      attr_accessor :node_order
    end

    # Internal: a class method for getting the name of the XML node used when
    # converting instances to XML for HMRC. If the `xml_name` has been set then
    # it will be used, otherwise the class name will be used as a default.
    #
    # Returns a String
    def self.xml_name
      @xml_name ||= name.demodulize
    end

    # Internal: provides access to the formats hash, falling back on an empty
    # hash by default. These formats are used by the date and monetary nodes
    # for converting their values to strings HMRC will accept.
    #
    # Returns a Hash
    def self.formats
      @formats ||= {
        date: "%Y-%m-%d",
        datetime: "%Y-%m-%d %H:%M:%S",
        money: "%.2f",
      }
    end

    # Internal: provides access to the hash of nodes where the default for an
    # unspecified key is an empty array. This stores the nodes as they are
    # created with the key being the name of the node (which is the name of the
    # method called to set it) and the value being an array of all the values
    # set on this node in the order they are set.
    #
    # Returns a Hash
    def nodes
      @nodes ||= Hash.new { |h, k| h[k] = [] }
    end

    private

    # Private: a convenience method for getting the format string for a given
    # key.
    #
    # Returns a format String
    # Raises a KeyError if the requested format is not found
    def format_for(type)
      self.class.formats.fetch(type)
    end
  end
end