nokogiri-happymapper 0.3.6 → 0.5.1

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