Skip to content

Commit

Permalink
Improved namespace support
Browse files Browse the repository at this point in the history
The previous namespace support did not handle documents containing multiply
namespaced nodes.  It also made assumptions about the 'default' namespace
when multiple prefixed namespaces were provided (with no implicit default).

This patch allows namespaces to be set item-wide (via 'namespace <ns>' in
the class) or on a per-element basis (via :namespace => <ns> on the
element mapping).

Signed-off-by: John Nunemaker <nunemaker@gmail.com>
  • Loading branch information
mojodna authored and jnunemaker committed Jan 30, 2009
1 parent 6903ee3 commit 11e4264
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 23 deletions.
5 changes: 5 additions & 0 deletions examples/amazon.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@


file_contents = File.read(dir + '/../spec/fixtures/pita.xml') file_contents = File.read(dir + '/../spec/fixtures/pita.xml')


# The document `pita.xml` contains both a default namespace and the 'georss'
# namespace (for the 'point' element).
module PITA module PITA
class Item class Item
include HappyMapper include HappyMapper
Expand All @@ -11,6 +13,9 @@ class Item
element :asin, String, :tag => 'ASIN' element :asin, String, :tag => 'ASIN'
element :detail_page_url, String, :tag => 'DetailPageURL' element :detail_page_url, String, :tag => 'DetailPageURL'
element :manufacturer, String, :tag => 'Manufacturer', :deep => true element :manufacturer, String, :tag => 'Manufacturer', :deep => true
# this is the only element that exists in a different namespace, so it
# must be explicitly specified
element :point, String, :tag => 'point', :namespace => 'georss'
end end


class Items class Items
Expand Down
52 changes: 34 additions & 18 deletions lib/happymapper.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
class Boolean; end class Boolean; end


module HappyMapper module HappyMapper


DEFAULT_NS = "happymapper"

def self.included(base) def self.included(base)
base.instance_variable_set("@attributes", {}) base.instance_variable_set("@attributes", {})
base.instance_variable_set("@elements", {}) base.instance_variable_set("@elements", {})
Expand Down Expand Up @@ -50,7 +52,15 @@ def has_one(name, type, options={})
def has_many(name, type, options={}) def has_many(name, type, options={})
element name, type, {:single => false}.merge(options) element name, type, {:single => false}.merge(options)
end 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.
def namespace(namespace = nil)
@namespace = namespace if namespace
@namespace
end

# Options: # Options:
# :root => Boolean, true means this is xml root # :root => Boolean, true means this is xml root
def tag(new_tag_name, o={}) def tag(new_tag_name, o={})
Expand All @@ -71,24 +81,30 @@ def is_root?


def parse(xml, o={}) def parse(xml, o={})
xpath, collection, options = '', [], {:single => false}.merge(o) xpath, collection, options = '', [], {:single => false}.merge(o)
doc = xml.is_a?(LibXML::XML::Node) ? xml : xml.to_libxml_doc
node = doc.respond_to?(:root) ? doc.root : doc # reset the namespace if it was set to the default

# this is necessary when using the same object mapping instance for
# puts doc.inspect, doc.respond_to?(:root) ? doc.root.inspect : '' # docs w/ and w/o default namespaces

@namespace = nil if @namespace == DEFAULT_NS
unless node.namespaces.default.nil?
namespace = "default_ns:" if xml.is_a?(XML::Node)
node.namespaces.default_prefix = namespace.chop node = xml
# warn "Default XML namespace present -- results are unpredictable" elsif xml.is_a?(XML::Document)
node = xml.root
else
node = xml.to_libxml_doc.root
end end


if node.namespaces.to_a.size > 0 && namespace.nil? && !node.namespaces.namespace.nil? # This is the entry point into the parsing pipeline, so the default
namespace = node.namespaces.namespace.prefix + ":" # namespace prefix registered here will propagate down
namespaces = node.namespaces
if namespaces && namespaces.default
namespaces.default_prefix = DEFAULT_NS
@namespace ||= DEFAULT_NS
end end


# xpath += doc.respond_to?(:root) ? '' : '.'
xpath += is_root? ? '/' : './/' xpath += is_root? ? '/' : './/'
xpath += namespace if namespace xpath += "#{namespace}:" if namespace
xpath += get_tag_name xpath += get_tag_name
# puts "parse: #{xpath}" # puts "parse: #{xpath}"


Expand All @@ -102,7 +118,7 @@ def parse(xml, o={})
end end


elements.each do |elem| elements.each do |elem|
elem.namespace = namespace elem.namespace ||= namespace
obj.send("#{elem.method_name}=", obj.send("#{elem.method_name}=",
elem.from_xml_node(n)) elem.from_xml_node(n))
end end
Expand Down
11 changes: 10 additions & 1 deletion lib/happymapper/item.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ class Item
# options: # options:
# :deep => Boolean False to only parse element's children, True to include # :deep => Boolean False to only parse element's children, True to include
# grandchildren and all others down the chain (// in expath) # grandchildren and all others down the chain (// in expath)
# :namespace => String Element's namespace if it's not the global or inherited
# default
# :single => Boolean False if object should be collection, True for single object # :single => Boolean False if object should be collection, True for single object
# :tag => String Element name if it doesn't match the specified name.
def initialize(name, type, o={}) def initialize(name, type, o={})
self.name = name.to_s self.name = name.to_s
self.type = type self.type = type
self.tag = o.delete(:tag) || name.to_s self.tag = o.delete(:tag) || name.to_s
self.namespace = o[:namespace]
self.options = { self.options = {
:single => false, :single => false,
:deep => false, :deep => false,
Expand All @@ -33,7 +37,7 @@ def from_xml_node(node)
def xpath def xpath
xpath = '' xpath = ''
xpath += './/' if options[:deep] xpath += './/' if options[:deep]
xpath += namespace if namespace xpath += "#{namespace}:" if namespace
xpath += tag xpath += tag
# puts "xpath: #{xpath}" # puts "xpath: #{xpath}"
xpath xpath
Expand Down Expand Up @@ -87,6 +91,11 @@ def typecast(value)


private private
def value_from_xml_node(node) def value_from_xml_node(node)
# this node has a custom namespace (that is present in the doc)
if namespace && !node.namespaces.find_by_prefix(namespace)
self.namespace = nil
end

if element? if element?
result = node.find_first(xpath) result = node.find_first(xpath)
# puts "vfxn: #{xpath} #{result.inspect}" # puts "vfxn: #{xpath} #{result.inspect}"
Expand Down
3 changes: 2 additions & 1 deletion spec/fixtures/pita.xml
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05"> <ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05" xmlns:georss="http://www.georss.org/georss">
<OperationRequest> <OperationRequest>
<HTTPHeaders> <HTTPHeaders>
<Header Name="UserAgent"> <Header Name="UserAgent">
Expand Down Expand Up @@ -27,6 +27,7 @@
<TotalPages>3</TotalPages> <TotalPages>3</TotalPages>
<Item> <Item>
<ASIN>0321480791</ASIN> <ASIN>0321480791</ASIN>
<georss:point>38.5351715088 -121.7948684692</georss:point>
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh</DetailPageURL> <DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh</DetailPageURL>
<ItemAttributes> <ItemAttributes>
<Author>Michael Hartl</Author> <Author>Michael Hartl</Author>
Expand Down
2 changes: 1 addition & 1 deletion spec/happymapper_item_spec.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@


it "should prepend namespace if namespace exists" do it "should prepend namespace if namespace exists" do
item = HappyMapper::Item.new(:foo, String, :tag => 'foobar') item = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
item.namespace = 'v2:' item.namespace = 'v2'
item.xpath.should == 'v2:foobar' item.xpath.should == 'v2:foobar'
end end
end end
Expand Down
20 changes: 18 additions & 2 deletions spec/happymapper_spec.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class Address
include HappyMapper include HappyMapper


tag 'Address' tag 'Address'
namespace 'v2'
element :city, String, :tag => 'City' element :city, String, :tag => 'City'
element :state, String, :tag => 'StateOrProvinceCode' element :state, String, :tag => 'StateOrProvinceCode'
element :zip, String, :tag => 'PostalCode' element :zip, String, :tag => 'PostalCode'
Expand All @@ -62,6 +63,7 @@ class Event
include HappyMapper include HappyMapper


tag 'Events' tag 'Events'
namespace 'v2'
element :timestamp, String, :tag => 'Timestamp' element :timestamp, String, :tag => 'Timestamp'
element :eventtype, String, :tag => 'EventType' element :eventtype, String, :tag => 'EventType'
element :eventdescription, String, :tag => 'EventDescription' element :eventdescription, String, :tag => 'EventDescription'
Expand All @@ -72,6 +74,7 @@ class PackageWeight
include HappyMapper include HappyMapper


tag 'PackageWeight' tag 'PackageWeight'
namespace 'v2'
element :units, String, :tag => 'Units' element :units, String, :tag => 'Units'
element :value, Integer, :tag => 'Value' element :value, Integer, :tag => 'Value'
end end
Expand All @@ -80,6 +83,7 @@ class TrackDetails
include HappyMapper include HappyMapper


tag 'TrackDetails' tag 'TrackDetails'
namespace 'v2'
element :tracking_number, String, :tag => 'TrackingNumber' element :tracking_number, String, :tag => 'TrackingNumber'
element :status_code, String, :tag => 'StatusCode' element :status_code, String, :tag => 'StatusCode'
element :status_desc, String, :tag => 'StatusDescription' element :status_desc, String, :tag => 'StatusDescription'
Expand All @@ -94,6 +98,7 @@ class Notification
include HappyMapper include HappyMapper


tag 'Notifications' tag 'Notifications'
namespace 'v2'
element :severity, String, :tag => 'Severity' element :severity, String, :tag => 'Severity'
element :source, String, :tag => 'Source' element :source, String, :tag => 'Source'
element :code, Integer, :tag => 'Code' element :code, Integer, :tag => 'Code'
Expand All @@ -105,13 +110,15 @@ class TransactionDetail
include HappyMapper include HappyMapper


tag 'TransactionDetail' tag 'TransactionDetail'
namespace 'v2'
element :cust_tran_id, String, :tag => 'CustomerTransactionId' element :cust_tran_id, String, :tag => 'CustomerTransactionId'
end end


class TrackReply class TrackReply
include HappyMapper include HappyMapper


tag 'TrackReply', :root => true tag 'TrackReply', :root => true
namespace 'v2'
element :highest_severity, String, :tag => 'HighestSeverity' element :highest_severity, String, :tag => 'HighestSeverity'
element :more_data, Boolean, :tag => 'MoreData' element :more_data, Boolean, :tag => 'MoreData'
has_many :notifications, Notification, :tag => 'Notifications' has_many :notifications, Notification, :tag => 'Notifications'
Expand Down Expand Up @@ -167,13 +174,15 @@ class Status
element :in_reply_to_status_id, Integer element :in_reply_to_status_id, Integer
element :in_reply_to_user_id, Integer element :in_reply_to_user_id, Integer
element :favorited, Boolean element :favorited, Boolean
element :non_existent, String, :tag => 'dummy', :namespace => 'fake'
has_one :user, User has_one :user, User
end end


class CurrentWeather class CurrentWeather
include HappyMapper include HappyMapper


tag 'ob' tag 'ob'
namespace 'aws'
element :temperature, Integer, :tag => 'temp' element :temperature, Integer, :tag => 'temp'
element :feels_like, Integer, :tag => 'feels-like' element :feels_like, Integer, :tag => 'feels-like'
element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String} element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String}
Expand All @@ -198,6 +207,7 @@ class Item
element :asin, String, :tag => 'ASIN' element :asin, String, :tag => 'ASIN'
element :detail_page_url, String, :tag => 'DetailPageURL' element :detail_page_url, String, :tag => 'DetailPageURL'
element :manufacturer, String, :tag => 'Manufacturer', :deep => true element :manufacturer, String, :tag => 'Manufacturer', :deep => true
element :point, String, :tag => 'point', :namespace => 'georss'
end end


class Items class Items
Expand Down Expand Up @@ -290,7 +300,7 @@ class Foo; include HappyMapper end
element.type.should == User element.type.should == User
element.options[:single] = false element.options[:single] = false
end end

it "should default tag name to lowercase class" do it "should default tag name to lowercase class" do
Foo.get_tag_name.should == 'foo' Foo.get_tag_name.should == 'foo'
end end
Expand All @@ -305,6 +315,11 @@ module Bar; class Baz; include HappyMapper; end; end
Foo.get_tag_name.should == 'FooBar' Foo.get_tag_name.should == 'FooBar'
end end


it "should allow setting a namespace" do
Foo.namespace(namespace = "foo")
Foo.namespace.should == namespace
end

it "should provide #parse" do it "should provide #parse" do
Foo.should respond_to(:parse) Foo.should respond_to(:parse)
end end
Expand All @@ -320,7 +335,7 @@ module Bar; class Baz; include HappyMapper; end; end
describe "#elements" do describe "#elements" do
it "should only return elements for the current class" do it "should only return elements for the current class" do
Post.elements.size.should == 0 Post.elements.size.should == 0
Status.elements.size.should == 9 Status.elements.size.should == 10
end end
end end


Expand Down Expand Up @@ -376,6 +391,7 @@ module Bar; class Baz; include HappyMapper; end; end
first = items.items[0] first = items.items[0]
second = items.items[1] second = items.items[1]
first.asin.should == '0321480791' first.asin.should == '0321480791'
first.point.should == '38.5351715088 -121.7948684692'
first.detail_page_url.should == 'http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh' first.detail_page_url.should == 'http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh'
first.manufacturer.should == 'Addison-Wesley Professional' first.manufacturer.should == 'Addison-Wesley Professional'
second.asin.should == '047022388X' second.asin.should == '047022388X'
Expand Down

0 comments on commit 11e4264

Please sign in to comment.