Skip to content

Commit

Permalink
Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
Browse files Browse the repository at this point in the history
…arting this and providing me with a lot of research.
  • Loading branch information
jnunemaker committed Jan 29, 2009
1 parent 94eb0a9 commit a88ab23
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 112 deletions.
4 changes: 4 additions & 0 deletions History
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
== 0.2.0
* 1 major enhancement
* Automatic handling of namespaces (part by Robert Lowrey and rest by John Nunemaker)

== 0.1.7 2009-01-29
* 1 minor enhancement
* Support dashes in elements (Josh Nichols)
Expand Down
2 changes: 1 addition & 1 deletion examples/amazon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Items
end
end

item = PITA::Items.parse(file_contents, :single => true, :use_default_namespace => true)
item = PITA::Items.parse(file_contents, :single => true)
item.items.each do |i|
puts i.asin, i.detail_page_url, i.manufacturer, ''
end
8 changes: 4 additions & 4 deletions examples/current_weather.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

class CurrentWeather
include HappyMapper
tag 'aws:ob'
element :temperature, Integer, :tag => 'aws:temp'
element :feels_like, Integer, :tag => 'aws:feels-like'
element :current_condition, String, :tag => 'aws:current-condition', :attributes => {:icon => String}
tag 'ob'
element :temperature, Integer, :tag => 'temp'
element :feels_like, Integer, :tag => 'feels-like'
element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String}
end

CurrentWeather.parse(file_contents).each do |current_weather|
Expand Down
93 changes: 31 additions & 62 deletions lib/happymapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def attribute(name, type, options={})
attribute = Attribute.new(name, type, options)
@attributes[to_s] ||= []
@attributes[to_s] << attribute
create_accessor(attribute.name)
attr_accessor attribute.method_name.intern
end

def attributes
Expand All @@ -36,7 +36,7 @@ def element(name, type, options={})
element = Element.new(name, type, options)
@elements[to_s] ||= []
@elements[to_s] << element
create_accessor(element.name)
attr_accessor element.method_name.intern
end

def elements
Expand Down Expand Up @@ -64,85 +64,54 @@ def parse(xml, o={})
:single => false,
:from_root => false,
}.merge(o)


xpath, collection = '', []

doc = xml.is_a?(LibXML::XML::Node) ? xml : xml.to_libxml_doc
node = doc.respond_to?(:root) ? doc.root : doc
xpath = ''

# if doc has a default namespace, turn on ':use_default_namespace' & set default_prefix for LibXML
# puts doc.inspect, doc.respond_to?(:root) ? doc.root.inspect : ''

unless node.namespaces.default.nil?
options[:use_default_namespace] = true
namespace = "default_ns:"
node.namespaces.default_prefix = namespace.chop
warn "Default XML namespace present -- results are unpredictable"
# warn "Default XML namespace present -- results are unpredictable"
end

# if not using default namespace, get our namespace prefix (if we have one) (thanks to LibXML)

if node.namespaces.to_a.size > 0 && namespace.nil? && !node.namespaces.namespace.nil?
namespace = node.namespaces.namespace.prefix + ":"
end

nodes = if namespace
xpath += '/' if options[:from_root]
xpath += namespace
xpath += get_tag_name
node.find(xpath)
else
xpath += '.' unless doc.respond_to?(:root)
xpath += '//'
xpath += get_tag_name
doc.find(xpath)
xpath += doc.respond_to?(:root) ? '' : '.'
xpath += options[:from_root] ? '/' : '//'
xpath += namespace if namespace
xpath += get_tag_name
# puts "parse: #{xpath}"

nodes = node.find(xpath)
nodes.each do |node|
obj = new

attributes.each do |attr|
obj.send("#{attr.method_name}=",
attr.from_xml_node(node))
end

elements.each do |elem|
elem.namespace = namespace
# puts "#{elem.method_name} - #{namespace} - #{elem.namespace}"
obj.send("#{elem.method_name}=",
elem.from_xml_node(node))
end
collection << obj
end

collection = create_collection(nodes, namespace)

# per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
nodes = nil
GC.start

options[:single] ? collection.first : collection
end

private
def create_collection(nodes, namespace=nil)
nodes.inject([]) do |acc, el|
obj = new
attributes.each { |attr| obj.send("#{normalize_name attr.name}=", attr.from_xml_node(el)) }
elements.each { |elem| obj.send("#{normalize_name elem.name}=", elem.from_xml_node(el, namespace)) }
acc << obj
end
end

def create_getter(name)
name = normalize_name(name)

class_eval <<-EOS, __FILE__, __LINE__
def #{name}
@#{name}
end
EOS
end

def create_setter(name)
name = normalize_name(name)

class_eval <<-EOS, __FILE__, __LINE__
def #{name}=(value)
@#{name} = value
end
EOS
end

def create_accessor(name)
name = normalize_name(name)

create_getter(name)
create_setter(name)
end

def normalize_name(name)
name.gsub('-', '_')
end
end
end

Expand Down
34 changes: 20 additions & 14 deletions lib/happymapper/item.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module HappyMapper
class Item
attr_accessor :type, :tag, :options
attr_reader :name
attr_accessor :name, :type, :tag, :options, :namespace

Types = [String, Float, Time, Date, DateTime, Integer, Boolean]

Expand All @@ -10,7 +9,7 @@ class Item
# grandchildren and all others down the chain (// in expath)
# :single => Boolean False if object should be collection, True for single object
def initialize(name, type, o={})
self.name = name
self.name = name.to_s
self.type = type
self.tag = o.delete(:tag) || name.to_s
self.options = {
Expand All @@ -20,21 +19,27 @@ def initialize(name, type, o={})

@xml_type = self.class.to_s.split('::').last.downcase
end

def name=(new_name)
@name = new_name.to_s
end

def from_xml_node(node, namespace = nil)
def from_xml_node(node)
if primitive?
value_from_xml_node(node, namespace) do |value_before_type_cast|
value_from_xml_node(node) do |value_before_type_cast|
typecast(value_before_type_cast)
end
else
type.parse(node, options)
end
end

def xpath
xpath = ''
xpath += './/' if options[:deep]
# puts "xpath namespace: #{namespace}"
xpath += namespace if namespace
xpath += tag
# puts "xpath: #{xpath}"
xpath
end

def primitive?
Types.include?(type)
end
Expand All @@ -47,6 +52,10 @@ def attribute?
!element?
end

def method_name
@method_name ||= name.tr('-', '_')
end

def typecast(value)
return value if value.kind_of?(type) || value.nil?
begin
Expand Down Expand Up @@ -78,13 +87,10 @@ def typecast(value)
end

private
def value_from_xml_node(node, namespace=nil)
def value_from_xml_node(node)
if element?
xpath = ''
xpath += './/' if options[:deep]
xpath += namespace if namespace
xpath += tag
result = node.find_first(xpath)
# puts "vfxn: #{xpath} #{result.inspect}"
if result
value = yield(result.content)
if options[:attributes].is_a?(Hash)
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/product_default_namespace.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<products xmlns="http://bigco.com">
<product>
<title> A Title</title>
<title>A Title</title>
<features_bullets>
<feature>This is feature text 1</feature>
<feature>This is feature text 2</feature>
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/product_no_namespace.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<products>
<product>
<title> A Title</title>
<title>A Title</title>
<features_bullets>
<feature>This is feature text 1</feature>
<feature>This is feature text 2</feature>
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/product_single_namespace.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<products xmlns:juju="http://bigco.com">
<product>
<title> A Title</title>
<title>A Title</title>
<features_bullets>
<feature>This is feature text 1</feature>
<feature>This is feature text 2</feature>
Expand Down
65 changes: 43 additions & 22 deletions spec/happymapper_item_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,91 @@

describe "new instance" do
before do
@attr = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
@item = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
end

it "should accept a name" do
@attr.name.should == 'foo'
@item.name.should == 'foo'
end

it 'should accept a type' do
@attr.type.should == String
@item.type.should == String
end

it 'should accept :tag as an option' do
@attr.tag.should == 'foobar'
@item.tag.should == 'foobar'
end

it 'should provide #name' do
@attr.should respond_to(:name)
it "should have a method_name" do
@item.method_name.should == 'foo'
end
end

describe "#method_name" do
it "should convert dashes to underscores" do
item = HappyMapper::Item.new(:'foo-bar', String, :tag => 'foobar')
item.method_name.should == 'foo_bar'
end
end

describe "#xpath" do
it "should default to tag" do
item = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
item.xpath.should == 'foobar'
end

it "should prepend with .// if options[:deep] true" do
item = HappyMapper::Item.new(:foo, String, :tag => 'foobar', :deep => true)
item.xpath.should == './/foobar'
end

it 'should provide #type' do
@attr.should respond_to(:type)
it "should prepend namespace if namespace exists" do
item = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
item.namespace = 'v2:'
item.xpath.should == 'v2:foobar'
end
end

describe "typecasting" do
it "should work with Strings" do
attribute = HappyMapper::Item.new(:foo, String)
item = HappyMapper::Item.new(:foo, String)
[21, '21'].each do |a|
attribute.typecast(a).should == '21'
item.typecast(a).should == '21'
end
end

it "should work with Integers" do
attribute = HappyMapper::Item.new(:foo, Integer)
item = HappyMapper::Item.new(:foo, Integer)
[21, 21.0, '21'].each do |a|
attribute.typecast(a).should == 21
item.typecast(a).should == 21
end
end

it "should work with Floats" do
attribute = HappyMapper::Item.new(:foo, Float)
item = HappyMapper::Item.new(:foo, Float)
[21, 21.0, '21'].each do |a|
attribute.typecast(a).should == 21.0
item.typecast(a).should == 21.0
end
end

it "should work with Times" do
attribute = HappyMapper::Item.new(:foo, Time)
attribute.typecast('2000-01-01 01:01:01.123456').should == Time.local(2000, 1, 1, 1, 1, 1, 123456)
item = HappyMapper::Item.new(:foo, Time)
item.typecast('2000-01-01 01:01:01.123456').should == Time.local(2000, 1, 1, 1, 1, 1, 123456)
end

it "should work with Dates" do
attribute = HappyMapper::Item.new(:foo, Date)
attribute.typecast('2000-01-01').should == Date.new(2000, 1, 1)
item = HappyMapper::Item.new(:foo, Date)
item.typecast('2000-01-01').should == Date.new(2000, 1, 1)
end

it "should work with DateTimes" do
attribute = HappyMapper::Item.new(:foo, DateTime)
attribute.typecast('2000-01-01 00:00:00').should == DateTime.new(2000, 1, 1, 0, 0, 0)
item = HappyMapper::Item.new(:foo, DateTime)
item.typecast('2000-01-01 00:00:00').should == DateTime.new(2000, 1, 1, 0, 0, 0)
end

it "should work with Boolean" do
attribute = HappyMapper::Item.new(:foo, Boolean)
attribute.typecast('false').should == false
item = HappyMapper::Item.new(:foo, Boolean)
item.typecast('false').should == false
end
end
end
Loading

0 comments on commit a88ab23

Please sign in to comment.