lib/happymapper/anonymous_mapper.rb
# 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