module XmlMapper::ClassMethods
Public Instance Methods
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.
# File lib/xmlmapper.rb, line 199 def after_parse(&block) after_parse_callbacks.push(block) end
The list of registered after_parse
callbacks.
# File lib/xmlmapper.rb, line 190 def after_parse_callbacks @after_parse_callbacks ||= [] end
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
# File lib/xmlmapper.rb, line 55 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.
# File lib/xmlmapper.rb, line 67 def attributes @attributes.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
# File lib/xmlmapper.rb, line 143 def content(name, type=String, options={}) @content = TextNode.new(name, type, options) attr_accessor @content.method_name.intern 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
# File lib/xmlmapper.rb, line 110 def element(name, type, options={}) element = Element.new(name, type, options) @elements[name] = element attr_accessor element.method_name.intern 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.
# File lib/xmlmapper.rb, line 123 def elements @elements.values 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
# File lib/xmlmapper.rb, line 183 def has_many(name, type, options={}) element name, type, {:single => false}.merge(options) 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
# File lib/xmlmapper.rb, line 169 def has_one(name, type, options={}) element name, type, {:single => true}.merge(options) 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.
# File lib/xmlmapper.rb, line 154 def has_xml_content attr_accessor :xml_content 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.
# File lib/xmlmapper.rb, line 211 def namespace(namespace = nil) @namespace = namespace if namespace @namespace end
The callback defined through {.with_nokogiri_config}.
@return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
# File lib/xmlmapper.rb, line 268 def nokogiri_config_callback @nokogiri_config_callback 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.
# File lib/xmlmapper.rb, line 289 def parse(xml, options = {}) # create a local copy of the objects namespace value for this parse execution namespace = @namespace # 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 # 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 # 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 'xmlmapper'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 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 end # Nothing matching found, we can go ahead and return 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. 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| # If an existing XmlMapper object is provided, update it with the # values from the xml being parsed. Otherwise, create a new object obj = options[:update] ? options[:update] : new 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 elements.each do |elem| obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces)) end if @content obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces)) end # If the XmlMapper 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=') # n.namespaces.each {|name,path| n[name] = path } obj.xml_value = n.canonicalize(Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0, n.document.collect_namespaces.keys.map { |name| name.split(":").last }) # obj.xml_value = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION) end if obj.respond_to?('xml_node=') obj.xml_node = n end # If the XmlMapper 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=') n = n.children if n.respond_to?(:children) obj.xml_content = n.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION) 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 # 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? and options[:in_groups_of] yield part else collection += part end end # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354 nodes = nil # 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 options[:single] or root collection.first else collection end 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] namespace the xml prefix @param [String] ns url for the xml namespace
# File lib/xmlmapper.rb, line 88 def register_namespace(namespace, ns) @registered_namespaces.merge!({namespace => ns}) end
@param [String] new_tag_name the name for the tag
# File lib/xmlmapper.rb, line 219 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
# File lib/xmlmapper.rb, line 228 def tag_name @tag_name ||= to_s.split('::')[-1].downcase end
Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse(). See nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
@param [Proc] the proc to pass to Nokogiri to setup parse options
# File lib/xmlmapper.rb, line 277 def with_nokogiri_config(&blk) @nokogiri_config_callback = blk 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
# File lib/xmlmapper.rb, line 239 def wrap(name, &blk) # Get an anonymous XmlMapper 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 # Create getter/setter for each element and attribute defined on the anonymous XmlMapper # 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 %{ def #{item.method_name} @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new @#{name}.#{item.method_name} end def #{item.method_name}=(value) @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new @#{name}.#{item.method_name} = value end } end has_one name, wrapper end