lib/happymapper.rb
# frozen_string_literal: true
require "nokogiri"
require "date"
require "time"
require "happymapper/version"
require "happymapper/anonymous_mapper"
require "happymapper/class_methods"
module HappyMapper
class Boolean; end
class XmlContent; end
def self.parse(xml_content)
AnonymousMapper.new.parse(xml_content)
end
def self.included(base)
if base.superclass <= HappyMapper
base.instance_eval do
@attributes =
superclass.instance_variable_get(:@attributes).dup
@elements =
superclass.instance_variable_get(:@elements).dup
@registered_namespaces =
superclass.instance_variable_get(:@registered_namespaces).dup
@wrapper_anonymous_classes =
superclass.instance_variable_get(:@wrapper_anonymous_classes).dup
end
else
base.instance_eval do
@attributes = {}
@elements = {}
@registered_namespaces = {}
@wrapper_anonymous_classes = {}
end
end
base.extend ClassMethods
end
# Set all attributes with a default to their default values
def initialize
super
self.class.attributes.reject { |attr| attr.default.nil? }.each do |attr|
send(:"#{attr.method_name}=", attr.default)
end
end
#
# Create an xml representation of the specified class based on defined
# HappyMapper elements and attributes. The method is defined in a way
# that it can be called recursively by classes that are also HappyMapper
# classes, allowg for the composition of classes.
#
# @param [Nokogiri::XML::Builder] builder an instance of the XML builder which
# is being used when called recursively.
# @param [String] default_namespace The name of the namespace which is the
# default for the xml being produced; this is the namespace of the
# parent
# @param [String] namespace_override The namespace specified with the element
# declaration in the parent. Overrides the namespace declaration in the
# element class itself when calling #to_xml recursively.
# @param [String] tag_from_parent The xml tag to use on the element when being
# called recursively. This lets the parent doc define its own structure.
# Otherwise the element uses the tag it has defined for itself. Should only
# apply when calling a child HappyMapper element.
#
# @return [String,Nokogiri::XML::Builder] return XML representation of the
# HappyMapper object; when called recursively this is going to return
# and Nokogiri::XML::Builder object.
#
def to_xml(builder = nil, default_namespace = nil, namespace_override = nil,
tag_from_parent = nil)
#
# If to_xml has been called without a passed in builder instance that
# means we are going to return xml output. When it has been called with
# a builder instance that means we most likely being called recursively
# and will return the end product as a builder instance.
#
unless builder
write_out_to_xml = true
builder = Nokogiri::XML::Builder.new
end
attributes = collect_writable_attributes
#
# If the object we are serializing has a namespace declaration we will want
# to use that namespace or we will use the default namespace.
# When neither are specifed we are simply using whatever is default to the
# builder
#
namespace_name = namespace_override || self.class.namespace || default_namespace
#
# Create a tag in the builder that matches the class's tag name unless a tag was passed
# in a recursive call from the parent doc. Then append
# any attributes to the element that were defined above.
#
tag_name = tag_from_parent || self.class.tag_name
builder.send(:"#{tag_name}_", attributes) do |xml|
register_namespaces_with_builder(builder)
xml.parent.namespace =
builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_name }
#
# When a content has been defined we add the resulting value
# the output xml
#
if (content = self.class.defined_content) && !content.options[:read_only]
value = send(content.name)
value = apply_on_save_action(content, value)
builder.text(value)
end
#
# for every define element (i.e. has_one, has_many, element) we are
# going to persist each one
#
self.class.elements.each do |element|
element_to_xml(element, xml, default_namespace)
end
end
# Write out to XML, this value was set above, based on whether or not an XML
# builder object was passed to it as a parameter. When there was no parameter
# we assume we are at the root level of the #to_xml call and want the actual
# xml generated from the object. If an XML builder instance was specified
# then we assume that has been called recursively to generate a larger
# XML document.
write_out_to_xml ? builder.to_xml.force_encoding("UTF-8") : builder
end
# Parse the xml and update this instance. This does not update instances
# of HappyMappers that are children of this object. New instances will be
# created for any HappyMapper children of this object.
#
# Params and return are the same as the class parse() method above.
def parse(xml, options = {})
self.class.parse(xml, options.merge!(update: self))
end
# Factory for creating anonmyous HappyMappers
class AnonymousWrapperClassFactory
def self.get(name, &blk)
Class.new do
include HappyMapper
tag name
instance_eval(&blk)
end
end
end
private
#
# If the item defines an on_save lambda/proc or value that maps to a method
# that the class has defined, then call it with the value as a parameter.
# This allows for operations to be performed to convert the value to a
# specific value to be saved to the xml.
#
def apply_on_save_action(item, value)
if (on_save_action = item.options[:on_save])
if on_save_action.is_a?(Proc)
value = on_save_action.call(value)
elsif respond_to?(on_save_action)
value = send(on_save_action, value)
end
end
value
end
#
# Find the attributes for the class and collect them into a Hash structure
#
def collect_writable_attributes
#
# Find the attributes for the class and collect them into an array
# that will be placed into a Hash structure
#
attributes = self.class.attributes.filter_map do |attribute|
#
# If an attribute is marked as read_only then we want to ignore the attribute
# when it comes to saving the xml document; so we will not go into any of
# the below process
#
next if attribute.options[:read_only]
value = send(attribute.method_name)
value = nil if value == attribute.default
#
# Apply any on_save lambda/proc or value defined on the attribute.
#
value = apply_on_save_action(attribute, value)
#
# Attributes that have a nil value should be ignored unless they explicitly
# state that they should be expressed in the output.
#
next if value.nil? && !attribute.options[:state_when_nil]
attribute_namespace = attribute.options[:namespace]
["#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value]
end
attributes.to_h
end
#
# Add all the registered namespaces to the builder's root element.
# When this is called recursively by composed classes the namespaces
# are still added to the root element
#
# However, we do not want to add the namespace if the namespace is 'xmlns'
# which means that it is the default namespace of the code.
#
def register_namespaces_with_builder(builder)
return unless self.class.instance_variable_get(:@registered_namespaces)
self.class.instance_variable_get(:@registered_namespaces).sort.each do |name, href|
name = nil if name == "xmlns"
builder.doc.root.add_namespace(name, href)
end
end
# Persist a single nested element as xml
def element_to_xml(element, xml, default_namespace)
#
# If an element is marked as read only do not consider at all when
# saving to XML.
#
return if element.options[:read_only]
tag = element.tag || element.name
#
# The value to store is the result of the method call to the element,
# by default this is simply utilizing the attr_accessor defined. However,
# this allows for this method to be overridden
#
value = send(element.name)
#
# Apply any on_save action defined on the element.
#
value = apply_on_save_action(element, value)
#
# To allow for us to treat both groups of items and singular items
# equally we wrap the value and treat it as an array.
#
values = if value.respond_to?(:to_ary) && !element.options[:single]
value.to_ary
else
[value]
end
values.each do |item|
if item.is_a?(HappyMapper)
#
# Other items are convertable to xml through the xml builder
# process should have their contents retrieved and attached
# to the builder structure
#
item.to_xml(xml, self.class.namespace || default_namespace,
element.options[:namespace],
element.options[:tag] || nil)
elsif !item.nil? || element.options[:state_when_nil]
item_namespace =
element.options[:namespace] ||
self.class.namespace ||
default_namespace
#
# When a value exists or the tag should always be emitted,
# we should append the value for the tag
#
if item_namespace
xml[item_namespace].send(:"#{tag}_", item.to_s)
else
xml.send(:"#{tag}_", item.to_s)
end
end
end
end
def wrapper_anonymous_classes
self.class.instance_variable_get(:@wrapper_anonymous_classes)
end
end
require "happymapper/supported_types"
require "happymapper/item"
require "happymapper/attribute"
require "happymapper/element"
require "happymapper/text_node"