blockscore-happymapper 0.6.0

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +605 -0
  4. data/lib/happymapper.rb +754 -0
  5. data/lib/happymapper/anonymous_mapper.rb +107 -0
  6. data/lib/happymapper/attribute.rb +21 -0
  7. data/lib/happymapper/element.rb +53 -0
  8. data/lib/happymapper/item.rb +156 -0
  9. data/lib/happymapper/supported_types.rb +137 -0
  10. data/lib/happymapper/text_node.rb +7 -0
  11. data/lib/happymapper/version.rb +3 -0
  12. data/spec/attribute_default_value_spec.rb +47 -0
  13. data/spec/attributes_spec.rb +33 -0
  14. data/spec/fixtures/address.xml +9 -0
  15. data/spec/fixtures/ambigous_items.xml +22 -0
  16. data/spec/fixtures/analytics.xml +61 -0
  17. data/spec/fixtures/analytics_profile.xml +127 -0
  18. data/spec/fixtures/atom.xml +19 -0
  19. data/spec/fixtures/commit.xml +52 -0
  20. data/spec/fixtures/current_weather.xml +89 -0
  21. data/spec/fixtures/current_weather_missing_elements.xml +18 -0
  22. data/spec/fixtures/default_namespace_combi.xml +6 -0
  23. data/spec/fixtures/dictionary.xml +20 -0
  24. data/spec/fixtures/family_tree.xml +21 -0
  25. data/spec/fixtures/inagy.xml +85 -0
  26. data/spec/fixtures/lastfm.xml +355 -0
  27. data/spec/fixtures/multiple_namespaces.xml +170 -0
  28. data/spec/fixtures/multiple_primitives.xml +5 -0
  29. data/spec/fixtures/optional_attributes.xml +6 -0
  30. data/spec/fixtures/pita.xml +133 -0
  31. data/spec/fixtures/posts.xml +23 -0
  32. data/spec/fixtures/product_default_namespace.xml +18 -0
  33. data/spec/fixtures/product_no_namespace.xml +10 -0
  34. data/spec/fixtures/product_single_namespace.xml +10 -0
  35. data/spec/fixtures/quarters.xml +19 -0
  36. data/spec/fixtures/radar.xml +21 -0
  37. data/spec/fixtures/set_config_options.xml +3 -0
  38. data/spec/fixtures/statuses.xml +422 -0
  39. data/spec/fixtures/subclass_namespace.xml +50 -0
  40. data/spec/fixtures/wrapper.xml +11 -0
  41. data/spec/happymapper/attribute_spec.rb +10 -0
  42. data/spec/happymapper/element_spec.rb +9 -0
  43. data/spec/happymapper/item_spec.rb +114 -0
  44. data/spec/happymapper/text_node_spec.rb +9 -0
  45. data/spec/happymapper_parse_spec.rb +99 -0
  46. data/spec/happymapper_spec.rb +1096 -0
  47. data/spec/has_many_empty_array_spec.rb +42 -0
  48. data/spec/ignay_spec.rb +87 -0
  49. data/spec/inheritance_spec.rb +105 -0
  50. data/spec/mixed_namespaces_spec.rb +58 -0
  51. data/spec/parse_with_object_to_update_spec.rb +108 -0
  52. data/spec/spec_helper.rb +8 -0
  53. data/spec/to_xml_spec.rb +197 -0
  54. data/spec/to_xml_with_namespaces_spec.rb +223 -0
  55. data/spec/wilcard_tag_name_spec.rb +94 -0
  56. data/spec/wrap_spec.rb +80 -0
  57. data/spec/xpath_spec.rb +86 -0
  58. metadata +183 -0
@@ -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>
@@ -0,0 +1,11 @@
1
+ <root attr1="somevalue">
2
+ <name>myname</name>
3
+ <mywraptag>
4
+ <description>some description</description>
5
+ <subclass myattr='attrvalue'>
6
+ <item>item1</item>
7
+ <item>item2</item>
8
+ </subclass>
9
+ </mywraptag>
10
+ <number>12345</number>
11
+ </root>
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe HappyMapper::Attribute do
4
+ describe 'initialization' do
5
+ it 'accepts :default option' do
6
+ attr = described_class.new(:foo, String, default: 'foobar')
7
+ attr.default.should == 'foobar'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe HappyMapper::Element do
4
+ describe 'initialization' do
5
+ before do
6
+ @attr = HappyMapper::Element.new(:foo, String)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ module Foo
4
+ class Bar; end
5
+ end
6
+
7
+ describe HappyMapper::Item do
8
+ describe 'new instance' do
9
+ before do
10
+ @item = HappyMapper::Item.new(:foo, String, tag: 'foobar')
11
+ end
12
+
13
+ it 'should accept a name' do
14
+ @item.name.should == 'foo'
15
+ end
16
+
17
+ it 'should accept a type' do
18
+ @item.type.should == String
19
+ end
20
+
21
+ it 'should accept :tag as an option' do
22
+ @item.tag.should == 'foobar'
23
+ end
24
+
25
+ it 'should have a method_name' do
26
+ @item.method_name.should == 'foo'
27
+ end
28
+ end
29
+
30
+ describe '#constant' do
31
+ it 'should just use type if constant' do
32
+ item = HappyMapper::Item.new(:foo, String)
33
+ item.constant.should == String
34
+ end
35
+
36
+ it 'should convert string type to constant' do
37
+ item = HappyMapper::Item.new(:foo, 'String')
38
+ item.constant.should == String
39
+ end
40
+
41
+ it 'should convert string with :: to constant' do
42
+ item = HappyMapper::Item.new(:foo, 'Foo::Bar')
43
+ item.constant.should == Foo::Bar
44
+ end
45
+ end
46
+
47
+ describe '#method_name' do
48
+ it 'should convert dashes to underscores' do
49
+ item = HappyMapper::Item.new(:'foo-bar', String, tag: 'foobar')
50
+ item.method_name.should == 'foo_bar'
51
+ end
52
+ end
53
+
54
+ describe '#xpath' do
55
+ it 'should default to tag' do
56
+ item = HappyMapper::Item.new(:foo, String, tag: 'foobar')
57
+ item.xpath.should == 'foobar'
58
+ end
59
+
60
+ it 'should prepend with .// if options[:deep] true' do
61
+ item = HappyMapper::Item.new(:foo, String, tag: 'foobar', deep: true)
62
+ item.xpath.should == './/foobar'
63
+ end
64
+
65
+ it 'should prepend namespace if namespace exists' do
66
+ item = HappyMapper::Item.new(:foo, String, tag: 'foobar')
67
+ item.namespace = 'v2'
68
+ item.xpath.should == 'v2:foobar'
69
+ end
70
+ end
71
+
72
+ describe 'typecasting' do
73
+ it 'should work with Strings' do
74
+ item = HappyMapper::Item.new(:foo, String)
75
+ [21, '21'].each do |a|
76
+ item.typecast(a).should == '21'
77
+ end
78
+ end
79
+
80
+ it 'should work with Integers' do
81
+ item = HappyMapper::Item.new(:foo, Integer)
82
+ [21, 21.0, '21'].each do |a|
83
+ item.typecast(a).should == 21
84
+ end
85
+ end
86
+
87
+ it 'should work with Floats' do
88
+ item = HappyMapper::Item.new(:foo, Float)
89
+ [21, 21.0, '21'].each do |a|
90
+ item.typecast(a).should == 21.0
91
+ end
92
+ end
93
+
94
+ it 'should work with Times' do
95
+ item = HappyMapper::Item.new(:foo, Time)
96
+ item.typecast('2000-01-01 01:01:01.123456').should == Time.local(2000, 1, 1, 1, 1, 1, 123_456)
97
+ end
98
+
99
+ it 'should work with Dates' do
100
+ item = HappyMapper::Item.new(:foo, Date)
101
+ item.typecast('2000-01-01').should == Date.new(2000, 1, 1)
102
+ end
103
+
104
+ it 'should work with DateTimes' do
105
+ item = HappyMapper::Item.new(:foo, DateTime)
106
+ item.typecast('2000-01-01 00:00:00').should == DateTime.new(2000, 1, 1, 0, 0, 0)
107
+ end
108
+
109
+ it 'should work with Boolean' do
110
+ item = HappyMapper::Item.new(:foo, HappyMapper::Boolean)
111
+ item.typecast('false').should == false
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe HappyMapper::Attribute do
4
+ describe 'initialization' do
5
+ before do
6
+ @attr = HappyMapper::TextNode.new(:foo, String)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe HappyMapper do
4
+ context '.parse' do
5
+ context 'on a single root node' do
6
+ subject { described_class.parse fixture_file('address.xml') }
7
+
8
+ it 'should parse child elements' do
9
+ subject.street.should == 'Milchstrasse'
10
+ subject.housenumber.should == '23'
11
+ subject.postcode.should == '26131'
12
+ subject.city.should == 'Oldenburg'
13
+ end
14
+
15
+ it 'should not create a content entry when the xml contents no text content' do
16
+ subject.should_not respond_to :content
17
+ end
18
+
19
+ context 'child elements with attributes' do
20
+ it 'should parse the attributes' do
21
+ subject.country.code.should == 'de'
22
+ end
23
+
24
+ it 'should parse the content' do
25
+ subject.country.content.should == 'Germany'
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'element names with special characters' do
31
+ subject { described_class.parse fixture_file('ambigous_items.xml') }
32
+
33
+ it 'should create accessor methods with similar names' do
34
+ subject.my_items.item.should be_kind_of Array
35
+ end
36
+ end
37
+
38
+ context 'element names with camelCased elements and Capital Letters' do
39
+ subject { described_class.parse fixture_file('subclass_namespace.xml') }
40
+
41
+ it 'should parse the elements and values correctly' do
42
+ subject.title.should == 'article title'
43
+ subject.photo.publish_options.author.should == 'Stephanie'
44
+ subject.gallery.photo.title.should == 'photo title'
45
+ end
46
+ end
47
+
48
+ context 'several elements nested deep' do
49
+ subject { described_class.parse fixture_file('ambigous_items.xml') }
50
+
51
+ it 'should parse the entire relationship' do
52
+ subject.my_items.item.first.item.name.should == 'My first internal item'
53
+ end
54
+ end
55
+
56
+ context 'xml that contains multiple entries' do
57
+ subject { described_class.parse fixture_file('multiple_primitives.xml') }
58
+
59
+ it "should parse the elements as it would a 'has_many'" do
60
+ subject.name.should == 'value'
61
+ subject.image.should == %w(image1 image2)
62
+ end
63
+ end
64
+
65
+ context 'xml with multiple namespaces' do
66
+ subject { described_class.parse fixture_file('subclass_namespace.xml') }
67
+
68
+ it 'should parse the elements an values correctly' do
69
+ subject.title.should == 'article title'
70
+ end
71
+ end
72
+
73
+ context 'after_parse callbacks' do
74
+ module AfterParseSpec
75
+ class Address
76
+ include HappyMapper
77
+ element :street, String
78
+ end
79
+ end
80
+
81
+ after do
82
+ AfterParseSpec::Address.after_parse_callbacks.clear
83
+ end
84
+
85
+ it 'should callback with the newly created object' do
86
+ from_cb = nil
87
+ called = false
88
+ cb1 = proc { |object| from_cb = object }
89
+ cb2 = proc { called = true }
90
+ AfterParseSpec::Address.after_parse(&cb1)
91
+ AfterParseSpec::Address.after_parse(&cb2)
92
+
93
+ object = AfterParseSpec::Address.parse fixture_file('address.xml')
94
+ from_cb.should == object
95
+ called.should == true
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,1096 @@
1
+ require 'spec_helper'
2
+ require 'uri'
3
+
4
+ module Analytics
5
+ class Property
6
+ include HappyMapper
7
+
8
+ tag 'property'
9
+ namespace 'dxp'
10
+ attribute :name, String
11
+ attribute :value, String
12
+ end
13
+
14
+ class Goal
15
+ include HappyMapper
16
+
17
+ # Google Analytics does a dirtry trick where a user with no goals
18
+ # returns a profile without any goals data or the declared namespace
19
+ # which means Nokogiri does not pick up the namespace automatically.
20
+ # To fix this, we manually register the namespace to avoid bad XPath
21
+ # expression. Dirty, but works.
22
+
23
+ register_namespace 'ga', 'http://schemas.google.com/ga/2009'
24
+ namespace 'ga'
25
+
26
+ tag 'goal'
27
+ attribute :active, Boolean
28
+ attribute :name, String
29
+ attribute :number, Integer
30
+ attribute :value, Float
31
+
32
+ def clean_name
33
+ name.gsub(/ga:/, '')
34
+ end
35
+ end
36
+
37
+ class Profile
38
+ include HappyMapper
39
+
40
+ tag 'entry'
41
+ element :title, String
42
+ element :tableId, String, namespace: 'dxp'
43
+
44
+ has_many :properties, Property
45
+ has_many :goals, Goal
46
+ end
47
+
48
+ class Entry
49
+ include HappyMapper
50
+
51
+ tag 'entry'
52
+ element :id, String
53
+ element :updated, DateTime
54
+ element :title, String
55
+ element :table_id, String, namespace: 'dxp', tag: 'tableId'
56
+ has_many :properties, Property
57
+ end
58
+
59
+ class Feed
60
+ include HappyMapper
61
+
62
+ tag 'feed'
63
+ element :id, String
64
+ element :updated, DateTime
65
+ element :title, String
66
+ has_many :entries, Entry
67
+ end
68
+ end
69
+
70
+ module Atom
71
+ class Feed
72
+ include HappyMapper
73
+ tag 'feed'
74
+
75
+ attribute :xmlns, String, single: true
76
+ element :id, String, single: true
77
+ element :title, String, single: true
78
+ element :updated, DateTime, single: true
79
+ element :link, String, single: false, attributes: {
80
+ rel: String,
81
+ type: String,
82
+ href: String
83
+ }
84
+ # has_many :entries, Entry # nothing interesting in the entries
85
+ end
86
+ end
87
+
88
+ class Address
89
+ include HappyMapper
90
+
91
+ tag 'address'
92
+ element :street, String
93
+ element :postcode, String
94
+ element :housenumber, String
95
+ element :city, String
96
+ element :country, String
97
+ end
98
+
99
+ class Feature
100
+ include HappyMapper
101
+ element :name, String, xpath: './/text()'
102
+ end
103
+
104
+ class FeatureBullet
105
+ include HappyMapper
106
+
107
+ tag 'features_bullets'
108
+ has_many :features, Feature
109
+ element :bug, String
110
+ end
111
+
112
+ class Product
113
+ include HappyMapper
114
+
115
+ element :title, String
116
+ has_one :feature_bullets, FeatureBullet
117
+ has_one :address, Address
118
+ end
119
+
120
+ class Rate
121
+ include HappyMapper
122
+ end
123
+
124
+ module FamilySearch
125
+ class AlternateIds
126
+ include HappyMapper
127
+
128
+ tag 'alternateIds'
129
+ has_many :ids, String, tag: 'id'
130
+ end
131
+
132
+ class Information
133
+ include HappyMapper
134
+
135
+ has_one :alternateIds, AlternateIds
136
+ end
137
+
138
+ class Person
139
+ include HappyMapper
140
+
141
+ attribute :version, String
142
+ attribute :modified, Time
143
+ attribute :id, String
144
+ has_one :information, Information
145
+ end
146
+
147
+ class Persons
148
+ include HappyMapper
149
+ has_many :person, Person
150
+ end
151
+
152
+ class FamilyTree
153
+ include HappyMapper
154
+
155
+ tag 'familytree'
156
+ attribute :version, String
157
+ attribute :status_message, String, tag: 'statusMessage'
158
+ attribute :status_code, String, tag: 'statusCode'
159
+ has_one :persons, Persons
160
+ end
161
+ end
162
+
163
+ module FedEx
164
+ class Address
165
+ include HappyMapper
166
+
167
+ tag 'Address'
168
+ namespace 'v2'
169
+ element :city, String, tag: 'City'
170
+ element :state, String, tag: 'StateOrProvinceCode'
171
+ element :zip, String, tag: 'PostalCode'
172
+ element :countrycode, String, tag: 'CountryCode'
173
+ element :residential, Boolean, tag: 'Residential'
174
+ end
175
+
176
+ class Event
177
+ include HappyMapper
178
+
179
+ tag 'Events'
180
+ namespace 'v2'
181
+ element :timestamp, String, tag: 'Timestamp'
182
+ element :eventtype, String, tag: 'EventType'
183
+ element :eventdescription, String, tag: 'EventDescription'
184
+ has_one :address, Address
185
+ end
186
+
187
+ class PackageWeight
188
+ include HappyMapper
189
+
190
+ tag 'PackageWeight'
191
+ namespace 'v2'
192
+ element :units, String, tag: 'Units'
193
+ element :value, Integer, tag: 'Value'
194
+ end
195
+
196
+ class TrackDetails
197
+ include HappyMapper
198
+
199
+ tag 'TrackDetails'
200
+ namespace 'v2'
201
+ element :tracking_number, String, tag: 'TrackingNumber'
202
+ element :status_code, String, tag: 'StatusCode'
203
+ element :status_desc, String, tag: 'StatusDescription'
204
+ element :carrier_code, String, tag: 'CarrierCode'
205
+ element :service_info, String, tag: 'ServiceInfo'
206
+ has_one :weight, PackageWeight, tag: 'PackageWeight'
207
+ element :est_delivery, String, tag: 'EstimatedDeliveryTimestamp'
208
+ has_many :events, Event
209
+ end
210
+
211
+ class Notification
212
+ include HappyMapper
213
+
214
+ tag 'Notifications'
215
+ namespace 'v2'
216
+ element :severity, String, tag: 'Severity'
217
+ element :source, String, tag: 'Source'
218
+ element :code, Integer, tag: 'Code'
219
+ element :message, String, tag: 'Message'
220
+ element :localized_message, String, tag: 'LocalizedMessage'
221
+ end
222
+
223
+ class TransactionDetail
224
+ include HappyMapper
225
+
226
+ tag 'TransactionDetail'
227
+ namespace 'v2'
228
+ element :cust_tran_id, String, tag: 'CustomerTransactionId'
229
+ end
230
+
231
+ class TrackReply
232
+ include HappyMapper
233
+
234
+ tag 'TrackReply'
235
+ namespace 'v2'
236
+ element :highest_severity, String, tag: 'HighestSeverity'
237
+ element :more_data, Boolean, tag: 'MoreData'
238
+ has_many :notifications, Notification, tag: 'Notifications'
239
+ has_many :trackdetails, TrackDetails, tag: 'TrackDetails'
240
+ has_one :tran_detail, TransactionDetail, tab: 'TransactionDetail'
241
+ end
242
+ end
243
+
244
+ class Place
245
+ include HappyMapper
246
+ element :name, String
247
+ end
248
+
249
+ class Radar
250
+ include HappyMapper
251
+ has_many :places, Place, tag: :place
252
+ end
253
+
254
+ class Post
255
+ include HappyMapper
256
+
257
+ attribute :href, String
258
+ attribute :hash, String
259
+ attribute :description, String
260
+ attribute :tag, String
261
+ attribute :time, Time
262
+ attribute :others, Integer
263
+ attribute :extended, String
264
+ end
265
+
266
+ class User
267
+ include HappyMapper
268
+
269
+ element :id, Integer
270
+ element :name, String
271
+ element :screen_name, String
272
+ element :location, String
273
+ element :description, String
274
+ element :profile_image_url, String
275
+ element :url, String
276
+ element :protected, Boolean
277
+ element :followers_count, Integer
278
+ end
279
+
280
+ class Status
281
+ include HappyMapper
282
+
283
+ register_namespace 'fake', 'faka:namespace'
284
+
285
+ element :id, Integer
286
+ element :text, String
287
+ element :created_at, Time
288
+ element :source, String
289
+ element :truncated, Boolean
290
+ element :in_reply_to_status_id, Integer
291
+ element :in_reply_to_user_id, Integer
292
+ element :favorited, Boolean
293
+ element :non_existent, String, tag: 'dummy', namespace: 'fake'
294
+ has_one :user, User
295
+ end
296
+
297
+ class CurrentWeather
298
+ include HappyMapper
299
+
300
+ tag 'ob'
301
+ namespace 'aws'
302
+ element :temperature, Integer, tag: 'temp'
303
+ element :feels_like, Integer, tag: 'feels-like'
304
+ element :current_condition, String, tag: 'current-condition', attributes: { icon: String }
305
+ end
306
+
307
+ class Country
308
+ include HappyMapper
309
+
310
+ attribute :code, String
311
+ content :name, String
312
+ end
313
+
314
+ class State
315
+ include HappyMapper
316
+ end
317
+
318
+ class Address
319
+ include HappyMapper
320
+
321
+ tag 'address'
322
+ element :street, String
323
+ element :postcode, String
324
+ element :housenumber, String
325
+ element :city, String
326
+ has_one :country, Country
327
+ has_one :state, State
328
+ end
329
+
330
+ # for type coercion
331
+ class ProductGroup < String; end
332
+
333
+ module PITA
334
+ class Item
335
+ include HappyMapper
336
+
337
+ tag 'Item' # if you put class in module you need tag
338
+ element :asin, String, tag: 'ASIN'
339
+ element :detail_page_url, URI, tag: 'DetailPageURL', parser: :parse
340
+ element :manufacturer, String, tag: 'Manufacturer', deep: true
341
+ element :point, String, tag: 'point', namespace: 'georss'
342
+ element :product_group, ProductGroup, tag: 'ProductGroup', deep: true, parser: :new, raw: true
343
+ end
344
+
345
+ class Items
346
+ include HappyMapper
347
+
348
+ tag 'Items' # if you put class in module you need tag
349
+ element :total_results, Integer, tag: 'TotalResults'
350
+ element :total_pages, Integer, tag: 'TotalPages'
351
+ has_many :items, Item
352
+ end
353
+ end
354
+
355
+ module GitHub
356
+ class Commit
357
+ include HappyMapper
358
+
359
+ tag 'commit'
360
+ element :url, String
361
+ element :tree, String
362
+ element :message, String
363
+ element :id, String
364
+ element :'committed-date', Date
365
+ end
366
+ end
367
+
368
+ module QuarterTest
369
+ class Quarter
370
+ include HappyMapper
371
+
372
+ element :start, String
373
+ end
374
+
375
+ class Details
376
+ include HappyMapper
377
+
378
+ element :round, Integer
379
+ element :quarter, Integer
380
+ end
381
+
382
+ class Game
383
+ include HappyMapper
384
+
385
+ # in an ideal world, the following elements would all be
386
+ # called 'quarter' with an attribute indicating which quarter
387
+ # it represented, but the refactoring that allows a single class
388
+ # to be used for all these differently named elements is the next
389
+ # best thing
390
+ has_one :details, QuarterTest::Details
391
+ has_one :q1, QuarterTest::Quarter, tag: 'q1'
392
+ has_one :q2, QuarterTest::Quarter, tag: 'q2'
393
+ has_one :q3, QuarterTest::Quarter, tag: 'q3'
394
+ has_one :q4, QuarterTest::Quarter, tag: 'q4'
395
+ end
396
+ end
397
+
398
+ # To check for multiple primitives
399
+ class Artist
400
+ include HappyMapper
401
+
402
+ tag 'artist'
403
+ element :images, String, tag: 'image', single: false
404
+ element :name, String
405
+ end
406
+
407
+ class Location
408
+ include HappyMapper
409
+
410
+ tag 'point'
411
+ namespace 'geo'
412
+ element :latitude, String, tag: 'lat'
413
+ end
414
+
415
+ # Testing the XmlContent type
416
+ module Dictionary
417
+ class Variant
418
+ include HappyMapper
419
+ tag 'var'
420
+ has_xml_content
421
+
422
+ def to_html
423
+ xml_content.gsub('<tag>', '<em>').gsub('</tag>', '</em>')
424
+ end
425
+ end
426
+
427
+ class Definition
428
+ include HappyMapper
429
+
430
+ tag 'def'
431
+ element :text, XmlContent, tag: 'dtext'
432
+ end
433
+
434
+ class Record
435
+ include HappyMapper
436
+
437
+ tag 'record'
438
+ has_many :definitions, Definition
439
+ has_many :variants, Variant, tag: 'var'
440
+ end
441
+ end
442
+
443
+ module AmbigousItems
444
+ class Item
445
+ include HappyMapper
446
+
447
+ tag 'item'
448
+ element :name, String
449
+ element :item, String
450
+ end
451
+ end
452
+
453
+ class PublishOptions
454
+ include HappyMapper
455
+
456
+ tag 'publishOptions'
457
+
458
+ element :author, String, tag: 'author'
459
+
460
+ element :draft, Boolean, tag: 'draft'
461
+ element :scheduled_day, String, tag: 'scheduledDay'
462
+ element :scheduled_time, String, tag: 'scheduledTime'
463
+ element :published_day, String, tag: 'publishDisplayDay'
464
+ element :published_time, String, tag: 'publishDisplayTime'
465
+ element :created_day, String, tag: 'publishDisplayDay'
466
+ element :created_time, String, tag: 'publishDisplayTime'
467
+ end
468
+
469
+ class Article
470
+ include HappyMapper
471
+
472
+ tag 'Article'
473
+ namespace 'article'
474
+
475
+ attr_writer :xml_value
476
+
477
+ element :title, String
478
+ element :text, String
479
+ has_many :photos, 'Photo', tag: 'Photo', namespace: 'photo', xpath: '/article:Article'
480
+ has_many :galleries, 'Gallery', tag: 'Gallery', namespace: 'gallery'
481
+
482
+ element :publish_options, PublishOptions, tag: 'publishOptions', namespace: 'article'
483
+ end
484
+
485
+ class PartiallyBadArticle
486
+ include HappyMapper
487
+
488
+ attr_writer :xml_value
489
+
490
+ tag 'Article'
491
+ namespace 'article'
492
+
493
+ element :title, String
494
+ element :text, String
495
+ has_many :photos, 'Photo', tag: 'Photo', namespace: 'photo', xpath: '/article:Article'
496
+ has_many :videos, 'Video', tag: 'Video', namespace: 'video'
497
+
498
+ element :publish_options, PublishOptions, tag: 'publishOptions', namespace: 'article'
499
+ end
500
+
501
+ class Photo
502
+ include HappyMapper
503
+
504
+ tag 'Photo'
505
+ namespace 'photo'
506
+
507
+ attr_writer :xml_value
508
+
509
+ element :title, String
510
+ element :publish_options, PublishOptions, tag: 'publishOptions', namespace: 'photo'
511
+ end
512
+
513
+ class Gallery
514
+ include HappyMapper
515
+
516
+ tag 'Gallery'
517
+ namespace 'gallery'
518
+
519
+ attr_writer :xml_value
520
+
521
+ element :title, String
522
+ end
523
+
524
+ class Video
525
+ include HappyMapper
526
+
527
+ tag 'Video'
528
+ namespace 'video'
529
+
530
+ attr_writer :xml_value
531
+
532
+ element :title, String
533
+ element :publish_options, PublishOptions, tag: 'publishOptions', namespace: 'video'
534
+ end
535
+
536
+ class OptionalAttribute
537
+ include HappyMapper
538
+ tag 'address'
539
+
540
+ attribute :street, String
541
+ end
542
+
543
+ class DefaultNamespaceCombi
544
+ include HappyMapper
545
+
546
+ register_namespace 'bk', 'urn:loc.gov:books'
547
+ register_namespace 'isbn', 'urn:ISBN:0-395-36341-6'
548
+ register_namespace 'p', 'urn:loc.gov:people'
549
+ namespace 'bk'
550
+
551
+ tag 'book'
552
+
553
+ element :title, String, namespace: 'bk', tag: 'title'
554
+ element :number, String, namespace: 'isbn', tag: 'number'
555
+ element :author, String, namespace: 'p', tag: 'author'
556
+ end
557
+
558
+ describe HappyMapper do
559
+ describe 'being included into another class' do
560
+ before do
561
+ @klass = Class.new do
562
+ include HappyMapper
563
+
564
+ def self.to_s
565
+ 'Boo'
566
+ end
567
+ end
568
+ end
569
+
570
+ class Boo; include HappyMapper end
571
+
572
+ it 'should set attributes to an array' do
573
+ @klass.attributes.should == []
574
+ end
575
+
576
+ it 'should set @elements to a hash' do
577
+ @klass.elements.should == []
578
+ end
579
+
580
+ it 'should allow adding an attribute' do
581
+ lambda do
582
+ @klass.attribute :name, String
583
+ end.should change(@klass, :attributes)
584
+ end
585
+
586
+ it 'should allow adding an attribute containing a dash' do
587
+ lambda do
588
+ @klass.attribute :'bar-baz', String
589
+ end.should change(@klass, :attributes)
590
+ end
591
+
592
+ it 'should be able to get all attributes in array' do
593
+ @klass.attribute :name, String
594
+ @klass.attributes.size.should == 1
595
+ end
596
+
597
+ it 'should allow adding an element' do
598
+ lambda do
599
+ @klass.element :name, String
600
+ end.should change(@klass, :elements)
601
+ end
602
+
603
+ it 'should allow adding an element containing a dash' do
604
+ lambda do
605
+ @klass.element :'bar-baz', String
606
+ end.should change(@klass, :elements)
607
+ end
608
+
609
+ it 'should be able to get all elements in array' do
610
+ @klass.element(:name, String)
611
+ @klass.elements.size.should == 1
612
+ end
613
+
614
+ it 'should allow has one association' do
615
+ @klass.has_one(:user, User)
616
+ element = @klass.elements.first
617
+ element.name.should == 'user'
618
+ element.type.should == User
619
+ element.options[:single].should == true
620
+ end
621
+
622
+ it 'should allow has many association' do
623
+ @klass.has_many(:users, User)
624
+ element = @klass.elements.first
625
+ element.name.should == 'users'
626
+ element.type.should == User
627
+ element.options[:single].should == false
628
+ end
629
+
630
+ it 'should default tag name to lowercase class' do
631
+ @klass.tag_name.should == 'boo'
632
+ end
633
+
634
+ it 'should default tag name of class in modules to the last constant lowercase' do
635
+ module Bar; class Baz; include HappyMapper; end; end
636
+ Bar::Baz.tag_name.should == 'baz'
637
+ end
638
+
639
+ it 'should allow setting tag name' do
640
+ @klass.tag('FooBar')
641
+ @klass.tag_name.should == 'FooBar'
642
+ end
643
+
644
+ it 'should allow setting a namespace' do
645
+ @klass.namespace(namespace = 'boo')
646
+ @klass.namespace.should == namespace
647
+ end
648
+
649
+ it 'should provide #parse' do
650
+ @klass.should respond_to(:parse)
651
+ end
652
+ end
653
+
654
+ describe '#attributes' do
655
+ it 'should only return attributes for the current class' do
656
+ Post.attributes.size.should == 7
657
+ Status.attributes.size.should == 0
658
+ end
659
+ end
660
+
661
+ describe '#elements' do
662
+ it 'should only return elements for the current class' do
663
+ Post.elements.size.should == 0
664
+ Status.elements.size.should == 10
665
+ end
666
+ end
667
+
668
+ describe '#content' do
669
+ it 'should take String as default argument for type' do
670
+ State.content :name
671
+ address = Address.parse(fixture_file('address.xml'))
672
+ address.state.name.should == 'Lower Saxony'
673
+ address.state.name.class == String
674
+ end
675
+
676
+ it 'should work when specific type is provided' do
677
+ Rate.content :value, Float
678
+ Product.has_one :rate, Rate
679
+ product = Product.parse(fixture_file('product_default_namespace.xml'), single: true)
680
+ product.rate.value.should == 120.25
681
+ product.rate.class == Float
682
+ end
683
+ end
684
+
685
+ it 'should parse xml attributes into ruby objects' do
686
+ posts = Post.parse(fixture_file('posts.xml'))
687
+ posts.size.should == 20
688
+ first = posts.first
689
+ first.href.should == 'http://roxml.rubyforge.org/'
690
+ first.hash.should == '19bba2ab667be03a19f67fb67dc56917'
691
+ first.description.should == 'ROXML - Ruby Object to XML Mapping Library'
692
+ first.tag.should == 'ruby xml gems mapping'
693
+ first.time.should == Time.utc(2008, 8, 9, 5, 24, 20)
694
+ first.others.should == 56
695
+ first.extended.should == 'ROXML is a Ruby library designed to make it easier for Ruby developers to work with XML. Using simple annotations, it enables Ruby classes to be custom-mapped to XML. ROXML takes care of the marshalling and unmarshalling of mapped attributes so that developers can focus on building first-class Ruby classes.'
696
+ end
697
+
698
+ it 'should parse xml elements to ruby objcts' do
699
+ statuses = Status.parse(fixture_file('statuses.xml'))
700
+ statuses.size.should == 20
701
+ first = statuses.first
702
+ first.id.should == 882_281_424
703
+ first.created_at.should == Time.utc(2008, 8, 9, 5, 38, 12)
704
+ first.source.should == 'web'
705
+ first.truncated.should be_falsey
706
+ first.in_reply_to_status_id.should == 1234
707
+ first.in_reply_to_user_id.should == 12_345
708
+ first.favorited.should be false
709
+ first.user.id.should == 4243
710
+ first.user.name.should == 'John Nunemaker'
711
+ first.user.screen_name.should == 'jnunemaker'
712
+ first.user.location.should == 'Mishawaka, IN, US'
713
+ first.user.description.should == 'Loves his wife, ruby, notre dame football and iu basketball'
714
+ first.user.profile_image_url.should == 'http://s3.amazonaws.com/twitter_production/profile_images/53781608/Photo_75_normal.jpg'
715
+ first.user.url.should == 'http://addictedtonew.com'
716
+ first.user.protected.should be false
717
+ first.user.followers_count.should == 486
718
+ end
719
+
720
+ it 'should parse xml containing the desired element as root node' do
721
+ address = Address.parse(fixture_file('address.xml'), single: true)
722
+ address.street.should == 'Milchstrasse'
723
+ address.postcode.should == '26131'
724
+ address.housenumber.should == '23'
725
+ address.city.should == 'Oldenburg'
726
+ address.country.class.should == Country
727
+ end
728
+
729
+ it 'should parse text node correctly' do
730
+ address = Address.parse(fixture_file('address.xml'), single: true)
731
+ address.country.name.should == 'Germany'
732
+ address.country.code.should == 'de'
733
+ end
734
+
735
+ it 'should treat Nokogiri::XML::Document as root' do
736
+ doc = Nokogiri::XML(fixture_file('address.xml'))
737
+ address = Address.parse(doc)
738
+ address.class.should == Address
739
+ end
740
+
741
+ it 'should parse xml with default namespace (amazon)' do
742
+ file_contents = fixture_file('pita.xml')
743
+ items = PITA::Items.parse(file_contents, single: true)
744
+ items.total_results.should == 22
745
+ items.total_pages.should == 3
746
+ first = items.items[0]
747
+ second = items.items[1]
748
+ first.asin.should == '0321480791'
749
+ first.point.should == '38.5351715088 -121.7948684692'
750
+ first.detail_page_url.should be_a_kind_of(URI)
751
+ 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'
752
+ first.manufacturer.should == 'Addison-Wesley Professional'
753
+ first.product_group.should == '<ProductGroup>Book</ProductGroup>'
754
+ second.asin.should == '047022388X'
755
+ second.manufacturer.should == 'Wrox'
756
+ end
757
+
758
+ it 'should parse xml that has attributes of elements' do
759
+ items = CurrentWeather.parse(fixture_file('current_weather.xml'))
760
+ first = items[0]
761
+ first.temperature.should == 51
762
+ first.feels_like.should == 51
763
+ first.current_condition.should == 'Sunny'
764
+ first.current_condition.icon.should == 'http://deskwx.weatherbug.com/images/Forecast/icons/cond007.gif'
765
+ end
766
+
767
+ it "parses xml with attributes of elements that aren't :single => true" do
768
+ feed = Atom::Feed.parse(fixture_file('atom.xml'))
769
+ feed.link.first.href.should == 'http://www.example.com'
770
+ feed.link.last.href.should == 'http://www.example.com/tv_shows.atom'
771
+ end
772
+
773
+ it 'parses xml with optional elements with embedded attributes' do
774
+ expect { CurrentWeather.parse(fixture_file('current_weather_missing_elements.xml')) }.to_not raise_error
775
+ end
776
+
777
+ it 'returns nil rather than empty array for absent values when :single => true' do
778
+ address = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', single: true)
779
+ address.should be_nil
780
+ end
781
+
782
+ it 'should return same result for absent values when :single => true, regardless of :in_groups_of' do
783
+ addr1 = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', single: true)
784
+ addr2 = Address.parse('<?xml version="1.0" encoding="UTF-8"?><foo/>', single: true, in_groups_of: 10)
785
+ addr1.should == addr2
786
+ end
787
+
788
+ it 'should parse xml with nested elements' do
789
+ radars = Radar.parse(fixture_file('radar.xml'))
790
+ first = radars[0]
791
+ first.places.size.should == 1
792
+ first.places[0].name.should == 'Store'
793
+ second = radars[1]
794
+ second.places.size.should == 0
795
+ third = radars[2]
796
+ third.places.size.should == 2
797
+ third.places[0].name.should == 'Work'
798
+ third.places[1].name.should == 'Home'
799
+ end
800
+
801
+ it 'should parse xml with element name different to class name' do
802
+ game = QuarterTest::Game.parse(fixture_file('quarters.xml'))
803
+ game.q1.start.should == '4:40:15 PM'
804
+ game.q2.start.should == '5:18:53 PM'
805
+ end
806
+
807
+ it 'should parse xml that has elements with dashes' do
808
+ commit = GitHub::Commit.parse(fixture_file('commit.xml'))
809
+ commit.message.should == 'move commands.rb and helpers.rb into commands/ dir'
810
+ commit.url.should == 'http://github.com/defunkt/github-gem/commit/c26d4ce9807ecf57d3f9eefe19ae64e75bcaaa8b'
811
+ commit.id.should == 'c26d4ce9807ecf57d3f9eefe19ae64e75bcaaa8b'
812
+ commit.committed_date.should == Date.parse('2008-03-02T16:45:41-08:00')
813
+ commit.tree.should == '28a1a1ca3e663d35ba8bf07d3f1781af71359b76'
814
+ end
815
+
816
+ it 'should parse xml with no namespace' do
817
+ product = Product.parse(fixture_file('product_no_namespace.xml'), single: true)
818
+ product.title.should == 'A Title'
819
+ product.feature_bullets.bug.should == 'This is a bug'
820
+ product.feature_bullets.features.size.should == 2
821
+ product.feature_bullets.features[0].name.should == 'This is feature text 1'
822
+ product.feature_bullets.features[1].name.should == 'This is feature text 2'
823
+ end
824
+
825
+ it 'should parse xml with default namespace' do
826
+ product = Product.parse(fixture_file('product_default_namespace.xml'), single: true)
827
+ product.title.should == 'A Title'
828
+ product.feature_bullets.bug.should == 'This is a bug'
829
+ product.feature_bullets.features.size.should == 2
830
+ product.feature_bullets.features[0].name.should == 'This is feature text 1'
831
+ product.feature_bullets.features[1].name.should == 'This is feature text 2'
832
+ end
833
+
834
+ it 'should parse xml with single namespace' do
835
+ product = Product.parse(fixture_file('product_single_namespace.xml'), single: true)
836
+ product.title.should == 'A Title'
837
+ product.feature_bullets.bug.should == 'This is a bug'
838
+ product.feature_bullets.features.size.should == 2
839
+ product.feature_bullets.features[0].name.should == 'This is feature text 1'
840
+ product.feature_bullets.features[1].name.should == 'This is feature text 2'
841
+ end
842
+
843
+ it 'should parse xml with multiple namespaces' do
844
+ track = FedEx::TrackReply.parse(fixture_file('multiple_namespaces.xml'))
845
+ track.highest_severity.should == 'SUCCESS'
846
+ track.more_data.should be false
847
+ notification = track.notifications.first
848
+ notification.code.should == 0
849
+ notification.localized_message.should == 'Request was successfully processed.'
850
+ notification.message.should == 'Request was successfully processed.'
851
+ notification.severity.should == 'SUCCESS'
852
+ notification.source.should == 'trck'
853
+ detail = track.trackdetails.first
854
+ detail.carrier_code.should == 'FDXG'
855
+ detail.est_delivery.should == '2009-01-02T00:00:00'
856
+ detail.service_info.should == 'Ground-Package Returns Program-Domestic'
857
+ detail.status_code.should == 'OD'
858
+ detail.status_desc.should == 'On FedEx vehicle for delivery'
859
+ detail.tracking_number.should == '9611018034267800045212'
860
+ detail.weight.units.should == 'LB'
861
+ detail.weight.value.should == 2
862
+ events = detail.events
863
+ events.size.should == 10
864
+ first_event = events[0]
865
+ first_event.eventdescription.should == 'On FedEx vehicle for delivery'
866
+ first_event.eventtype.should == 'OD'
867
+ first_event.timestamp.should == '2009-01-02T06:00:00'
868
+ first_event.address.city.should == 'WICHITA'
869
+ first_event.address.countrycode.should == 'US'
870
+ first_event.address.residential.should be false
871
+ first_event.address.state.should == 'KS'
872
+ first_event.address.zip.should == '67226'
873
+ last_event = events[-1]
874
+ last_event.eventdescription.should == 'In FedEx possession'
875
+ last_event.eventtype.should == 'IP'
876
+ last_event.timestamp.should == '2008-12-27T09:40:00'
877
+ last_event.address.city.should == 'LONGWOOD'
878
+ last_event.address.countrycode.should == 'US'
879
+ last_event.address.residential.should be false
880
+ last_event.address.state.should == 'FL'
881
+ last_event.address.zip.should == '327506398'
882
+ track.tran_detail.cust_tran_id.should == '20090102-111321'
883
+ end
884
+
885
+ it 'should be able to parse google analytics api xml' do
886
+ data = Analytics::Feed.parse(fixture_file('analytics.xml'))
887
+ data.id.should == 'http://www.google.com/analytics/feeds/accounts/nunemaker@gmail.com'
888
+ data.entries.size.should == 4
889
+
890
+ entry = data.entries[0]
891
+ entry.title.should == 'addictedtonew.com'
892
+ entry.properties.size.should == 4
893
+
894
+ property = entry.properties[0]
895
+ property.name.should == 'ga:accountId'
896
+ property.value.should == '85301'
897
+ end
898
+
899
+ it 'should be able to parse google analytics profile xml with manually declared namespace' do
900
+ data = Analytics::Profile.parse(fixture_file('analytics_profile.xml'))
901
+ data.entries.size.should == 6
902
+
903
+ entry = data.entries[0]
904
+ entry.title.should == 'www.homedepot.com'
905
+ entry.properties.size.should == 6
906
+ entry.goals.size.should == 0
907
+ end
908
+
909
+ it 'should allow instantiating with a string' do
910
+ module StringFoo
911
+ class Bar
912
+ include HappyMapper
913
+ has_many :things, 'StringFoo::Thing'
914
+ end
915
+
916
+ class Thing
917
+ include HappyMapper
918
+ end
919
+ end
920
+ end
921
+
922
+ it 'should parse family search xml' do
923
+ tree = FamilySearch::FamilyTree.parse(fixture_file('family_tree.xml'))
924
+ tree.version.should == '1.0.20071213.942'
925
+ tree.status_message.should == 'OK'
926
+ tree.status_code.should == '200'
927
+ tree.persons.person.size.should == 1
928
+ tree.persons.person.first.version.should == '1199378491000'
929
+ tree.persons.person.first.modified.should == Time.utc(2008, 1, 3, 16, 41, 31) # 2008-01-03T09:41:31-07:00
930
+ tree.persons.person.first.id.should == 'KWQS-BBQ'
931
+ tree.persons.person.first.information.alternateIds.ids.should_not be_kind_of(String)
932
+ tree.persons.person.first.information.alternateIds.ids.size.should == 8
933
+ end
934
+
935
+ it 'should parse multiple images' do
936
+ artist = Artist.parse(fixture_file('multiple_primitives.xml'))
937
+ artist.name.should == 'value'
938
+ artist.images.size.should == 2
939
+ end
940
+
941
+ it 'should parse lastfm namespaces' do
942
+ l = Location.parse(fixture_file('lastfm.xml'))
943
+ l.first.latitude.should == '51.53469'
944
+ end
945
+
946
+ describe 'Parse optional attributes' do
947
+ it 'should parse an empty String as empty' do
948
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
949
+ a[0].street.should == ''
950
+ end
951
+
952
+ it 'should parse a String with value' do
953
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
954
+ a[1].street.should == 'Milchstrasse'
955
+ end
956
+
957
+ it 'should parse a String with value' do
958
+ a = OptionalAttribute.parse(fixture_file('optional_attributes.xml'))
959
+ a[2].street.should be_nil
960
+ end
961
+ end
962
+
963
+ describe 'Default namespace combi' do
964
+ before(:each) do
965
+ file_contents = fixture_file('default_namespace_combi.xml')
966
+ @book = DefaultNamespaceCombi.parse(file_contents, single: true)
967
+ end
968
+
969
+ it 'should parse author' do
970
+ @book.author.should == 'Frank Gilbreth'
971
+ end
972
+
973
+ it 'should parse title' do
974
+ @book.title.should == 'Cheaper by the Dozen'
975
+ end
976
+
977
+ it 'should parse number' do
978
+ @book.number.should == '1568491379'
979
+ end
980
+ end
981
+
982
+ describe 'Xml Content' do
983
+ before(:each) do
984
+ file_contents = fixture_file('dictionary.xml')
985
+ @records = Dictionary::Record.parse(file_contents)
986
+ end
987
+
988
+ it 'should parse XmlContent' do
989
+ @records.first.definitions.first.text.should ==
990
+ 'a large common parrot, <bn>Cacatua galerita</bn>, predominantly white, with yellow on the undersides of wings and tail and a forward curving yellow crest, found in Australia, New Guinea and nearby islands.'
991
+ end
992
+
993
+ it "should save object's xml content" do
994
+ @records.first.variants.first.xml_content.should ==
995
+ 'white <tag>cockatoo</tag>'
996
+ @records.first.variants.last.to_html.should ==
997
+ '<em>white</em> cockatoo'
998
+ end
999
+ end
1000
+
1001
+ it 'should parse ambigous items' do
1002
+ items = AmbigousItems::Item.parse(fixture_file('ambigous_items.xml'), xpath: '/ambigous/my-items')
1003
+ items.map(&:name).should == %w(first second third).map { |s| "My #{s} item" }
1004
+ end
1005
+
1006
+ context Article do
1007
+ it 'should parse the publish options for Article and Photo' do
1008
+ @article.title.should_not be_nil
1009
+ @article.text.should_not be_nil
1010
+ @article.photos.should_not be_nil
1011
+ @article.photos.first.title.should_not be_nil
1012
+ end
1013
+
1014
+ it 'should parse the publish options for Article' do
1015
+ @article.publish_options.should_not be_nil
1016
+ end
1017
+
1018
+ it 'should parse the publish options for Photo' do
1019
+ @article.photos.first.publish_options.should_not be_nil
1020
+ end
1021
+
1022
+ it 'should only find only items at the parent level' do
1023
+ @article.photos.length.should == 1
1024
+ end
1025
+
1026
+ before(:all) do
1027
+ @article = Article.parse(fixture_file('subclass_namespace.xml'))
1028
+ end
1029
+ end
1030
+
1031
+ context 'Namespace is missing because an optional element that uses it is not present' do
1032
+ it 'should parse successfully' do
1033
+ @article = PartiallyBadArticle.parse(fixture_file('subclass_namespace.xml'))
1034
+ @article.should_not be_nil
1035
+ @article.title.should_not be_nil
1036
+ @article.text.should_not be_nil
1037
+ @article.photos.should_not be_nil
1038
+ @article.photos.first.title.should_not be_nil
1039
+ end
1040
+ end
1041
+
1042
+ describe 'with limit option' do
1043
+ it 'should return results with limited size: 6' do
1044
+ sizes = []
1045
+ posts = Post.parse(fixture_file('posts.xml'), in_groups_of: 6) do |a|
1046
+ sizes << a.size
1047
+ end
1048
+ sizes.should == [6, 6, 6, 2]
1049
+ end
1050
+
1051
+ it 'should return results with limited size: 10' do
1052
+ sizes = []
1053
+ posts = Post.parse(fixture_file('posts.xml'), in_groups_of: 10) do |a|
1054
+ sizes << a.size
1055
+ end
1056
+ sizes.should == [10, 10]
1057
+ end
1058
+ end
1059
+
1060
+ context 'when letting user set Nokogiri::XML::ParseOptions' do
1061
+ let(:default) do
1062
+ Class.new do
1063
+ include HappyMapper
1064
+ element :item, String
1065
+ end
1066
+ end
1067
+ let(:custom) do
1068
+ Class.new do
1069
+ include HappyMapper
1070
+ element :item, String
1071
+ with_nokogiri_config(&:default_xml)
1072
+ end
1073
+ end
1074
+
1075
+ it 'initializes @nokogiri_config_callback to nil' do
1076
+ default.nokogiri_config_callback.should be_nil
1077
+ end
1078
+
1079
+ it 'defaults to Nokogiri::XML::ParseOptions::STRICT' do
1080
+ expect { default.parse(fixture_file('set_config_options.xml')) }.to raise_error(Nokogiri::XML::SyntaxError)
1081
+ end
1082
+
1083
+ it 'accepts .on_config callback' do
1084
+ custom.nokogiri_config_callback.should_not be_nil
1085
+ end
1086
+
1087
+ it 'parses according to @nokogiri_config_callback' do
1088
+ expect { custom.parse(fixture_file('set_config_options.xml')) }.to_not raise_error
1089
+ end
1090
+
1091
+ it 'can clear @nokogiri_config_callback' do
1092
+ custom.with_nokogiri_config {}
1093
+ expect { custom.parse(fixture_file('set_config_options.xml')) }.to raise_error(Nokogiri::XML::SyntaxError)
1094
+ end
1095
+ end
1096
+ end