nokogiri-happymapper 0.3.6 → 0.5.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.
@@ -26,14 +26,25 @@ module HappyMapper
26
26
  def constant
27
27
  @constant ||= constantize(type)
28
28
  end
29
-
29
+
30
+ #
31
+ # @param [XMLNode] node the xml node that is being parsed
32
+ # @param [String] namespace the name of the namespace
33
+ # @param [Hash] xpath_options additional xpath options
34
+ #
30
35
  def from_xml_node(node, namespace, xpath_options)
36
+
37
+ # If the item is defined as a primitive type then cast the value to that type
38
+ # else if the type is XMLContent then store the xml value
39
+ # else the type, specified, needs to handle the parsing.
40
+ #
41
+
31
42
  if primitive?
32
43
  find(node, namespace, xpath_options) do |n|
33
44
  if n.respond_to?(:content)
34
45
  typecast(n.content)
35
46
  else
36
- typecast(n.to_s)
47
+ typecast(n)
37
48
  end
38
49
  end
39
50
  elsif constant == XmlContent
@@ -42,6 +53,11 @@ module HappyMapper
42
53
  n.respond_to?(:to_xml) ? n.to_xml : n.to_s
43
54
  end
44
55
  else
56
+
57
+ # When not a primitive type or XMLContent then default to using the
58
+ # class method #parse of the type class. If the option 'parser' has been
59
+ # defined then call that method on the type class instead of #parse
60
+
45
61
  if options[:parser]
46
62
  find(node, namespace, xpath_options) do |n|
47
63
  if n.respond_to?(:content) && !options[:raw]
@@ -67,10 +83,12 @@ module HappyMapper
67
83
  xpath += './/' if options[:deep]
68
84
  xpath += "#{namespace}:" if namespace
69
85
  xpath += tag
70
- # puts "xpath: #{xpath}"
86
+ #puts "xpath: #{xpath}"
71
87
  xpath
72
88
  end
73
-
89
+
90
+ # @return [Boolean] true if the type defined for the item is defined in the
91
+ # list of primite types {Types}.
74
92
  def primitive?
75
93
  Types.include?(constant)
76
94
  end
@@ -91,6 +109,17 @@ module HappyMapper
91
109
  @method_name ||= name.tr('-', '_')
92
110
  end
93
111
 
112
+ #
113
+ # When the type of the item is a primitive type, this will convert value specifed
114
+ # to the particular primitive type. If it fails during this process it will
115
+ # return the original String value.
116
+ #
117
+ # @param [String] value the string value parsed from the XML value that will
118
+ # be converted to the particular primitive type.
119
+ #
120
+ # @return [String,Float,Time,Date,DateTime,Boolean,Integer] the converted value
121
+ # to the new type.
122
+ #
94
123
  def typecast(value)
95
124
  return value if value.kind_of?(constant) || value.nil?
96
125
  begin
@@ -123,6 +152,14 @@ module HappyMapper
123
152
 
124
153
  private
125
154
 
155
+ #
156
+ # Convert any String defined types into their constant version so that
157
+ # the method #parse or the custom defined parser method would be used.
158
+ #
159
+ # @param [String,Constant] type is the name of the class or the constant
160
+ # for the class.
161
+ # @return [Constant] the constant of the type
162
+ #
126
163
  def constantize(type)
127
164
  if type.is_a?(String)
128
165
  names = type.split('::')
@@ -152,7 +189,14 @@ module HappyMapper
152
189
 
153
190
  if element?
154
191
  if options[:single]
155
- result = node.xpath(xpath(namespace), xpath_options)
192
+
193
+ result = nil
194
+
195
+ if options[:xpath]
196
+ result = node.xpath(options[:xpath], xpath_options)
197
+ else
198
+ result = node.xpath(xpath(namespace), xpath_options)
199
+ end
156
200
 
157
201
  if result
158
202
  value = options[:single] ? yield(result.first) : result.map {|r| yield r }
@@ -161,7 +205,10 @@ module HappyMapper
161
205
  value
162
206
  end
163
207
  else
164
- results = node.xpath(xpath(namespace), xpath_options).collect do |result|
208
+
209
+ target_path = options[:xpath] ? options[:xpath] : xpath(namespace)
210
+
211
+ results = node.xpath(target_path, xpath_options).collect do |result|
165
212
  value = yield(result)
166
213
  handle_attributes_option(result, value, xpath_options)
167
214
  value
@@ -169,7 +216,13 @@ module HappyMapper
169
216
  results
170
217
  end
171
218
  elsif attribute?
172
- yield(node[tag])
219
+
220
+ if options[:xpath]
221
+ yield(node.xpath(options[:xpath],xpath_options))
222
+ else
223
+ yield(node[tag])
224
+ end
225
+
173
226
  else # text node
174
227
  yield(node.children.detect{|c| c.text?})
175
228
  end
@@ -177,7 +230,7 @@ module HappyMapper
177
230
 
178
231
  def handle_attributes_option(result, value, xpath_options)
179
232
  if options[:attributes].is_a?(Hash)
180
- result = result.first if result.respond_to?(:first)
233
+ result = result.first unless result.respond_to?(:attribute_nodes)
181
234
 
182
235
  result.attribute_nodes.each do |xml_attribute|
183
236
  if attribute_options = options[:attributes][xml_attribute.name.to_sym]
@@ -18,5 +18,5 @@
18
18
  <item>
19
19
  <name>Other item</name>
20
20
  </item>
21
- <others-items>
21
+ </others-items>
22
22
  </ambigous>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
3
+ <id>tag:www.example.com,2005:/tv_shows</id>
4
+ <link rel="alternate" type="text/html" href="http://www.example.com"/>
5
+ <link rel="self" type="application/atom+xml" href="http://www.example.com/tv_shows.atom"/>
6
+ <title>TV Shows</title>
7
+ <updated>2011-07-08T13:47:01Z</updated>
8
+ <entry>
9
+ <id>tag:www.example.com,2005:TvShow/17</id>
10
+ <published>2011-07-08T13:47:01Z</published>
11
+ <updated>2011-07-08T13:47:01Z</updated>
12
+ <link rel="alternate" type="text/html" href="http://www.example.com/sources/channel-twenty-seven/tv_shows/name-goes-here.atom"/>
13
+ <title>Name goes here</title>
14
+ <content type="html">Name goes here (0 episodes)</content>
15
+ <author>
16
+ <name>Source URL goes here</name>
17
+ </author>
18
+ </entry>
19
+ </feed>
@@ -0,0 +1,85 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+ <CatalogTree code="NLD" xmlns="urn:eventis:prodis:onlineapi:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
3
+ <Node vodBackOfficeId="69fcb5eb-1020-4053-8fce-6bce030cac5b">
4
+ <Name>Parent 1</Name>
5
+ <Description>
6
+ 111#111#120#NPO-BrowseMode.jpg#NPO-ExtendedMode.jpg
7
+ </Description>
8
+ <Translations>
9
+ <Translation Language="en-GB">
10
+ <Name>Parent 1 en</Name>
11
+ </Translation>
12
+ <Translation Language="de-DE">
13
+ <Name>Parent 1 de</Name>
14
+ </Translation>
15
+ </Translations>
16
+ <Rating>0</Rating>
17
+ <Adult>false</Adult>
18
+ <CatalogCode>NLD</CatalogCode>
19
+ <Position>1</Position>
20
+ <SubNodes>
21
+ <Node vodBackOfficeId="27feedad-dead-4721-ba61-c3f9f17d0956">
22
+ <Name>First</Name>
23
+ <Translations>
24
+ <Translation Language="en-GB">
25
+ <Name>First en</Name>
26
+ </Translation>
27
+ </Translations>
28
+ <Rating>0</Rating>
29
+ <Adult>false</Adult>
30
+ <CatalogCode>NLD</CatalogCode>
31
+ <ParentId>69fcb5eb-1020-4053-8fce-6bce030cac5b</ParentId>
32
+ <Position>0</Position>
33
+ <SubNodes>
34
+ <Node vodBackOfficeId="7edbcc13-2d03-4254-aaf6-307ba553d4fd">
35
+ <Name>Second</Name>
36
+ <Translations>
37
+ <Translation Language="en-GB">
38
+ <Name>Second en</Name>
39
+ </Translation>
40
+ </Translations>
41
+ <Rating>0</Rating>
42
+ <Adult>false</Adult>
43
+ <CatalogCode>NLD</CatalogCode>
44
+ <ParentId>b17dd22b-c028-44ad-8e75-e676e2619ceb</ParentId>
45
+ <Position>0</Position>
46
+ <SubNodes>
47
+ <Node vodBackOfficeId="27feedad-dead-4721-ba61-c3f9f17d1111">
48
+ <Name>Third</Name>
49
+ <Translations>
50
+ <Translation Language="en-GB">
51
+ <Name>Third en</Name>
52
+ </Translation>
53
+ </Translations>
54
+ <Rating>0</Rating>
55
+ <Adult>false</Adult>
56
+ <CatalogCode>NLD</CatalogCode>
57
+ <ParentId>69fcb5eb-1020-4053-8fce-6bce030cac5b</ParentId>
58
+ <Position>0</Position>
59
+ </Node>
60
+ </SubNodes>
61
+ </Node>
62
+ </SubNodes>
63
+ </Node>
64
+ </SubNodes>
65
+ </Node>
66
+ <Node vodBackOfficeId="69fcb5eb-1020-4053-8fce-12312312312">
67
+ <Name>Parent 2</Name>
68
+ <Description>
69
+ 111#111#120#NPO-BrowseMode.jpg#NPO-ExtendedMode.jpg
70
+ </Description>
71
+ <Translations>
72
+ <Translation Language="en-GB">
73
+ <Name>Parent 2 en</Name>
74
+ </Translation>
75
+ <Translation Language="de-DE">
76
+ <Name>Parent 2 de</Name>
77
+ </Translation>
78
+ </Translations>
79
+ <Rating>0</Rating>
80
+ <Adult>false</Adult>
81
+ <CatalogCode>NLD</CatalogCode>
82
+ <Position>1</Position>
83
+ <SubNodes/>
84
+ </Node>
85
+ </CatalogTree>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <addresses>
3
+ <address street=""/>
4
+ <address street="Milchstrasse"/>
5
+ <address />
6
+ </addresses>
@@ -0,0 +1,50 @@
1
+ <article:Article xmlns:article="http://www.wetpaint.com/alfresco/article"
2
+ xmlns:photo="http://www.wetpaint.com/alfresco/photo"
3
+ xmlns:gallery="http://www.wetpaint.com/alfresco/gallery"
4
+ xmlns:alf="http://www.alfresco.org"
5
+ alf:name="title">
6
+ <article:title>article title</article:title>
7
+ <article:text>article text</article:text>
8
+ <article:publishOptions>
9
+ <article:author>Nathan</article:author>
10
+ <article:draft>false</article:draft>
11
+ <article:scheduledDay>2011-01-14</article:scheduledDay>
12
+ <article:scheduledTime>11:31:45.0</article:scheduledTime>
13
+ <article:publishDisplayDay>2011-01-14</article:publishDisplayDay>
14
+ <article:publishDisplayTime>11:31:45.0</article:publishDisplayTime>
15
+ <article:createdDay>2011-01-14</article:createdDay>
16
+ <article:createdTime>11:52:24.0</article:createdTime>
17
+ </article:publishOptions>
18
+
19
+ <photo:Photo>
20
+ <photo:title>photo title</photo:title>
21
+ <photo:publishOptions>
22
+ <photo:author>Stephanie</photo:author>
23
+ <photo:draft>false</photo:draft>
24
+ <photo:scheduledDay>2011-01-13</photo:scheduledDay>
25
+ <photo:scheduledTime>15:47:30.0</photo:scheduledTime>
26
+ <photo:publishDisplayDay>2011-01-13</photo:publishDisplayDay>
27
+ <photo:publishDisplayTime>15:47:30.0</photo:publishDisplayTime>
28
+ <photo:createdDay>2011-01-13</photo:createdDay>
29
+ <photo:createdTime>15:51:47.0</photo:createdTime>
30
+ </photo:publishOptions>
31
+ </photo:Photo>
32
+
33
+ <gallery:Gallery>
34
+ <gallery:title>gallery title</gallery:title>
35
+ <photo:Photo>
36
+ <photo:title>photo title</photo:title>
37
+ <photo:publishOptions>
38
+ <photo:author>Stephanie</photo:author>
39
+ <photo:draft>false</photo:draft>
40
+ <photo:scheduledDay>2011-01-13</photo:scheduledDay>
41
+ <photo:scheduledTime>15:47:30.0</photo:scheduledTime>
42
+ <photo:publishDisplayDay>2011-01-13</photo:publishDisplayDay>
43
+ <photo:publishDisplayTime>15:47:30.0</photo:publishDisplayTime>
44
+ <photo:createdDay>2011-01-13</photo:createdDay>
45
+ <photo:createdTime>15:51:47.0</photo:createdTime>
46
+ </photo:publishOptions>
47
+ </photo:Photo>
48
+ </gallery:Gallery>
49
+
50
+ </article:Article>
@@ -69,6 +69,24 @@ module Analytics
69
69
  end
70
70
  end
71
71
 
72
+ module Atom
73
+ class Feed
74
+ include HappyMapper
75
+ tag 'feed'
76
+
77
+ attribute :xmlns, String, :single => true
78
+ element :id, String, :single => true
79
+ element :title, String, :single => true
80
+ element :updated, DateTime, :single => true
81
+ element :link, String, :single => false, :attributes => {
82
+ :rel => String,
83
+ :type => String,
84
+ :href => String
85
+ }
86
+ # has_many :entries, Entry # nothing interesting in the entries
87
+ end
88
+ end
89
+
72
90
  class Address
73
91
  include HappyMapper
74
92
 
@@ -82,7 +100,7 @@ end
82
100
 
83
101
  class Feature
84
102
  include HappyMapper
85
- element :name, String, :tag => '.|.//text()'
103
+ element :name, String, :xpath => './/text()'
86
104
  end
87
105
 
88
106
  class FeatureBullet
@@ -286,7 +304,7 @@ class Country
286
304
  include HappyMapper
287
305
 
288
306
  attribute :code, String
289
- text_node :name, String
307
+ content :name, String
290
308
  end
291
309
 
292
310
  class Address
@@ -423,6 +441,103 @@ module AmbigousItems
423
441
  end
424
442
  end
425
443
 
444
+ class PublishOptions
445
+ include HappyMapper
446
+
447
+ tag 'publishOptions'
448
+
449
+ element :author, String, :tag => 'author'
450
+
451
+ element :draft, Boolean, :tag => 'draft'
452
+ element :scheduled_day, String, :tag => 'scheduledDay'
453
+ element :scheduled_time, String, :tag => 'scheduledTime'
454
+ element :published_day, String, :tag => 'publishDisplayDay'
455
+ element :published_time, String, :tag => 'publishDisplayTime'
456
+ element :created_day, String, :tag => 'publishDisplayDay'
457
+ element :created_time, String, :tag => 'publishDisplayTime'
458
+
459
+ end
460
+
461
+ class Article
462
+ include HappyMapper
463
+
464
+ tag 'Article'
465
+ namespace 'article'
466
+
467
+ attr_writer :xml_value
468
+
469
+ element :title, String
470
+ element :text, String
471
+ has_many :photos, 'Photo', :tag => 'Photo', :namespace => 'photo', :xpath => '/article:Article'
472
+ has_many :galleries, 'Gallery', :tag => 'Gallery', :namespace => 'gallery'
473
+
474
+ element :publish_options, PublishOptions, :tag => 'publishOptions', :namespace => 'article'
475
+
476
+ end
477
+
478
+ class PartiallyBadArticle
479
+ include HappyMapper
480
+
481
+ attr_writer :xml_value
482
+
483
+ tag 'Article'
484
+ namespace 'article'
485
+
486
+ element :title, String
487
+ element :text, String
488
+ has_many :photos, 'Photo', :tag => 'Photo', :namespace => 'photo', :xpath => '/article:Article'
489
+ has_many :videos, 'Video', :tag => 'Video', :namespace => 'video'
490
+
491
+ element :publish_options, PublishOptions, :tag => 'publishOptions', :namespace => 'article'
492
+
493
+ end
494
+
495
+ class Photo
496
+ include HappyMapper
497
+
498
+ tag 'Photo'
499
+ namespace 'photo'
500
+
501
+ attr_writer :xml_value
502
+
503
+ element :title, String
504
+ element :publish_options, PublishOptions, :tag => 'publishOptions', :namespace => 'photo'
505
+
506
+ end
507
+
508
+ class Gallery
509
+ include HappyMapper
510
+
511
+ tag 'Gallery'
512
+ namespace 'gallery'
513
+
514
+ attr_writer :xml_value
515
+
516
+ element :title, String
517
+
518
+ end
519
+
520
+ class Video
521
+ include HappyMapper
522
+
523
+ tag 'Video'
524
+ namespace 'video'
525
+
526
+ attr_writer :xml_value
527
+
528
+ element :title, String
529
+ element :publish_options, PublishOptions, :tag => 'publishOptions', :namespace => 'video'
530
+
531
+ end
532
+
533
+ class OptionalAttribute
534
+ include HappyMapper
535
+ tag 'address'
536
+
537
+ attribute :street, String
538
+ end
539
+
540
+
426
541
  describe HappyMapper do
427
542
 
428
543
  describe "being included into another class" do
@@ -486,7 +601,7 @@ describe HappyMapper do
486
601
  element = @klass.elements.first
487
602
  element.name.should == 'user'
488
603
  element.type.should == User
489
- element.options[:single] = true
604
+ element.options[:single].should == true
490
605
  end
491
606
 
492
607
  it "should allow has many association" do
@@ -494,7 +609,7 @@ describe HappyMapper do
494
609
  element = @klass.elements.first
495
610
  element.name.should == 'users'
496
611
  element.type.should == User
497
- element.options[:single] = false
612
+ element.options[:single].should == false
498
613
  end
499
614
 
500
615
  it "should default tag name to lowercase class" do
@@ -585,6 +700,12 @@ describe HappyMapper do
585
700
  address.country.code.should == 'de'
586
701
  end
587
702
 
703
+ it "should treat Nokogiri::XML::Document as root" do
704
+ doc = Nokogiri::XML(fixture_file('address.xml'))
705
+ address = Address.parse(doc)
706
+ address.class.should == Address
707
+ end
708
+
588
709
  it "should parse xml with default namespace (amazon)" do
589
710
  file_contents = fixture_file('pita.xml')
590
711
  items = PITA::Items.parse(file_contents, :single => true)
@@ -611,6 +732,23 @@ describe HappyMapper do
611
732
  first.current_condition.icon.should == 'http://deskwx.weatherbug.com/images/Forecast/icons/cond007.gif'
612
733
  end
613
734
 
735
+ it "parses xml with attributes of elements that aren't :single => true" do
736
+ feed = Atom::Feed.parse(fixture_file('atom.xml'))
737
+ feed.link.first.href.should == 'http://www.example.com'
738
+ feed.link.last.href.should == 'http://www.example.com/tv_shows.atom'
739
+ end
740
+
741
+ it "returns nil rather than empty array for absent values when :single => true" do
742
+ address = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', :single => true)
743
+ address.should be_nil
744
+ end
745
+
746
+ it "should return same result for absent values when :single => true, regardless of :in_groups_of" do
747
+ addr1 = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', :single => true)
748
+ addr2 = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', :single => true, :in_groups_of => 10)
749
+ addr1.should == addr2
750
+ end
751
+
614
752
  it "should parse xml with nested elements" do
615
753
  radars = Radar.parse(fixture_file('radar.xml'))
616
754
  first = radars[0]
@@ -768,6 +906,25 @@ describe HappyMapper do
768
906
  l = Location.parse(fixture_file('lastfm.xml'))
769
907
  l.first.latitude.should == "51.53469"
770
908
  end
909
+
910
+ describe "Parse optional attributes" do
911
+
912
+ it "should parse an empty String as empty" do
913
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
914
+ a[0].street.should == ""
915
+ end
916
+
917
+ it "should parse a String with value" do
918
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
919
+ a[1].street.should == "Milchstrasse"
920
+ end
921
+
922
+ it "should parse a String with value" do
923
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
924
+ a[2].street.should be_nil
925
+ end
926
+
927
+ end
771
928
 
772
929
  describe 'Xml Content' do
773
930
  before(:each) do
@@ -789,8 +946,65 @@ describe HappyMapper do
789
946
  end
790
947
 
791
948
  it "should parse ambigous items" do
792
- items = AmbigousItems::Item.parse(fixture_file('ambigous_items.xml'),
793
- :xpath => '/ambigous/my-items')
949
+ items = AmbigousItems::Item.parse(fixture_file('ambigous_items.xml'), :xpath => '/ambigous/my-items')
794
950
  items.map(&:name).should == %w(first second third).map{|s| "My #{s} item" }
795
951
  end
952
+
953
+
954
+ context Article do
955
+ it "should parse the publish options for Article and Photo" do
956
+ @article.title.should_not be_nil
957
+ @article.text.should_not be_nil
958
+ @article.photos.should_not be_nil
959
+ @article.photos.first.title.should_not be_nil
960
+ end
961
+
962
+ it "should parse the publish options for Article" do
963
+ @article.publish_options.should_not be_nil
964
+ end
965
+
966
+ it "should parse the publish options for Photo" do
967
+ @article.photos.first.publish_options.should_not be_nil
968
+ end
969
+
970
+ it "should only find only items at the parent level" do
971
+ @article.photos.length.should == 1
972
+ end
973
+
974
+ before(:all) do
975
+ @article = Article.parse(fixture_file('subclass_namespace.xml'))
976
+ end
977
+
978
+ end
979
+
980
+ context "Namespace is missing because an optional element that uses it is not present" do
981
+ it "should parse successfully" do
982
+ @article = PartiallyBadArticle.parse(fixture_file('subclass_namespace.xml'))
983
+ @article.should_not be_nil
984
+ @article.title.should_not be_nil
985
+ @article.text.should_not be_nil
986
+ @article.photos.should_not be_nil
987
+ @article.photos.first.title.should_not be_nil
988
+ end
989
+ end
990
+
991
+
992
+ describe "with limit option" do
993
+ it "should return results with limited size: 6" do
994
+ sizes = []
995
+ posts = Post.parse(fixture_file('posts.xml'), :in_groups_of => 6) do |a|
996
+ sizes << a.size
997
+ end
998
+ sizes.should == [6, 6, 6, 2]
999
+ end
1000
+
1001
+ it "should return results with limited size: 10" do
1002
+ sizes = []
1003
+ posts = Post.parse(fixture_file('posts.xml'), :in_groups_of => 10) do |a|
1004
+ sizes << a.size
1005
+ end
1006
+ sizes.should == [10, 10]
1007
+ end
1008
+ end
1009
+
796
1010
  end