mvz/happymapper

View on GitHub
lib/happymapper/anonymous_mapper.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module HappyMapper
  class AnonymousMapper
    def parse(xml_content)
      # TODO: this should be able to handle all the types of functionality that
      # parse is able to handle which includes the text, xml document, node,
      # fragment, etc.
      xml = Nokogiri::XML(xml_content)

      klass = create_happymapper_class_from_node(xml.root)

      # With all the elements and attributes defined on the class it is time
      # for the class to actually use the normal HappyMapper powers to parse
      # the content. At this point this code is utilizing all of the existing
      # code implemented for parsing.
      klass.parse(xml_content, single: true)
    end

    private

    #
    # Borrowed from Active Support to convert unruly element names into a format
    # known and loved by Rubyists.
    #
    def underscore(camel_cased_word)
      word = camel_cased_word.to_s.dup
      word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end

    #
    # Used internally when parsing to create a class that is capable of
    # parsing the content. The name of the class is of course not likely
    # going to match the content it will be able to parse so the tag
    # value is set to the one provided.
    #
    def create_happymapper_class_with_tag(tag_name)
      klass = Class.new
      klass.class_eval do
        include HappyMapper
        tag tag_name
      end
      klass
    end

    #
    # Used internally to create and define the necessary happymapper
    # elements.
    #
    def create_happymapper_class_from_node(node)
      klass = create_happymapper_class_with_tag(node.name)

      klass.namespace node.namespace.prefix if node.namespace

      node.namespaces.each do |prefix, namespace|
        klass.register_namespace prefix, namespace
      end

      node.attributes.each_value do |attribute|
        define_attribute_on_class(klass, attribute)
      end

      node.children.each do |child|
        define_element_on_class(klass, child)
      end

      klass
    end

    #
    # Define a HappyMapper element on the provided class based on
    # the node provided.
    #
    def define_element_on_class(klass, node)
      # When a text node has been provided create the necessary
      # HappyMapper content attribute if the text happens to contain
      # some content.

      if node.text?
        klass.content :content, String if node.content.strip != ""
        return
      end

      # When the node has child elements, that are not text
      # nodes, then we want to recursively define a new HappyMapper
      # class that will have elements and attributes.

      element_type = if node.elements.any? || node.attributes.any?
                       create_happymapper_class_from_node(node)
                     else
                       String
                     end

      element_name = underscore(node.name)
      method = klass.elements.find { |e| e.name == element_name } ? :has_many : :has_one

      options = {}
      options[:tag] = node.name
      namespace = node.namespace
      options[:namespace] = namespace.prefix if namespace
      options[:xpath] = "./" unless element_type == String

      klass.send(method, element_name, element_type, options)
    end

    #
    # Define a HappyMapper attribute on the provided class based on
    # the attribute provided.
    #
    def define_attribute_on_class(klass, attribute)
      klass.attribute underscore(attribute.name), String, tag: attribute.name
    end
  end
end