ROXML 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/.gitignore +6 -0
  2. data/.gitmodules +3 -0
  3. data/History.txt +299 -0
  4. data/MIT-LICENSE +18 -0
  5. data/README.rdoc +161 -0
  6. data/Rakefile +95 -0
  7. data/TODO +39 -0
  8. data/VERSION +1 -0
  9. data/config/website.yml +2 -0
  10. data/examples/amazon.rb +35 -0
  11. data/examples/current_weather.rb +27 -0
  12. data/examples/dashed_elements.rb +20 -0
  13. data/examples/library.rb +40 -0
  14. data/examples/posts.rb +27 -0
  15. data/examples/rails.rb +70 -0
  16. data/examples/twitter.rb +37 -0
  17. data/examples/xml/active_record.xml +70 -0
  18. data/examples/xml/amazon.xml +133 -0
  19. data/examples/xml/current_weather.xml +89 -0
  20. data/examples/xml/dashed_elements.xml +52 -0
  21. data/examples/xml/posts.xml +23 -0
  22. data/examples/xml/twitter.xml +422 -0
  23. data/lib/roxml.rb +547 -0
  24. data/lib/roxml/definition.rb +236 -0
  25. data/lib/roxml/hash_definition.rb +25 -0
  26. data/lib/roxml/xml.rb +43 -0
  27. data/lib/roxml/xml/parsers/libxml.rb +91 -0
  28. data/lib/roxml/xml/parsers/nokogiri.rb +77 -0
  29. data/lib/roxml/xml/references.rb +297 -0
  30. data/roxml.gemspec +201 -0
  31. data/spec/definition_spec.rb +486 -0
  32. data/spec/examples/active_record_spec.rb +40 -0
  33. data/spec/examples/amazon_spec.rb +54 -0
  34. data/spec/examples/current_weather_spec.rb +37 -0
  35. data/spec/examples/dashed_elements_spec.rb +20 -0
  36. data/spec/examples/library_spec.rb +46 -0
  37. data/spec/examples/post_spec.rb +24 -0
  38. data/spec/examples/twitter_spec.rb +32 -0
  39. data/spec/roxml_spec.rb +372 -0
  40. data/spec/shared_specs.rb +15 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +14 -0
  43. data/spec/support/libxml.rb +3 -0
  44. data/spec/support/nokogiri.rb +3 -0
  45. data/spec/xml/attributes_spec.rb +36 -0
  46. data/spec/xml/namespace_spec.rb +240 -0
  47. data/spec/xml/namespaces_spec.rb +32 -0
  48. data/spec/xml/parser_spec.rb +26 -0
  49. data/tasks/rdoc.rake +13 -0
  50. data/tasks/rspec.rake +25 -0
  51. data/tasks/test.rake +35 -0
  52. data/test/fixtures/book_malformed.xml +5 -0
  53. data/test/fixtures/book_pair.xml +8 -0
  54. data/test/fixtures/book_text_with_attribute.xml +5 -0
  55. data/test/fixtures/book_valid.xml +5 -0
  56. data/test/fixtures/book_with_authors.xml +7 -0
  57. data/test/fixtures/book_with_contributions.xml +9 -0
  58. data/test/fixtures/book_with_contributors.xml +7 -0
  59. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  60. data/test/fixtures/book_with_default_namespace.xml +9 -0
  61. data/test/fixtures/book_with_depth.xml +6 -0
  62. data/test/fixtures/book_with_octal_pages.xml +4 -0
  63. data/test/fixtures/book_with_publisher.xml +7 -0
  64. data/test/fixtures/book_with_wrapped_attr.xml +3 -0
  65. data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
  66. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  67. data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
  68. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  69. data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
  70. data/test/fixtures/dictionary_of_names.xml +4 -0
  71. data/test/fixtures/dictionary_of_texts.xml +10 -0
  72. data/test/fixtures/library.xml +30 -0
  73. data/test/fixtures/library_uppercase.xml +30 -0
  74. data/test/fixtures/muffins.xml +3 -0
  75. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  76. data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
  77. data/test/fixtures/node_with_name_conflicts.xml +4 -0
  78. data/test/fixtures/numerology.xml +4 -0
  79. data/test/fixtures/person.xml +1 -0
  80. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  81. data/test/fixtures/person_with_mothers.xml +10 -0
  82. data/test/mocks/dictionaries.rb +57 -0
  83. data/test/mocks/mocks.rb +279 -0
  84. data/test/support/fixtures.rb +11 -0
  85. data/test/test_helper.rb +34 -0
  86. data/test/unit/definition_test.rb +235 -0
  87. data/test/unit/deprecations_test.rb +24 -0
  88. data/test/unit/to_xml_test.rb +81 -0
  89. data/test/unit/xml_attribute_test.rb +39 -0
  90. data/test/unit/xml_block_test.rb +81 -0
  91. data/test/unit/xml_bool_test.rb +122 -0
  92. data/test/unit/xml_convention_test.rb +150 -0
  93. data/test/unit/xml_hash_test.rb +115 -0
  94. data/test/unit/xml_initialize_test.rb +49 -0
  95. data/test/unit/xml_name_test.rb +141 -0
  96. data/test/unit/xml_namespace_test.rb +31 -0
  97. data/test/unit/xml_object_test.rb +207 -0
  98. data/test/unit/xml_required_test.rb +94 -0
  99. data/test/unit/xml_text_test.rb +71 -0
  100. data/website/index.html +98 -0
  101. metadata +254 -0
@@ -0,0 +1,40 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/rails'
3
+
4
+ describe ROXML, "under ActiveRecord" do
5
+ before do
6
+ @route = Route.from_xml(xml_for('active_record'))
7
+ end
8
+
9
+ before(:all) do
10
+ FileUtils.rm(DB_PATH) if File.exists?(DB_PATH)
11
+ end
12
+
13
+ it "should be parsed" do
14
+ @route.should_not == nil
15
+ @route.should be_an_instance_of(Route)
16
+ end
17
+
18
+ describe "xml attributes" do
19
+ it "should extract xml attributes" do
20
+ @route.totalHg.should == "640"
21
+ @route.lonlatx.should == "357865"
22
+ @route.lonlaty.should == "271635"
23
+ @route.grcenter.should == "SH 71635 57865"
24
+ @route.totalMins.should == "235.75000000000003"
25
+ @route.totalDist.should == "11185.321521477119"
26
+ end
27
+ end
28
+
29
+ describe "xml sub-objects" do
30
+ it "should extract xml sub-objects" do
31
+ @route.should have(6).waypoints
32
+ @route.waypoints.each {|waypoint| waypoint.should be_an_instance_of(Waypoint)}
33
+ end
34
+ it "should be usable as a ActiveRecord object" do
35
+ Waypoint.count.should == 0
36
+ @route.save!
37
+ Waypoint.count.should == 6
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/amazon'
3
+
4
+ describe PITA::ItemSearchResponse do
5
+ before do
6
+ @response = PITA::ItemSearchResponse.from_xml(xml_for('amazon'))
7
+ end
8
+
9
+ describe "#total_results" do
10
+ it "should be parsed as a number" do
11
+ @response.total_results.should > 0
12
+ end
13
+ end
14
+
15
+ describe "#total_pages" do
16
+ it "should be parsed as a number" do
17
+ @response.total_pages.should > 0
18
+ end
19
+ end
20
+
21
+ describe "#items" do
22
+ it "should return a collection of items" do
23
+ @response.items.should be_an_instance_of(Array)
24
+ @response.items.size.should > 0
25
+ @response.items.each {|item| item.should be_an_instance_of(PITA::Item) }
26
+ end
27
+
28
+ it "should have the some number less than or equal to #total_results" do
29
+ @response.items.size.should > 0
30
+ @response.items.size.should <= @response.total_results
31
+ end
32
+ end
33
+ end
34
+
35
+ describe PITA::Item do
36
+ before do
37
+ @items = PITA::ItemSearchResponse.from_xml(xml_for('amazon')).items
38
+ end
39
+
40
+ it "should extract asin" do
41
+ @items.each {|item| item.asin.should be_an_instance_of(String) }
42
+ @items.each {|item| item.asin.should_not be_empty }
43
+ end
44
+
45
+ it "should extract detail_page_url" do
46
+ @items.each {|item| item.detail_page_url.should be_an_instance_of(String) }
47
+ @items.each {|item| item.detail_page_url.should_not be_empty }
48
+ end
49
+
50
+ it "should extract manufacturer" do
51
+ @items.each {|item| item.manufacturer.should be_an_instance_of(String) }
52
+ @items.each {|item| item.manufacturer.should_not be_empty }
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/current_weather'
3
+
4
+ describe Weather do
5
+ before do
6
+ @weather = Weather.from_xml(xml_for('current_weather'))
7
+ end
8
+
9
+ it "should extract observations" do
10
+ @weather.observation.should be_an_instance_of(WeatherObservation)
11
+ end
12
+ end
13
+
14
+ describe WeatherObservation do
15
+ before do
16
+ @observation = Weather.from_xml(xml_for('current_weather')).observation
17
+ end
18
+
19
+ it "should extract temperature" do
20
+ @observation.temperature.should > 0
21
+ end
22
+
23
+ it "should extract feels_like" do
24
+ @observation.feels_like.should > 0
25
+ end
26
+
27
+ describe "#current_condition" do
28
+ it "should extract current_condition" do
29
+ @observation.current_condition.should_not be_empty
30
+ end
31
+
32
+ it "should extract icon attribute" do
33
+ pending "need to think options through for HappyMapper-style :attributes extensions"
34
+ @observation.current_condition.icon.should_not be_empty
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/dashed_elements'
3
+
4
+ describe GitHub::Commit do
5
+ before do
6
+ @commit = GitHub::Commit.from_xml(xml_for('dashed_elements'))
7
+ end
8
+
9
+ it "should extract committed date" do
10
+ @commit.committed_date.should be_an_instance_of(Date)
11
+ end
12
+
13
+ it "should extract url" do
14
+ @commit.url.should_not be_empty
15
+ end
16
+
17
+ it "should extract id" do
18
+ @commit.id.should_not be_empty
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/library'
3
+
4
+ describe Library do
5
+ before :all do
6
+ book = Novel.new
7
+ book.isbn = "0201710897"
8
+ book.title = "The PickAxe"
9
+ book.description = "Best Ruby book out there!"
10
+ book.author = "David Thomas, Andrew Hunt, Dave Thomas"
11
+ book.publisher = Publisher.new('Addison Wesley Longman, Inc.')
12
+
13
+ @lib = Library.new
14
+ @lib.name = "Favorite Books"
15
+ @lib.novels = [book]
16
+ end
17
+
18
+ describe "#to_xml" do
19
+ it "should contain the expected information" do
20
+ @lib.to_xml.to_s.should == ROXML::XML::Parser.parse(%{<library><NAME><![CDATA[Favorite Books]]></NAME><novel ISBN='0201710897'><title>The PickAxe</title><description><![CDATA[Best Ruby book out there!]]></description><author>David Thomas, Andrew Hunt, Dave Thomas</author><publisher><name>Addison Wesley Longman, Inc.</name></publisher></novel></library>}).root.to_s
21
+ end
22
+
23
+ context "when written to a file" do
24
+ before :all do
25
+ @path = "spec/examples/library.xml"
26
+ @doc = ROXML::XML::Document.new
27
+ @doc.root = @lib.to_xml
28
+ @doc.save(@path)
29
+ end
30
+
31
+ after :all do
32
+ FileUtils.rm @path
33
+ end
34
+
35
+ it "should be contain the expected xml" do
36
+ ROXML::XML::Parser.parse(File.read(@path)).to_s.should == ROXML::XML::Parser.parse(%{<?xml version="1.0"?><library><NAME><![CDATA[Favorite Books]]></NAME><novel ISBN='0201710897'><title>The PickAxe</title><description><![CDATA[Best Ruby book out there!]]></description><author>David Thomas, Andrew Hunt, Dave Thomas</author><publisher><name>Addison Wesley Longman, Inc.</name></publisher></novel></library>}).to_s
37
+ end
38
+
39
+ it "should be re-parsable via .from_xml" do
40
+ File.open("spec/examples/library.xml") do |file|
41
+ Library.from_xml(file).should == @lib
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/posts'
3
+
4
+ describe Post do
5
+ before do
6
+ @posts = Posts.from_xml(xml_for('posts')).posts
7
+ end
8
+
9
+ it "should extract description" do
10
+ @posts.each {|post| post.description.should_not be_empty }
11
+ end
12
+
13
+ it "should extract href" do
14
+ @posts.each {|post| post.href.should_not be_empty }
15
+ end
16
+
17
+ it "should extract extended" do
18
+ @posts.each {|post| post.extended.should_not be_empty }
19
+ end
20
+
21
+ it "should extract time" do
22
+ @posts.each {|post| post.created_at.should be_an_instance_of(DateTime) }
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec/spec_helper'
2
+ require 'examples/twitter'
3
+
4
+ describe Statuses do
5
+ describe Status do
6
+ before do
7
+ @statuses = Statuses.from_xml(xml_for('twitter')).statuses
8
+ end
9
+
10
+ it "should extract text" do
11
+ @statuses.each {|status| status.text.should_not be_empty }
12
+ end
13
+
14
+ it "should extract source" do
15
+ @statuses.each {|status| status.source.should_not be_empty }
16
+ end
17
+
18
+ describe User do
19
+ before do
20
+ @users = @statuses.map(&:user)
21
+ end
22
+
23
+ it "should extract name" do
24
+ @users.each {|user| user.name.should == "John Nunemaker" }
25
+ end
26
+
27
+ it "should extract screen_name" do
28
+ @users.each {|user| user.screen_name.should == "jnunemaker" }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,372 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe ROXML, "#from_xml" do
4
+ describe "from_xml call", :shared => true do
5
+ it "should fetch values" do
6
+ book = BookWithContributors.from_xml(@path)
7
+ book.title.should == "Programming Ruby - 2nd Edition"
8
+ book.contributors.map(&:name).should == ["David Thomas","Andrew Hunt","Chad Fowler"]
9
+ end
10
+ end
11
+
12
+ context "called with PathName" do
13
+ before do
14
+ @path = Pathname.new(fixture_path(:book_with_contributors))
15
+ end
16
+ it_should_behave_like "from_xml call"
17
+ end
18
+
19
+ context "called with File" do
20
+ before do
21
+ @path = File.new(fixture_path(:book_with_contributors))
22
+ end
23
+ it_should_behave_like "from_xml call"
24
+ end
25
+
26
+ context "called with URI" do
27
+ before do
28
+ require 'uri'
29
+ @path = URI.parse("file://#{File.expand_path(File.expand_path(fixture_path(:book_with_contributors)))}")
30
+ end
31
+ it_should_behave_like "from_xml call"
32
+ end
33
+ end
34
+
35
+ describe ROXML, "#xml" do
36
+ class DescriptionReadonly
37
+ include ROXML
38
+
39
+ xml_reader :writable, :from => :content
40
+ xml_reader :readonly, :from => :content, :frozen => true
41
+ end
42
+
43
+ class Contributor
44
+ include ROXML
45
+
46
+ xml_reader :role, :from => :attr
47
+ xml_reader :name
48
+ end
49
+
50
+ class BookWithContributions
51
+ include ROXML
52
+
53
+ xml_name :book
54
+ xml_reader :isbn, :from => :attr
55
+ xml_reader :title
56
+ xml_reader :description, :as => DescriptionReadonly
57
+ xml_reader :contributions, :as => [Contributor], :from => 'contributor', :in => "contributions"
58
+ end
59
+
60
+ class BookWithContributionsReadonly
61
+ include ROXML
62
+
63
+ xml_name :book
64
+ xml_reader :isbn, :from => :attr, :frozen => true
65
+ xml_reader :title, :frozen => true
66
+ xml_reader :description, :as => DescriptionReadonly, :frozen => true
67
+ xml_reader :contributions, :as => [Contributor], :from => 'contributor', :in => "contributions", :frozen => true
68
+ end
69
+
70
+ before do
71
+ @writable = BookWithContributions.from_xml(fixture(:book_with_contributions))
72
+ @readonly = BookWithContributionsReadonly.from_xml(fixture(:book_with_contributions))
73
+ end
74
+
75
+ it "should raise on duplicate accessor name" do
76
+ proc do
77
+ Class.new do
78
+ include ROXML
79
+
80
+ xml_reader :id
81
+ xml_accessor :id
82
+ end
83
+ end.should raise_error(RuntimeError)
84
+ end
85
+
86
+ class OctalInteger
87
+ def self.from_xml(val)
88
+ new(Integer(val.content))
89
+ end
90
+
91
+ def initialize(value)
92
+ @val = value
93
+ end
94
+
95
+ def ==(other)
96
+ @val == other
97
+ end
98
+
99
+ def to_xml
100
+ sprintf("%#o", @val)
101
+ end
102
+ end
103
+
104
+ describe "overriding output" do
105
+ class BookWithOctalPages
106
+ include ROXML
107
+
108
+ xml_accessor :pages_with_as, :as => Integer, :to_xml => proc {|val| sprintf("%#o", val) }, :required => true
109
+ xml_accessor :pages_with_type, :as => OctalInteger, :required => true
110
+ end
111
+
112
+ # to_xml_test :book_with_octal_pages
113
+
114
+ describe "with :to_xml option" do
115
+ it "should output with to_xml filtering"
116
+ end
117
+
118
+ describe "with #to_xml on the object" do
119
+ it "should output with to_xml filtering"
120
+ end
121
+ end
122
+
123
+ describe "overriding input" do
124
+ before do
125
+ @book_with_octal_pages_xml = %{
126
+ <book>
127
+ <pages>0357</pages>
128
+ </book>
129
+ }
130
+
131
+ @expected_pages = 239
132
+ end
133
+
134
+ describe "with :as block shorthand" do
135
+ class BookWithOctalPagesBlockShorthand
136
+ include ROXML
137
+
138
+ xml_accessor :pages, :as => Integer, :required => true
139
+ end
140
+
141
+ it "should apply filtering on input" do
142
+ book = BookWithOctalPagesBlockShorthand.from_xml(@book_with_octal_pages_xml)
143
+ book.pages.should == @expected_pages
144
+ end
145
+ end
146
+
147
+ describe "with #from_xml defined on the object" do
148
+ class BookWithOctalPagesType
149
+ include ROXML
150
+
151
+ xml_accessor :pages, :as => OctalInteger, :required => true
152
+ end
153
+
154
+ it "should apply filtering on input" do
155
+ book = BookWithOctalPagesType.from_xml(@book_with_octal_pages_xml)
156
+ book.pages.should == @expected_pages
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "attribute reference" do
162
+ before do
163
+ @frozen = @readonly.isbn
164
+ @unfrozen = @writable.isbn
165
+ end
166
+
167
+ it_should_behave_like "freezable xml reference"
168
+ end
169
+
170
+ describe "text reference" do
171
+ before do
172
+ @frozen = @readonly.title
173
+ @unfrozen = @writable.title
174
+ end
175
+
176
+ it_should_behave_like "freezable xml reference"
177
+ end
178
+
179
+ describe "object reference" do
180
+ before do
181
+ @frozen = @readonly.description
182
+ @unfrozen = @writable.description
183
+ end
184
+
185
+ it_should_behave_like "freezable xml reference"
186
+
187
+ describe "indirect reference via an object" do
188
+ it "does not inherit the frozen status from its parent" do
189
+ @frozen.writable.frozen?.should be_false
190
+ @frozen.readonly.frozen?.should be_true
191
+
192
+ @unfrozen.writable.frozen?.should be_false
193
+ @unfrozen.readonly.frozen?.should be_true
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "array reference" do
199
+ before do
200
+ @frozen = @readonly.contributions
201
+ @unfrozen = @writable.contributions
202
+ end
203
+
204
+ it_should_behave_like "freezable xml reference"
205
+
206
+ it "should apply :frozen to the constituent elements" do
207
+ @frozen.all?(&:frozen?).should be_true
208
+ @unfrozen.any?(&:frozen?).should be_false
209
+ end
210
+
211
+ context "no elements are present in root, no :in is specified" do
212
+ class BookWithContributors
213
+ include ROXML
214
+
215
+ xml_name :book
216
+ xml_reader :isbn, :from => :attr
217
+ xml_reader :title
218
+ xml_reader :description
219
+ xml_reader :contributors, :as => [Contributor]
220
+ end
221
+
222
+ it "should look for elements :in the plural of name" do
223
+ book = BookWithContributors.from_xml(%{
224
+ <book isbn="0974514055">
225
+ <contributors>
226
+ <contributor role="author"><name>David Thomas</name></contributor>
227
+ <contributor role="supporting author"><name>Andrew Hunt</name></contributor>
228
+ <contributor role="supporting author"><name>Chad Fowler</name></contributor>
229
+ </contributors>
230
+ </book>
231
+ })
232
+ book.contributors.map(&:name).sort.should == ["David Thomas","Andrew Hunt","Chad Fowler"].sort
233
+ end
234
+ end
235
+ end
236
+
237
+ describe "hash reference" do
238
+ class DictionaryOfGuardedNames
239
+ include ROXML
240
+
241
+ xml_name :dictionary
242
+ xml_reader :definitions, :as => {:key => :name,
243
+ :value => :content}, :in => :definitions
244
+ end
245
+
246
+ class DictionaryOfGuardedNamesReadonly
247
+ include ROXML
248
+
249
+ xml_name :dictionary
250
+ xml_reader :definitions, :as => {:key => :name,
251
+ :value => :content}, :in => :definitions, :frozen => true
252
+ end
253
+
254
+ before do
255
+ @frozen = DictionaryOfGuardedNamesReadonly.from_xml(fixture(:dictionary_of_guarded_names)).definitions
256
+ @unfrozen = DictionaryOfGuardedNames.from_xml(fixture(:dictionary_of_guarded_names)).definitions
257
+ end
258
+
259
+ it_should_behave_like "freezable xml reference"
260
+
261
+ it "should have frozen keys, as with all hashes" do
262
+ @frozen.keys.all?(&:frozen?).should be_true
263
+ @unfrozen.keys.all?(&:frozen?).should be_true
264
+ end
265
+
266
+ it "should apply :frozen to the constituent values" do
267
+ @frozen.values.all?(&:frozen?).should be_true
268
+ @unfrozen.values.any?(&:frozen?).should be_false
269
+ end
270
+ end
271
+ end
272
+
273
+ describe ROXML, "inheritance" do
274
+ class Book
275
+ include ROXML
276
+
277
+ xml_accessor :isbn, :from => '@ISBN'
278
+ xml_reader :title
279
+ xml_reader :description, :cdata => true
280
+ xml_reader :author
281
+ xml_accessor :pages, :from => 'pagecount', :as => Integer
282
+ end
283
+
284
+ class Measurement
285
+ include ROXML
286
+
287
+ xml_reader :units, :from => :attr
288
+ xml_reader :value, :from => :content, :as => Float
289
+
290
+ def initialize(value = 0, units = 'pixels')
291
+ @value = Float(value)
292
+ @units = units.to_s
293
+ normalize_hundredths
294
+ end
295
+
296
+ def to_s
297
+ "#{value} #{units}"
298
+ end
299
+
300
+ def ==(other)
301
+ other.units == @units && other.value == @value
302
+ end
303
+
304
+ private
305
+ def after_parse
306
+ normalize_hundredths
307
+ end
308
+
309
+ def normalize_hundredths
310
+ if @units.starts_with? 'hundredths-'
311
+ @value /= 100
312
+ @units = @units.split('hundredths-')[1]
313
+ end
314
+ end
315
+ end
316
+
317
+ class InheritedBookWithDepth < Book
318
+ xml_reader :depth, :as => Measurement
319
+ end
320
+
321
+ before do
322
+ @book_xml = %{
323
+ <book ISBN="0201710897">
324
+ <title>The PickAxe</title>
325
+ <description><![CDATA[Probably the best Ruby book out there]]></description>
326
+ <author>David Thomas, Andrew Hunt, Dave Thomas</author>
327
+ <depth units="hundredths-meters">1130</depth>
328
+ <publisher>Pragmattic Programmers</publisher>
329
+ </book>
330
+ }
331
+
332
+ @parent = Book.from_xml(@book_xml)
333
+ @child = InheritedBookWithDepth.from_xml(@book_xml)
334
+ end
335
+
336
+ describe "parent" do
337
+ it "should include its attributes" do
338
+ @child.isbn.should == "0201710897"
339
+ @child.title.should == "The PickAxe"
340
+ @child.description.should == "Probably the best Ruby book out there"
341
+ @child.author.should == 'David Thomas, Andrew Hunt, Dave Thomas'
342
+ @child.pages.should == nil
343
+ end
344
+
345
+ it "should not include its child's attributes" do
346
+ @parent.should_not respond_to(:depth)
347
+ end
348
+ end
349
+
350
+ describe "child" do
351
+ it "should include its parent's attributes" do
352
+ @child.isbn.should == @parent.isbn
353
+ @child.title.should == @parent.title
354
+ @child.description.should == @parent.description
355
+ @child.author.should == @parent.author
356
+ @child.pages.should == @parent.pages
357
+ end
358
+
359
+ it "should include its attributes" do
360
+ @child.depth.to_s.should == '11.3 meters'
361
+ end
362
+
363
+ it "should include parent's attributes added after declaration" do
364
+ Book.class_eval do
365
+ xml_reader :publisher, :required => true
366
+ end
367
+
368
+ book = InheritedBookWithDepth.from_xml(@book_xml)
369
+ book.publisher.should == "Pragmattic Programmers"
370
+ end
371
+ end
372
+ end