Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Feature: #to_xml

  Unhappymapper (or happyunmapper) which will take the HappyMapper mappings
  and write them back to XML
  • Loading branch information...
commit a25d080fa440adf8306baf811fcdd2c3de4a0a9a 1 parent 5c88279
@burtlo burtlo authored
View
166 lib/happymapper.rb
@@ -12,6 +12,8 @@ module HappyMapper
def self.included(base)
base.instance_variable_set("@attributes", {})
base.instance_variable_set("@elements", {})
+ base.instance_variable_set("@registered_namespaces", {})
+
base.extend ClassMethods
end
@@ -66,6 +68,10 @@ def namespace(namespace = nil)
@namespace = namespace if namespace
@namespace
end
+
+ def register_namespace(namespace, ns)
+ @registered_namespaces.merge!(namespace => ns)
+ end
def tag(new_tag_name)
@tag_name = new_tag_name.to_s
@@ -101,12 +107,12 @@ def parse(xml, options = {})
attributes.each do |attr|
obj.send("#{attr.method_name}=",
- attr.from_xml_node(n, namespace))
+ attr.from_xml_node(n, namespace))
end
elements.each do |elem|
obj.send("#{elem.method_name}=",
- elem.from_xml_node(n, namespace))
+ elem.from_xml_node(n, namespace))
end
obj.send("#{@content}=", n.content) if @content
@@ -125,7 +131,163 @@ def parse(xml, options = {})
collection
end
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.
+ #
+ def to_xml(node = nil, default_namespace = nil)
+
+
+ #
+ # If to_xml has been called without a Node (and namespace) that
+ # means we are going to return an xml document. When it has been called
+ # with a Node instance that means this method is being called recursively
+ # and will return the node with elements defined here attached.
+ #
+ unless node
+ write_out_to_xml = true
+ node = XML::Node.new(self.class.tag_name)
+ end
+
+ #
+ # Create a tag that uses the tag name of the class that has no contents
+ # but has the specified namespace or uses the default namespace
+ #
+ child_node = XML::Node.new(self.class.tag_name)
+
+ #
+ # For all the registered namespaces, add them to node
+ #
+ if self.class.instance_variable_get('@registered_namespaces')
+
+ root_node = node
+
+ while root_node.parent?
+ root_node = root_node.parent
+ end
+
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |prefix,href|
+ XML::Namespace.new(child_node,prefix,href)
+ XML::Namespace.new(root_node,prefix,href) unless root_node.namespaces.find_by_prefix(prefix)
+ end
+ end
+
+ #
+ # When there is a defined namespace or one passed to the #to_xml method
+ # then create and set that namespace as the default namespace for the node
+ #
+ tag_namespace = child_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
+
+ if tag_namespace
+ child_node.namespaces.namespace = tag_namespace
+ #XML::Namespace.new(child_node,tag_namespace,self.class.instance_variable_get('@registered_namespaces')[tag_namespace])
+ #child_node.namespaces.default_prefix = tag_namespace
+ end
+
+
+ #
+ # Add all the attribute tags to the child node with their namespace or the
+ # the default namespace.
+ #
+ self.class.attributes.each do |attribute|
+ attribute_namespace = child_node.namespaces.find_by_prefix(attribute.options[:namespace]) || default_namespace
+ # TODO: we need saving attribute functionality as well that is similar to elements
+ child_node[ "#{attribute_namespace ? "#{attribute_namespace.prefix}:" : ""}#{attribute.tag}" ] = send(attribute.method_name)
+ end
+
+ self.class.elements.each do |element|
+
+ tag = element.tag || element.name
+
+ value = send(element.name)
+
+ #
+ # 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 element.options[:on_save]
+ value = element.options[:on_save].call(value)
+ end
+
+ #
+ # 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[:state_when_nil]
+ item_namespace = child_node.namespaces.find_by_prefix(element.options[:namespace]) || child_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
+
+ child_node << XML::Node.new(tag,nil,item_namespace)
+ 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
+
+
+ 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(child_node,child_node.namespaces.find_by_prefix(element.options[:namespace]))
+
+ elsif item
+
+ item_namespace = child_node.namespaces.find_by_prefix(element.options[:namespace]) || child_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
+
+ #
+ # When a value exists we should append the value for the tag
+ #
+ child_node << XML::Node.new(tag,item.to_s,item_namespace)
+
+ else
+
+ item_namespace = child_node.namespaces.find_by_prefix(element.options[:namespace]) || child_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
+
+ #
+ # Normally a nil value would be ignored, however if specified then
+ # an empty element will be written to the xml
+ #
+ child_node << XML.Node.new(tag,nil,item_namespace) if element.options[:state_when_nil]
+
+ end
+
+ end
+
+ end
+
+
+
+ if write_out_to_xml
+ document = XML::Document.new
+ document.root = child_node
+ document.to_s
+ else
+ node << child_node
+ end
+
+
+ end
+
+
end
require File.dirname(__FILE__) + '/happymapper/item'
View
151 spec/happymapper_to_xml_namespaces_spec.rb
@@ -0,0 +1,151 @@
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+module ToXMLWithNamespaces
+
+ #
+ # Similar example as the to_xml but this time with namespacing
+ #
+ class Address
+ include HappyMapper
+
+ register_namespace 'address', 'http://www.company.com/address'
+ register_namespace 'country', 'http://www.company.com/country'
+
+ tag 'Address'
+ namespace 'address'
+
+ element :country, 'Country', :tag => 'country', :namespace => 'country'
+
+
+ attribute :location, String
+
+ element :street, String
+ element :postcode, String
+ element :city, String
+
+ element :housenumber, String
+
+ #
+ # to_xml will default to the attr_accessor method and not the attribute,
+ # allowing for that to be overwritten
+ #
+ def housenumber
+ "[#{@housenumber}]"
+ end
+
+ #
+ # Write a empty element even if this is not specified
+ #
+ element :description, String, :state_when_nil => true
+
+ #
+ # Perform the on_save operation when saving
+ #
+ has_one :date_created, Time, :on_save => lambda {|time| DateTime.parse(time).strftime("%T %D") if time }
+
+ #
+ # Write multiple elements and call on_save when saving
+ #
+ has_many :dates_updated, Time, :on_save => lambda {|times|
+ times.compact.map {|time| DateTime.parse(time).strftime("%T %D") } if times }
+
+ #
+ # Class composition
+ #
+
+ def initialize(parameters)
+ parameters.each_pair do |property,value|
+ send("#{property}=",value) if respond_to?("#{property}=")
+ end
+ end
+
+ end
+
+ #
+ # Country is composed above the in Address class. Here is a demonstration
+ # of how to_xml will handle class composition as well as utilizing the tag
+ # value.
+ #
+ class Country
+ include HappyMapper
+
+ register_namespace 'countryName', 'http://www.company.com/countryName'
+
+ attribute :code, String, :tag => 'countryCode'
+ has_one :name, String, :tag => 'countryName', :namespace => 'countryName'
+
+ def initialize(parameters)
+ parameters.each_pair do |property,value|
+ send("#{property}=",value) if respond_to?("#{property}=")
+ end
+ end
+
+ end
+
+ describe "#to_xml" do
+
+ context "Address" do
+
+ before(:all) do
+ address = Address.new('street' => 'Mockingbird Lane',
+ 'location' => 'Home',
+ 'housenumber' => '1313',
+ 'postcode' => '98103',
+ 'city' => 'Seattle',
+ 'country' => Country.new(:name => 'USA', :code => 'us'),
+ 'date_created' => '2011-01-01 15:00:00')
+
+
+ address.dates_updated = ["2011-01-01 16:01:00","2011-01-02 11:30:01"]
+ puts address.to_xml
+
+ @address_xml = XML::Parser.string(address.to_xml).parse.root
+ end
+
+ # { 'street' => 'Mockingbird Lane',
+ # 'postcode' => '98103',
+ # 'city' => 'Seattle' }.each_pair do |property,value|
+ #
+ # it "should have the element '#{property}' with the value '#{value}'" do
+ # @address_xml.find("address:#{property}").first.child.to_s.should == value
+ # end
+ #
+ # end
+ #
+ # it "should use the result of #housenumber method (not the @housenumber)" do
+ # @address_xml.find("address:housenumber").first.child.to_s.should == "[1313]"
+ # end
+ #
+ # it "should have the attribute 'location' with the value 'Home'" do
+ # @address_xml.find('@location').first.child.to_s.should == "Home"
+ # end
+ #
+ # it "should add an empty description element" do
+ # @address_xml.find('address:description').first.child.to_s.should == ""
+ # end
+ #
+ # it "should call #on_save when saving the time to convert the time" do
+ # @address_xml.find('address:date_created').first.child.to_s.should == "15:00:00 01/01/11"
+ # end
+ #
+ # it "should handle multiple elements for 'has_many'" do
+ # dates_updated = @address_xml.find('address:dates_updated')
+ # dates_updated.length.should == 2
+ # dates_updated.first.child.to_s.should == "16:01:00 01/01/11"
+ # dates_updated.last.child.to_s.should == "11:30:01 01/02/11"
+ # end
+ #
+ # it "should write the country code" do
+ # @address_xml.find('country:country/@country:countryCode').first.child.to_s.should == "us"
+ # end
+
+ it "should write the country name" do
+ @address_xml.find('country:country/countryName:countryName').first.child.to_s.should == "USA"
+ end
+
+ end
+
+
+ end
+
+end
View
139 spec/happymapper_to_xml_spec.rb
@@ -0,0 +1,139 @@
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+module ToXML
+
+ class Address
+ include HappyMapper
+
+ tag 'address'
+
+ attribute :location, String
+
+ element :street, String
+ element :postcode, String
+ element :city, String
+
+ element :housenumber, String
+
+ #
+ # to_xml will default to the attr_accessor method and not the attribute,
+ # allowing for that to be overwritten
+ #
+ def housenumber
+ "[#{@housenumber}]"
+ end
+
+ #
+ # Write a empty element even if this is not specified
+ #
+ element :description, String, :state_when_nil => true
+
+ #
+ # Perform the on_save operation when saving
+ #
+ has_one :date_created, Time, :on_save => lambda {|time| DateTime.parse(time).strftime("%T %D") if time }
+
+ #
+ # Write multiple elements and call on_save when saving
+ #
+ has_many :dates_updated, Time, :on_save => lambda {|times|
+ times.compact.map {|time| DateTime.parse(time).strftime("%T %D") } if times }
+
+ #
+ # Class composition
+ #
+ element :country, 'Country', :tag => 'country'
+
+ def initialize(parameters)
+ parameters.each_pair do |property,value|
+ send("#{property}=",value) if respond_to?("#{property}=")
+ end
+ end
+
+ end
+
+ #
+ # Country is composed above the in Address class. Here is a demonstration
+ # of how to_xml will handle class composition as well as utilizing the tag
+ # value.
+ #
+ class Country
+ include HappyMapper
+
+ attribute :code, String, :tag => 'countryCode'
+ has_one :name, String, :tag => 'countryName'
+
+ def initialize(parameters)
+ parameters.each_pair do |property,value|
+ send("#{property}=",value) if respond_to?("#{property}=")
+ end
+ end
+
+ end
+
+ describe "#to_xml" do
+
+ context "Address" do
+
+ before(:all) do
+ address = Address.new('street' => 'Mockingbird Lane',
+ 'location' => 'Home',
+ 'housenumber' => '1313',
+ 'postcode' => '98103',
+ 'city' => 'Seattle',
+ 'country' => Country.new(:name => 'USA', :code => 'us'),
+ 'date_created' => '2011-01-01 15:00:00')
+
+ address.dates_updated = ["2011-01-01 16:01:00","2011-01-02 11:30:01"]
+
+ puts address.to_xml
+ @address_xml = XML::Parser.string(address.to_xml).parse.root
+ end
+
+ { 'street' => 'Mockingbird Lane',
+ 'postcode' => '98103',
+ 'city' => 'Seattle' }.each_pair do |property,value|
+
+ it "should have the element '#{property}' with the value '#{value}'" do
+ @address_xml.find("#{property}").first.child.to_s.should == value
+ end
+
+ end
+
+ it "should use the result of #housenumber method (not the @housenumber)" do
+ @address_xml.find("housenumber").first.child.to_s.should == "[1313]"
+ end
+
+ it "should have the attribute 'location' with the value 'Home'" do
+ @address_xml.find('@location').first.child.to_s.should == "Home"
+ end
+
+ it "should add an empty description element" do
+ @address_xml.find('description').first.child.to_s.should == ""
+ end
+
+ it "should call #on_save when saving the time to convert the time" do
+ @address_xml.find('date_created').first.child.to_s.should == "15:00:00 01/01/11"
+ end
+
+ it "should handle multiple elements for 'has_many'" do
+ dates_updated = @address_xml.find('dates_updated')
+ dates_updated.length.should == 2
+ dates_updated.first.child.to_s.should == "16:01:00 01/01/11"
+ dates_updated.last.child.to_s.should == "11:30:01 01/02/11"
+ end
+
+ it "should write the country code" do
+ @address_xml.find('country/@countryCode').first.child.to_s.should == "us"
+ end
+
+ it "should write the country name" do
+ @address_xml.find('country/countryName').first.child.to_s.should == "USA"
+ end
+
+ end
+
+
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.