Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'unhappymapper' of https://github.com/burtlo/happymapper

…into burtlo-unhappymapper
  • Loading branch information...
commit a7a7e135bc568aeb5e3e13d6d43269b157b0f336 2 parents 259c9fa + 47b9565
John Nunemaker authored
186 lib/happymapper.rb
View
@@ -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,183 @@ 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(parent_node = nil, default_namespace = nil)
+
+ #
+ # 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
+ #
+ current_node = XML::Node.new(self.class.tag_name)
+
+
+ if parent_node
+ #
+ # if #to_xml has been called with a parent_node that means this method
+ # is being called recursively (or a special case) and we want to return
+ # the parent_node with the new node as a child
+ #
+ parent_node << current_node
+ else
+ #
+ # If #to_xml has been called without a Node (and namespace) that
+ # means we want to return an xml document
+ #
+ write_out_to_xml = true
+ end
+
+ #
+ # Add all the registered namespaces to the current node and the current node's
+ # root element. Without adding it to the root element it is not possible to
+ # parse or use xpath to find elements.
+ #
+ if self.class.instance_variable_get('@registered_namespaces')
+
+ # Given a node, continue moving up to parents until there are no more parents
+ find_root_node = lambda {|node| while node.parent? ; node = node.parent ; end ; node }
+ root_node = find_root_node.call(current_node)
+
+ # Add the registered namespace to the found root node only if it does not already have one defined
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |prefix,href|
+ XML::Namespace.new(current_node,prefix,href)
+ XML::Namespace.new(root_node,prefix,href) unless root_node.namespaces.find_by_prefix(prefix)
+ end
+ end
+
+ #
+ # Determine the tag namespace if one has been specified. This value takes
+ # precendence over one that is handed down to composed sub-classes.
+ #
+ tag_namespace = current_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
+
+ # Set the namespace of the current node to the specified namespace
+ current_node.namespaces.namespace = tag_namespace if tag_namespace
+
+ #
+ # Add all the attribute tags to the current node with their namespace, if one
+ # is defined, or the namespace handed down to the node.
+ #
+ self.class.attributes.each do |attribute|
+ attribute_namespace = current_node.namespaces.find_by_prefix(attribute.options[:namespace]) || default_namespace
+
+ value = send(attribute.method_name)
+
+ #
+ # If the attribute has a :on_save attribute defined that is a proc or
+ # a defined method, then call those with the current value.
+ #
+ if on_save_operation = attribute.options[:on_save]
+ if on_save_operation.is_a?(Proc)
+ value = on_save_operation.call(value)
+ elsif respond_to?(on_save_operation)
+ value = send(on_save_operation,value)
+ end
+ end
+
+ current_node[ "#{attribute_namespace ? "#{attribute_namespace.prefix}:" : ""}#{attribute.tag}" ] = value
+ end
+
+ #
+ # All all the elements defined (e.g. has_one, has_many, element) ...
+ #
+ self.class.elements.each do |element|
+
+ tag = element.tag || element.name
+
+ element_namespace = current_node.namespaces.find_by_prefix(element.options[:namespace]) || tag_namespace
+
+ 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 on_save_operation = element.options[:on_save]
+ if on_save_operation.is_a?(Proc)
+ value = on_save_operation.call(value)
+ elsif respond_to?(on_save_operation)
+ value = send(on_save_operation,value)
+ end
+ 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]
+ current_node << XML::Node.new(tag,nil,element_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 HappyMapper items that are convertable should not be called
+ # with the current node and the namespace defined for the element.
+ #
+ item.to_xml(current_node,element_namespace)
+
+ elsif item
+
+ #
+ # When a value exists we should append the value for the tag
+ #
+ current_node << XML::Node.new(tag,item.to_s,element_namespace)
+
+ else
+
+ #
+ # Normally a nil value would be ignored, however if specified then
+ # an empty element will be written to the xml
+ #
+ current_node << XML.Node.new(tag,nil,element_namespace) if element.options[:state_when_nil]
+
+ end
+
+ end
+
+ end
+
+
+ #
+ # Generate xml from a document if no node was passed as a parameter. Otherwise
+ # this method is being called recursively (or special case) and we should
+ # return the node with this node attached as a child.
+ #
+ if write_out_to_xml
+ document = XML::Document.new
+ document.root = current_node
+ document.to_s
+ else
+ parent_node
+ end
+
end
+
+
end
require File.dirname(__FILE__) + '/happymapper/item'
149 spec/happymapper_to_xml_namespaces_spec.rb
View
@@ -0,0 +1,149 @@
+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"]
+
+ @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
138 spec/happymapper_to_xml_spec.rb
View
@@ -0,0 +1,138 @@
+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"]
+
+ @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.