jnunemaker-happymapper 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History CHANGED
@@ -1,3 +1,10 @@
1
+ == 0.2.1
2
+ * 1 minor fix, 3 major enhancements
3
+ * fixed warnings about using XML::Parser (mojodna)
4
+ * Improved namespace support, now handles multiple namespaces and allows namespaces to be set item wide or on a per element basis (mojodna)
5
+ * Auto detect root nodes (mojodna)
6
+ * Type coercion (mojodna)
7
+
1
8
  == 0.2.0
2
9
  * 1 major enhancement, 2 minor ehancements
3
10
  * Automatic handling of namespaces (part by Robert Lowrey and rest by John Nunemaker)
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ Echoe.new(ProjectName, HappyMapper::Version) do |p|
13
13
  p.url = "http://#{ProjectName}.rubyforge.org"
14
14
  p.author = "John Nunemaker"
15
15
  p.email = "nunemaker@gmail.com"
16
- p.extra_deps = [['libxml-ruby', '>= 0.9.7']]
16
+ p.extra_deps = [['libxml-ruby', '= 0.9.8']]
17
17
  p.need_tar_gz = false
18
18
  p.docs_host = WebsitePath
19
19
  end
data/TODO CHANGED
@@ -1 +0,0 @@
1
- * doesn't do xml namespaces really (does work with default namespace though)
data/examples/amazon.rb CHANGED
@@ -3,6 +3,8 @@ require File.join(dir, 'happymapper')
3
3
 
4
4
  file_contents = File.read(dir + '/../spec/fixtures/pita.xml')
5
5
 
6
+ # The document `pita.xml` contains both a default namespace and the 'georss'
7
+ # namespace (for the 'point' element).
6
8
  module PITA
7
9
  class Item
8
10
  include HappyMapper
@@ -11,6 +13,9 @@ module PITA
11
13
  element :asin, String, :tag => 'ASIN'
12
14
  element :detail_page_url, String, :tag => 'DetailPageURL'
13
15
  element :manufacturer, String, :tag => 'Manufacturer', :deep => true
16
+ # this is the only element that exists in a different namespace, so it
17
+ # must be explicitly specified
18
+ element :point, String, :tag => 'point', :namespace => 'georss'
14
19
  end
15
20
 
16
21
  class Items
@@ -5,7 +5,9 @@ file_contents = File.read(dir + '/../spec/fixtures/current_weather.xml')
5
5
 
6
6
  class CurrentWeather
7
7
  include HappyMapper
8
+
8
9
  tag 'ob'
10
+ namespace 'aws'
9
11
  element :temperature, Integer, :tag => 'temp'
10
12
  element :feels_like, Integer, :tag => 'feels-like'
11
13
  element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String}
@@ -7,7 +7,7 @@ module GitHub
7
7
  class Commit
8
8
  include HappyMapper
9
9
 
10
- tag "commit", :root => true
10
+ tag "commit"
11
11
  element :url, String
12
12
  element :tree, String
13
13
  element :message, String
data/happymapper.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{happymapper}
5
- s.version = "0.2.0"
5
+ s.version = "0.2.1"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["John Nunemaker"]
@@ -25,14 +25,14 @@ Gem::Specification.new do |s|
25
25
  s.specification_version = 2
26
26
 
27
27
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
- s.add_runtime_dependency(%q<libxml-ruby>, [">= 0.9.7"])
28
+ s.add_runtime_dependency(%q<libxml-ruby>, ["= 0.9.8"])
29
29
  s.add_development_dependency(%q<echoe>, [">= 0"])
30
30
  else
31
- s.add_dependency(%q<libxml-ruby>, [">= 0.9.7"])
31
+ s.add_dependency(%q<libxml-ruby>, ["= 0.9.8"])
32
32
  s.add_dependency(%q<echoe>, [">= 0"])
33
33
  end
34
34
  else
35
- s.add_dependency(%q<libxml-ruby>, [">= 0.9.7"])
35
+ s.add_dependency(%q<libxml-ruby>, ["= 0.9.8"])
36
36
  s.add_dependency(%q<echoe>, [">= 0"])
37
37
  end
38
38
  end
data/lib/happymapper.rb CHANGED
@@ -5,7 +5,7 @@ require 'date'
5
5
  require 'time'
6
6
  require 'rubygems'
7
7
 
8
- gem 'libxml-ruby', '>= 0.9.7'
8
+ gem 'libxml-ruby', '= 0.9.8'
9
9
  require 'xml'
10
10
  require 'libxml_ext/libxml_helper'
11
11
 
@@ -13,7 +13,9 @@ require 'libxml_ext/libxml_helper'
13
13
  class Boolean; end
14
14
 
15
15
  module HappyMapper
16
-
16
+
17
+ DEFAULT_NS = "happymapper"
18
+
17
19
  def self.included(base)
18
20
  base.instance_variable_set("@attributes", {})
19
21
  base.instance_variable_set("@elements", {})
@@ -50,12 +52,16 @@ module HappyMapper
50
52
  def has_many(name, type, options={})
51
53
  element name, type, {:single => false}.merge(options)
52
54
  end
53
-
54
- # Options:
55
- # :root => Boolean, true means this is xml root
56
- def tag(new_tag_name, o={})
57
- options = {:root => false}.merge(o)
58
- @root = options.delete(:root)
55
+
56
+ # Specify a namespace if a node and all its children are all namespaced
57
+ # elements. This is simpler than passing the :namespace option to each
58
+ # defined element.
59
+ def namespace(namespace = nil)
60
+ @namespace = namespace if namespace
61
+ @namespace
62
+ end
63
+
64
+ def tag(new_tag_name)
59
65
  @tag_name = new_tag_name.to_s
60
66
  end
61
67
 
@@ -65,55 +71,61 @@ module HappyMapper
65
71
  end
66
72
  end
67
73
 
68
- def is_root?
69
- @root
70
- end
71
-
72
- def parse(xml, o={})
73
- xpath, collection, options = '', [], {:single => false}.merge(o)
74
- doc = xml.is_a?(LibXML::XML::Node) ? xml : xml.to_libxml_doc
75
- node = doc.respond_to?(:root) ? doc.root : doc
76
-
77
- # puts doc.inspect, doc.respond_to?(:root) ? doc.root.inspect : ''
78
-
79
- unless node.namespaces.default.nil?
80
- namespace = "default_ns:"
81
- node.namespaces.default_prefix = namespace.chop
82
- # warn "Default XML namespace present -- results are unpredictable"
74
+ def parse(xml, options = {})
75
+ # locally scoped copy of namespace for this parse run
76
+ namespace = @namespace
77
+
78
+ if xml.is_a?(XML::Node)
79
+ node = xml
80
+ else
81
+ if xml.is_a?(XML::Document)
82
+ node = xml.root
83
+ else
84
+ node = xml.to_libxml_doc.root
85
+ end
86
+
87
+ root = node.name == get_tag_name
83
88
  end
84
-
85
- if node.namespaces.to_a.size > 0 && namespace.nil? && !node.namespaces.namespace.nil?
86
- namespace = node.namespaces.namespace.prefix + ":"
89
+
90
+ # This is the entry point into the parsing pipeline, so the default
91
+ # namespace prefix registered here will propagate down
92
+ namespaces = node.namespaces
93
+ if namespaces && namespaces.default
94
+ namespaces.default_prefix = DEFAULT_NS
95
+ namespace ||= DEFAULT_NS
87
96
  end
88
-
89
- # xpath += doc.respond_to?(:root) ? '' : '.'
90
- xpath += is_root? ? '/' : './/'
91
- xpath += namespace if namespace
97
+
98
+ xpath = root ? '/' : './/'
99
+ xpath += "#{namespace}:" if namespace
92
100
  xpath += get_tag_name
93
101
  # puts "parse: #{xpath}"
94
102
 
95
103
  nodes = node.find(xpath)
96
- nodes.each do |n|
104
+ collection = nodes.collect do |n|
97
105
  obj = new
98
106
 
99
107
  attributes.each do |attr|
100
108
  obj.send("#{attr.method_name}=",
101
- attr.from_xml_node(n))
109
+ attr.from_xml_node(n, namespace))
102
110
  end
103
111
 
104
112
  elements.each do |elem|
105
- elem.namespace = namespace
106
113
  obj.send("#{elem.method_name}=",
107
- elem.from_xml_node(n))
114
+ elem.from_xml_node(n, namespace))
108
115
  end
109
- collection << obj
116
+
117
+ obj
110
118
  end
111
119
 
112
120
  # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
113
121
  nodes = nil
114
122
  GC.start
115
123
 
116
- options[:single] || is_root? ? collection.first : collection
124
+ if options[:single] || root
125
+ collection.first
126
+ else
127
+ collection
128
+ end
117
129
  end
118
130
  end
119
131
  end
@@ -7,33 +7,55 @@ module HappyMapper
7
7
  # options:
8
8
  # :deep => Boolean False to only parse element's children, True to include
9
9
  # grandchildren and all others down the chain (// in expath)
10
+ # :namespace => String Element's namespace if it's not the global or inherited
11
+ # default
12
+ # :parser => Symbol Class method to use for type coercion.
13
+ # :raw => Boolean Use raw node value (inc. tags) when parsing.
10
14
  # :single => Boolean False if object should be collection, True for single object
15
+ # :tag => String Element name if it doesn't match the specified name.
11
16
  def initialize(name, type, o={})
12
17
  self.name = name.to_s
13
18
  self.type = type
14
19
  self.tag = o.delete(:tag) || name.to_s
15
- self.options = {
16
- :single => false,
17
- :deep => false,
18
- }.merge(o)
20
+ self.options = o
19
21
 
20
22
  @xml_type = self.class.to_s.split('::').last.downcase
21
23
  end
22
24
 
23
- def from_xml_node(node)
25
+ def from_xml_node(node, namespace)
24
26
  if primitive?
25
- value_from_xml_node(node) do |value_before_type_cast|
26
- typecast(value_before_type_cast)
27
+ find(node, namespace) do |n|
28
+ if n.respond_to?(:content)
29
+ typecast(n.content)
30
+ else
31
+ typecast(n.to_s)
32
+ end
27
33
  end
28
34
  else
29
- type.parse(node, options)
35
+ if options[:parser]
36
+ find(node, namespace) do |n|
37
+ if n.respond_to?(:content) && !options[:raw]
38
+ value = n.content
39
+ else
40
+ value = n.to_s
41
+ end
42
+
43
+ begin
44
+ type.send(options[:parser].to_sym, value)
45
+ rescue
46
+ nil
47
+ end
48
+ end
49
+ else
50
+ type.parse(node, options)
51
+ end
30
52
  end
31
53
  end
32
54
 
33
- def xpath
55
+ def xpath(namespace = self.namespace)
34
56
  xpath = ''
35
57
  xpath += './/' if options[:deep]
36
- xpath += namespace if namespace
58
+ xpath += "#{namespace}:" if namespace
37
59
  xpath += tag
38
60
  # puts "xpath: #{xpath}"
39
61
  xpath
@@ -86,16 +108,25 @@ module HappyMapper
86
108
  end
87
109
 
88
110
  private
89
- def value_from_xml_node(node)
111
+ def find(node, namespace, &block)
112
+ # this node has a custom namespace (that is present in the doc)
113
+ if self.namespace && node.namespaces.find_by_prefix(self.namespace)
114
+ # from the class definition
115
+ namespace = self.namespace
116
+ elsif options[:namespace] && node.namespaces.find_by_prefix(options[:namespace])
117
+ # from an element definition
118
+ namespace = options[:namespace]
119
+ end
120
+
90
121
  if element?
91
- result = node.find_first(xpath)
122
+ result = node.find_first(xpath(namespace))
92
123
  # puts "vfxn: #{xpath} #{result.inspect}"
93
124
  if result
94
- value = yield(result.content)
125
+ value = yield(result)
95
126
  if options[:attributes].is_a?(Hash)
96
127
  result.attributes.each do |xml_attribute|
97
128
  if attribute_options = options[:attributes][xml_attribute.name.to_sym]
98
- attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result)
129
+ attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result, namespace)
99
130
  result.instance_eval <<-EOV
100
131
  def value.#{xml_attribute.name}
101
132
  #{attribute_value.inspect}
@@ -1,3 +1,3 @@
1
1
  module HappyMapper
2
- Version = '0.2.0'
2
+ Version = '0.2.1'
3
3
  end
@@ -50,8 +50,6 @@ end
50
50
 
51
51
  class String
52
52
  def to_libxml_doc
53
- xp = XML::Parser.new
54
- xp.string = self
55
- return xp.parse
53
+ XML::Parser.string(self).parse
56
54
  end
57
55
  end
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05">
2
+ <ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2005-10-05" xmlns:georss="http://www.georss.org/georss">
3
3
  <OperationRequest>
4
4
  <HTTPHeaders>
5
5
  <Header Name="UserAgent">
@@ -27,6 +27,7 @@
27
27
  <TotalPages>3</TotalPages>
28
28
  <Item>
29
29
  <ASIN>0321480791</ASIN>
30
+ <georss:point>38.5351715088 -121.7948684692</georss:point>
30
31
  <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>
31
32
  <ItemAttributes>
32
33
  <Author>Michael Hartl</Author>
@@ -44,7 +44,7 @@ describe HappyMapper::Item do
44
44
 
45
45
  it "should prepend namespace if namespace exists" do
46
46
  item = HappyMapper::Item.new(:foo, String, :tag => 'foobar')
47
- item.namespace = 'v2:'
47
+ item.namespace = 'v2'
48
48
  item.xpath.should == 'v2:foobar'
49
49
  end
50
50
  end
@@ -1,5 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
  require 'pp'
3
+ require 'uri'
3
4
 
4
5
  class Feature
5
6
  include HappyMapper
@@ -38,7 +39,7 @@ module FamilySearch
38
39
  class FamilyTree
39
40
  include HappyMapper
40
41
 
41
- tag 'familytree', :root => true
42
+ tag 'familytree'
42
43
  attribute :version, String
43
44
  attribute :status_message, String, :tag => 'statusMessage'
44
45
  attribute :status_code, String, :tag => 'statusCode'
@@ -51,6 +52,7 @@ module FedEx
51
52
  include HappyMapper
52
53
 
53
54
  tag 'Address'
55
+ namespace 'v2'
54
56
  element :city, String, :tag => 'City'
55
57
  element :state, String, :tag => 'StateOrProvinceCode'
56
58
  element :zip, String, :tag => 'PostalCode'
@@ -62,6 +64,7 @@ module FedEx
62
64
  include HappyMapper
63
65
 
64
66
  tag 'Events'
67
+ namespace 'v2'
65
68
  element :timestamp, String, :tag => 'Timestamp'
66
69
  element :eventtype, String, :tag => 'EventType'
67
70
  element :eventdescription, String, :tag => 'EventDescription'
@@ -72,6 +75,7 @@ module FedEx
72
75
  include HappyMapper
73
76
 
74
77
  tag 'PackageWeight'
78
+ namespace 'v2'
75
79
  element :units, String, :tag => 'Units'
76
80
  element :value, Integer, :tag => 'Value'
77
81
  end
@@ -80,6 +84,7 @@ module FedEx
80
84
  include HappyMapper
81
85
 
82
86
  tag 'TrackDetails'
87
+ namespace 'v2'
83
88
  element :tracking_number, String, :tag => 'TrackingNumber'
84
89
  element :status_code, String, :tag => 'StatusCode'
85
90
  element :status_desc, String, :tag => 'StatusDescription'
@@ -94,6 +99,7 @@ module FedEx
94
99
  include HappyMapper
95
100
 
96
101
  tag 'Notifications'
102
+ namespace 'v2'
97
103
  element :severity, String, :tag => 'Severity'
98
104
  element :source, String, :tag => 'Source'
99
105
  element :code, Integer, :tag => 'Code'
@@ -105,13 +111,15 @@ module FedEx
105
111
  include HappyMapper
106
112
 
107
113
  tag 'TransactionDetail'
114
+ namespace 'v2'
108
115
  element :cust_tran_id, String, :tag => 'CustomerTransactionId'
109
116
  end
110
117
 
111
118
  class TrackReply
112
119
  include HappyMapper
113
120
 
114
- tag 'TrackReply', :root => true
121
+ tag 'TrackReply'
122
+ namespace 'v2'
115
123
  element :highest_severity, String, :tag => 'HighestSeverity'
116
124
  element :more_data, Boolean, :tag => 'MoreData'
117
125
  has_many :notifications, Notification, :tag => 'Notifications'
@@ -167,6 +175,7 @@ class Status
167
175
  element :in_reply_to_status_id, Integer
168
176
  element :in_reply_to_user_id, Integer
169
177
  element :favorited, Boolean
178
+ element :non_existent, String, :tag => 'dummy', :namespace => 'fake'
170
179
  has_one :user, User
171
180
  end
172
181
 
@@ -174,6 +183,7 @@ class CurrentWeather
174
183
  include HappyMapper
175
184
 
176
185
  tag 'ob'
186
+ namespace 'aws'
177
187
  element :temperature, Integer, :tag => 'temp'
178
188
  element :feels_like, Integer, :tag => 'feels-like'
179
189
  element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String}
@@ -182,7 +192,7 @@ end
182
192
  class Address
183
193
  include HappyMapper
184
194
 
185
- tag 'address', :root => true
195
+ tag 'address'
186
196
  element :street, String
187
197
  element :postcode, String
188
198
  element :housenumber, String
@@ -190,14 +200,19 @@ class Address
190
200
  element :country, String
191
201
  end
192
202
 
203
+ # for type coercion
204
+ class ProductGroup < String; end
205
+
193
206
  module PITA
194
207
  class Item
195
208
  include HappyMapper
196
209
 
197
210
  tag 'Item' # if you put class in module you need tag
198
211
  element :asin, String, :tag => 'ASIN'
199
- element :detail_page_url, String, :tag => 'DetailPageURL'
212
+ element :detail_page_url, URI, :tag => 'DetailPageURL', :parser => :parse
200
213
  element :manufacturer, String, :tag => 'Manufacturer', :deep => true
214
+ element :point, String, :tag => 'point', :namespace => 'georss'
215
+ element :product_group, ProductGroup, :tag => 'ProductGroup', :deep => true, :parser => :new, :raw => true
201
216
  end
202
217
 
203
218
  class Items
@@ -214,7 +229,7 @@ module GitHub
214
229
  class Commit
215
230
  include HappyMapper
216
231
 
217
- tag "commit", :root => true
232
+ tag "commit"
218
233
  element :url, String
219
234
  element :tree, String
220
235
  element :message, String
@@ -290,7 +305,7 @@ describe HappyMapper do
290
305
  element.type.should == User
291
306
  element.options[:single] = false
292
307
  end
293
-
308
+
294
309
  it "should default tag name to lowercase class" do
295
310
  Foo.get_tag_name.should == 'foo'
296
311
  end
@@ -305,6 +320,11 @@ describe HappyMapper do
305
320
  Foo.get_tag_name.should == 'FooBar'
306
321
  end
307
322
 
323
+ it "should allow setting a namespace" do
324
+ Foo.namespace(namespace = "foo")
325
+ Foo.namespace.should == namespace
326
+ end
327
+
308
328
  it "should provide #parse" do
309
329
  Foo.should respond_to(:parse)
310
330
  end
@@ -320,7 +340,7 @@ describe HappyMapper do
320
340
  describe "#elements" do
321
341
  it "should only return elements for the current class" do
322
342
  Post.elements.size.should == 0
323
- Status.elements.size.should == 9
343
+ Status.elements.size.should == 10
324
344
  end
325
345
  end
326
346
 
@@ -376,8 +396,11 @@ describe HappyMapper do
376
396
  first = items.items[0]
377
397
  second = items.items[1]
378
398
  first.asin.should == '0321480791'
379
- 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'
399
+ first.point.should == '38.5351715088 -121.7948684692'
400
+ first.detail_page_url.should be_a_kind_of(URI)
401
+ first.detail_page_url.to_s.should == 'http://www.amazon.com/gp/redirect.html%3FASIN=0321480791%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/0321480791%253FSubscriptionId=dontbeaswoosh'
380
402
  first.manufacturer.should == 'Addison-Wesley Professional'
403
+ first.product_group.should == '<ProductGroup>Book</ProductGroup>'
381
404
  second.asin.should == '047022388X'
382
405
  second.manufacturer.should == 'Wrox'
383
406
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jnunemaker-happymapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
@@ -17,9 +17,9 @@ dependencies:
17
17
  version_requirement:
18
18
  version_requirements: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - ">="
20
+ - - "="
21
21
  - !ruby/object:Gem::Version
22
- version: 0.9.7
22
+ version: 0.9.8
23
23
  version:
24
24
  - !ruby/object:Gem::Dependency
25
25
  name: echoe