lib/happymapper.rb in nokogiri-happymapper-0.9.0 vs lib/happymapper.rb in nokogiri-happymapper-0.10.0
- old
+ new
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-require 'nokogiri'
-require 'date'
-require 'time'
-require 'happymapper/version'
-require 'happymapper/anonymous_mapper'
+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
@@ -37,464 +38,15 @@
end
base.extend ClassMethods
end
- module ClassMethods
- #
- # The xml has the following attributes defined.
- #
- # @example
- #
- # "<country code='de'>Germany</country>"
- #
- # # definition of the 'code' attribute within the class
- # attribute :code, String
- #
- # @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 = {})
- attribute = Attribute.new(name, type, options)
- @attributes[name] = attribute
- attr_accessor attribute.method_name.intern
- end
-
- #
- # The elements defined through {#attribute}.
- #
- # @return [Array<Attribute>] a list of the attributes defined for this class;
- # an empty array is returned when there have been no attributes defined.
- #
- def attributes
- @attributes.values
- end
-
- #
- # Register a namespace that is used to persist the object namespace back to
- # XML.
- #
- # @example
- #
- # register_namespace 'prefix', 'http://www.unicornland.com/prefix'
- #
- # # the output will contain the namespace defined
- #
- # "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
- # ...
- # </outputXML>"
- #
- # @param [String] name the xml prefix
- # @param [String] href url for the xml namespace
- #
- def register_namespace(name, href)
- @registered_namespaces.merge!(name => href)
- end
-
- #
- # An element defined in the XML that is parsed.
- #
- # @example
- #
- # "<address location='home'>
- # <city>Oldenburg</city>
- # </address>"
- #
- # # definition of the 'city' element within the class
- #
- # element :city, String
- #
- # @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 = {})
- element = Element.new(name, type, options)
- attr_accessor element.method_name.intern unless @elements[name]
- @elements[name] = element
- end
-
- #
- # The elements defined through {#element}, {#has_one}, and {#has_many}.
- #
- # @return [Array<Element>] a list of the elements contained defined for this
- # class; an empty array is returned when there have been no elements
- # defined.
- #
- def elements
- @elements.values
- end
-
- #
- # The value stored in the text node of the current element.
- #
- # @example
- #
- # "<firstName>Michael Jackson</firstName>"
- #
- # # definition of the 'firstName' text node within the class
- #
- # content :first_name, String
- #
- # @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 = {})
- @content = TextNode.new(name, type, options)
- attr_accessor @content.method_name.intern
- end
-
- #
- # Sets the object to have xml content, this will assign the XML contents
- # that are parsed to the attribute accessor xml_content. The object will
- # respond to the method #xml_content and will return the XML data that
- # it has parsed.
- #
- def has_xml_content
- attr_accessor :xml_content
- end
-
- #
- # The object has one of these elements in the XML. If there are multiple,
- # the last one will be set to this value.
- #
- # @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
- #
- # @see #element
- #
- def has_one(name, type, options = {})
- element name, type, { single: true }.merge(options)
- end
-
- #
- # The object has many of these elements in the XML.
- #
- # @param [Symbol] name the name of accessor that is created
- # @param [String,Class] type the class name or the name of the class which
- # 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)
- end
-
- #
- # The list of registered after_parse callbacks.
- #
- def after_parse_callbacks
- @after_parse_callbacks ||= []
- end
-
- #
- # Register a new after_parse callback, given as a block.
- #
- # @yield [object] Yields the newly-parsed object to the block after parsing.
- # Sub-objects will be already populated.
- def after_parse(&block)
- after_parse_callbacks.push(block)
- end
-
- #
- # Specify a namespace if a node and all its children are all namespaced
- # elements. This is simpler than passing the :namespace option to each
- # defined element.
- #
- # @param [String] namespace the namespace to set as default for the class
- # element.
- #
- def namespace(namespace = nil)
- @namespace = namespace if namespace
- @namespace if defined? @namespace
- end
-
- #
- # @param [String] new_tag_name the name for the tag
- #
- def tag(new_tag_name)
- @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
- end
-
- #
- # The name of the tag
- #
- # @return [String] the name of the tag as a string, downcased
- #
- def tag_name
- @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
- #
- # @param [String] name the name of the element that is just a place holder
- # @param [Proc] blk the element definitions inside the place holder tag
- #
- 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_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|
- method_name = item.method_name
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method_name} # def property
- @#{name} ||= # @wrapper ||=
- wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
- @#{name}.#{method_name} # @wrapper.property
- end # end
-
- def #{method_name}=(value) # def property=(value)
- @#{name} ||= # @wrapper ||=
- wrapper_anonymous_classes['#{wrapper_key}'].new # wrapper_anonymous_classes['#<Class:0x0000555b7d0b9220>'].new
- @#{name}.#{method_name} = value # @wrapper.property = value
- end # end
- RUBY
- end
-
- has_one name, wrapper
- end
-
- # The callback defined through {.with_nokogiri_config}.
- #
- # @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().
- #
- # 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)
- @nokogiri_config_callback = blk
- end
-
- #
- # @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
- # contents to convert into Object.
- # @param [Hash] options additional information for parsing.
- # :single => true 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 = {})
- # 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
-
- 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
- )
- 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 the :single option has been specified or we are at the root element
- # then we are going to return a single element or nil if no nodes are found
- single = root || options[:single]
-
- # if a namespace has been provided then set the current namespace to it
- # or use the namespace provided by the class
- # or use the 'xmlns' namespace if defined
-
- namespace = options[:namespace] || self.namespace || namespaces.key?('xmlns') && 'xmlns'
-
- # 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
- find_nodes_to_parse(options, namespace, tag_name, namespaces, node, root)
- end
-
- # Nothing matching found, we can go ahead and return
- return (single ? nil : []) if nodes.empty?
-
- # 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.
-
- limit = options[:in_groups_of] || nodes.size
-
- # If the limit of 0 has been specified then the user obviously wants
- # none of the nodes that we are serving within this batch of nodes.
-
- return [] if limit == 0
-
- collection = []
-
- nodes.each_slice(limit) do |slice|
- part = slice.map do |n|
- parse_node(n, options, namespace, namespaces)
- 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
-
- if block_given? && options[:in_groups_of]
- yield part
- else
- collection += part
- end
- end
-
- # If we're parsing a single 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 single
- collection.first
- else
- collection
- end
- end
-
- # @private
- def defined_content
- @content if defined? @content
- end
-
- private
-
- 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
- #
-
- xpath = if options[:xpath]
- options[:xpath].to_s.sub(%r{([^/])$}, '\1/')
- elsif root
- '/'
- else
- './/'
- end
- if namespace
- return [] unless namespaces.find { |name, _url| ["xmlns:#{namespace}", namespace].include? name }
-
- xpath += "#{namespace}:"
- end
-
- 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)
- nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
- 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|
- nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
- break if nodes && !nodes.empty?
- end
-
- 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
-
- elements.each do |elem|
- obj.send("#{elem.method_name}=", elem.from_xml_node(node, namespace, namespaces))
- end
-
- if (content = defined_content)
- obj.send("#{content.method_name}=", content.from_xml_node(node, namespace, namespaces))
- 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
-
- 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|
- send("#{attr.method_name}=", attr.default)
+ send(:"#{attr.method_name}=", attr.default)
end
end
#
# Create an xml representation of the specified class based on defined
@@ -547,11 +99,13 @@
#
# 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|
+
+ 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 }
@@ -579,11 +133,11 @@
# 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
+ 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.
@@ -629,11 +183,11 @@
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|
+ 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
#
@@ -652,12 +206,12 @@
# 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.compact
+ ["#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value]
+ end
attributes.to_h
end
#
@@ -670,11 +224,11 @@
#
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'
+ name = nil if name == "xmlns"
builder.doc.root.add_namespace(name, href)
end
end
# Persist a single nested element as xml
@@ -721,30 +275,33 @@
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
+ 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)
+ xml[item_namespace].send(:"#{tag}_", item.to_s)
else
- xml.send("#{tag}_", item.to_s)
+ 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'
+require "happymapper/supported_types"
+require "happymapper/item"
+require "happymapper/attribute"
+require "happymapper/element"
+require "happymapper/text_node"