lib/happymapper.rb in nokogiri-happymapper-0.6.0 vs lib/happymapper.rb in nokogiri-happymapper-0.7.0
- old
+ new
@@ -1,44 +1,44 @@
+# frozen_string_literal: true
+
require 'nokogiri'
require 'date'
require 'time'
+require 'happymapper/version'
require 'happymapper/anonymous_mapper'
module HappyMapper
class Boolean; end
class XmlContent; end
extend AnonymousMapper
- DEFAULT_NS = "happymapper"
-
def self.included(base)
- if !(base.superclass <= HappyMapper)
+ if base.superclass <= HappyMapper
base.instance_eval do
- @attributes = {}
- @elements = {}
- @registered_namespaces = {}
- @wrapper_anonymous_classes = {}
- end
- else
- base.instance_eval do
@attributes =
- superclass.instance_variable_get(:@attributes).dup
+ superclass.instance_variable_get(:@attributes).dup
@elements =
- superclass.instance_variable_get(:@elements).dup
+ superclass.instance_variable_get(:@elements).dup
@registered_namespaces =
- superclass.instance_variable_get(:@registered_namespaces).dup
+ superclass.instance_variable_get(:@registered_namespaces).dup
@wrapper_anonymous_classes =
- superclass.instance_variable_get(:@wrapper_anonymous_classes).dup
+ 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
module ClassMethods
-
#
# The xml has the following attributes defined.
#
# @example
#
@@ -50,11 +50,11 @@
# @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
#
- def attribute(name, type, options={})
+ def attribute(name, type, options = {})
attribute = Attribute.new(name, type, options)
@attributes[name] = attribute
attr_accessor attribute.method_name.intern
end
@@ -80,15 +80,15 @@
#
# "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
# ...
# </outputXML>"
#
- # @param [String] namespace the xml prefix
- # @param [String] ns url for the xml namespace
+ # @param [String] name the xml prefix
+ # @param [String] href url for the xml namespace
#
- def register_namespace(namespace, ns)
- @registered_namespaces.merge!({namespace => ns})
+ def register_namespace(name, href)
+ @registered_namespaces.merge!(name => href)
end
#
# An element defined in the XML that is parsed.
#
@@ -105,11 +105,11 @@
# @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
#
- def element(name, type, options={})
+ def element(name, type, options = {})
element = Element.new(name, type, options)
attr_accessor element.method_name.intern unless @elements[name]
@elements[name] = element
end
@@ -138,11 +138,11 @@
# @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
#
- def content(name, type=String, options={})
+ def content(name, type = String, options = {})
@content = TextNode.new(name, type, options)
attr_accessor @content.method_name.intern
end
#
@@ -164,12 +164,12 @@
# the object will be converted upon parsing
# @param [Hash] options additional parameters to send to the relationship
#
# @see #element
#
- def has_one(name, type, options={})
- element name, type, {:single => true}.merge(options)
+ def has_one(name, type, options = {})
+ element name, type, { single: true }.merge(options)
end
#
# The object has many of these elements in the XML.
#
@@ -178,12 +178,12 @@
# the object will be converted upon parsing.
# @param [Hash] options additional parameters to send to the relationship
#
# @see #element
#
- def has_many(name, type, options={})
- element name, type, {:single => false}.merge(options)
+ def has_many(name, type, options = {})
+ element name, type, { single: false }.merge(options)
end
#
# The list of registered after_parse callbacks.
#
@@ -224,11 +224,11 @@
# The name of the tag
#
# @return [String] the name of the tag as a string, downcased
#
def tag_name
- @tag_name ||= to_s.split('::')[-1].downcase
+ @tag_name ||= name && name.to_s.split('::')[-1].downcase
end
# 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
@@ -238,26 +238,27 @@
#
def wrap(name, &blk)
# Get an anonymous HappyMapper 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
+ wrapper_key = wrapper.inspect
+ @wrapper_anonymous_classes[wrapper_key] = wrapper
# Create getter/setter for each element and attribute defined on the anonymous HappyMapper
# 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 %{
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{item.method_name}
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
+ @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
@#{name}.#{item.method_name}
end
def #{item.method_name}=(value)
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
+ @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper_key}'].new
@#{name}.#{item.method_name} = value
end
- }
+ RUBY
end
has_one name, wrapper
end
@@ -265,11 +266,13 @@
#
# @return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
#
attr_reader :nokogiri_config_callback
- # Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse().
+ # Register a config callback according to the block Nokogori expects when
+ # calling Nokogiri::XML::Document.parse().
+ #
# See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
#
# @param [Proc] the proc to pass to Nokogiri to setup parse options
#
def with_nokogiri_config(&blk)
@@ -283,116 +286,64 @@
# 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.
#
def parse(xml, options = {})
-
# create a local copy of the objects namespace value for this parse execution
namespace = (@namespace if defined? @namespace)
+ # Capture any provided namespaces and merge in any namespaces that have
+ # been registered on the object.
+ namespaces = options[:namespaces] || {}
+ namespaces = namespaces.merge(@registered_namespaces)
+
# 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
-
+ unless xml.is_a?(Nokogiri::XML::Document)
# 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
+ # Now xml is certainly an XML document: Select the root node of the document
+ node = xml.root
+ # merge any namespaces found on the xml node into the namespace hash
+ namespaces = namespaces.merge(xml.collect_namespaces)
+
# 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 'happymapper'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
+ elsif namespaces.has_key?('xmlns')
+ namespace ||= 'xmlns'
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
+ find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
end
# Nothing matching found, we can go ahead and return
- return ( ( options[:single] || root ) ? nil : [] ) if nodes.size == 0
+ 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.
@@ -404,90 +355,134 @@
return [] if limit == 0
collection = []
nodes.each_slice(limit) do |slice|
-
part = slice.map do |n|
+ parse_node(n, options, namespace, namespaces)
+ end
- # If an existing HappyMapper object is provided, update it with the
- # values from the xml being parsed. Otherwise, create a new object
+ # 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
- obj = options[:update] ? options[:update] : new
+ if block_given? && options[:in_groups_of]
+ yield part
+ else
+ collection += part
+ end
+ end
- 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
+ # 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.
- elements.each do |elem|
- obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
- end
+ if options[:single] || root
+ collection.first
+ else
+ collection
+ end
+ end
- if (defined? @content) && @content
- obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces))
- end
+ # @private
+ def defined_content
+ @content if defined? @content
+ end
- # If the HappyMapper 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
+ private
- if obj.respond_to?('xml_value=')
- n.namespaces.each {|name,path| n[name] = path }
- obj.xml_value = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
- end
+ def find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
+ # 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
+ #
- # If the HappyMapper 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
+ xpath = (root ? '/' : './/')
+ xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
+ if namespace
+ return [] unless namespaces.find { |name, _url| ["xmlns:#{namespace}", namespace].include? name }
+ xpath += "#{namespace}:"
+ end
- 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)
- end
+ nodes = []
- # Call any registered after_parse callbacks for the object's class
+ # 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)
- obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
+ # If a tag has been provided we need to search for it.
- # collect the object that we have created
+ if options.key?(:tag)
+ nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
+ else
- obj
+ # 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|
+ nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
+ break if nodes && !nodes.empty?
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
+ end
- if block_given? and options[:in_groups_of]
- yield part
- else
- collection += part
- end
+ nodes
+ end
+ def parse_node(node, options, namespace, namespaces)
+ # If an existing HappyMapper object is provided, update it with the
+ # values from the xml being parsed. Otherwise, create a new object
+
+ obj = options[:update] || new
+
+ attributes.each do |attr|
+ value = attr.from_xml_node(node, namespace, namespaces)
+ value = attr.default if value.nil?
+ obj.send("#{attr.method_name}=", value)
end
- # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
- nodes = nil
+ elements.each do |elem|
+ obj.send("#{elem.method_name}=", elem.from_xml_node(node, namespace, namespaces))
+ end
- # 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 (content = defined_content)
+ obj.send("#{content.method_name}=", content.from_xml_node(node, namespace, namespaces))
+ end
- if options[:single] or root
- collection.first
- else
- collection
+ # If the HappyMapper 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=')
+ obj.xml_value = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
end
+
+ # If the HappyMapper 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=')
+ node = node.children if node.respond_to?(:children)
+ obj.xml_content = node.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
+ 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
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|
+ self.class.attributes.reject { |attr| attr.default.nil? }.each do |attr|
send("#{attr.method_name}=", attr.default)
end
end
#
@@ -517,257 +512,237 @@
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
+
#
- # Find the attributes for the class and collect them into an array
- # that will be placed into a Hash structure
+ # 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
#
- attributes = self.class.attributes.collect do |attribute|
+ namespace_name = namespace_override || self.class.namespace || default_namespace
- #
- # 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 wiill not go into any of
- # the below process
- #
- unless attribute.options[:read_only]
-
- value = send(attribute.method_name)
- value = nil if value == attribute.default
-
- #
- # If the attribute 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.
- #
- if on_save_action = attribute.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
-
- #
- # Attributes that have a nil value should be ignored unless they explicitly
- # state that they should be expressed in the output.
- #
- if not value.nil? || attribute.options[:state_when_nil]
- attribute_namespace = attribute.options[:namespace]
- [ "#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value ]
- else
- []
- end
-
- else
- []
- end
-
- end.flatten
-
- attributes = Hash[ *attributes ]
-
#
# 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.
#
- builder.send("#{tag_from_parent || self.class.tag_name}_",attributes) do |xml|
+ builder.send("#{tag_from_parent || self.class.tag_name}_", attributes) do |xml|
+ register_namespaces_with_builder(builder)
- #
- # Add all the registered namespaces to the root element.
- # When this is called recurisvely 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 namesapce of the code.
- #
- if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
- self.class.instance_variable_get('@registered_namespaces').each_pair do |name,href|
- name = nil if name == "xmlns"
- builder.doc.root.add_namespace(name,href)
- end
- end
-
- #
- # 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_for_parent = namespace_override
- if self.class.respond_to?(:namespace) && self.class.namespace
- namespace_for_parent ||= self.class.namespace
- end
- namespace_for_parent ||= default_namespace
-
xml.parent.namespace =
- builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_for_parent }
+ 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 self.class.instance_variable_defined?('@content')
- if content = self.class.instance_variable_get('@content')
+ if (content = self.class.defined_content)
- unless content.options[:read_only]
- text_accessor = content.tag || content.name
- value = send(text_accessor)
+ unless content.options[:read_only]
+ value = send(content.name)
+ value = apply_on_save_action(content, value)
- if on_save_action = content.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
-
- builder.text(value)
- end
-
+ builder.text(value)
end
+
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
- #
- # If an element is marked as read only do not consider at all when
- # saving to XML.
- #
- unless element.options[:read_only]
+ # 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
- tag = element.tag || element.name
+ # 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
- #
- # 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)
+ # 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
- #
- # If the element defines an on_save lambda/proc then we will call that
- # operation on the specified value. This allows for operations to be
- # performed to convert the value to a specific value to be saved to the xml.
- #
- if on_save_action = element.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
+ private
- #
- # Normally a nil value would be ignored, however if specified then
- # an empty element will be written to the xml
- #
- if value.nil? && element.options[:single] && element.options[:state_when_nil]
- xml.send("#{tag}_","")
- end
+ #
+ # 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
- #
- # To allow for us to treat both groups of items and singular items
- # equally we wrap the value and treat it as an array.
- #
- if value.nil?
- values = []
- elsif value.respond_to?(:to_ary) && !element.options[:single]
- values = value.to_ary
- else
- values = [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.collect 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
+ #
+ if attribute.options[:read_only]
+ []
+ else
- values.each do |item|
+ value = send(attribute.method_name)
+ value = nil if value == attribute.default
- if item.is_a?(HappyMapper)
+ #
+ # Apply any on_save lambda/proc or value defined on the attribute.
+ #
+ value = apply_on_save_action(attribute, value)
- #
- # 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)
+ #
+ # Attributes that have a nil value should be ignored unless they explicitly
+ # state that they should be expressed in the output.
+ #
+ if not value.nil? || attribute.options[:state_when_nil]
+ attribute_namespace = attribute.options[:namespace]
+ ["#{attribute_namespace ? "#{attribute_namespace}:" : ''}#{attribute.tag}", value]
+ else
+ []
+ end
- elsif !item.nil?
+ end
+ end.flatten
- item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
+ Hash[*attributes]
+ end
- #
- # When a value exists 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
+ #
+ # 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
- else
+ # 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]
- #
- # Normally a nil value would be ignored, however if specified then
- # an empty element will be written to the xml
- #
- xml.send("#{tag}_","") if element.options[:state_when_nil]
+ tag = element.tag || element.name
- end
+ #
+ # 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)
- end
+ #
+ # Apply any on_save action defined on the element.
+ #
+ value = apply_on_save_action(element, value)
- end
- end
+ #
+ # 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
- end
+ values.each do |item|
+ if item.is_a?(HappyMapper)
- # 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 : builder
+ #
+ # 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)
- end
+ elsif !item.nil?
- # 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
+ item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
- private
+ #
+ # When a value exists 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
- # 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
+ elsif element.options[:state_when_nil]
+ #
+ # Normally a nil value would be ignored, however if specified then
+ # an empty element will be written to the xml
+ #
+ xml.send("#{tag}_", '')
+ end
+ end
+ end
end
require 'happymapper/supported_types'
require 'happymapper/item'
require 'happymapper/attribute'