jnunemaker-happymapper 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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