saxophone 1.0.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.
@@ -0,0 +1,1218 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Saxophone" do
4
+ describe "element" do
5
+ describe "when parsing a single element" do
6
+ before do
7
+ @klass = Class.new do
8
+ include Saxophone
9
+ element :title
10
+ ancestor :body
11
+ value :something, required: false
12
+ attribute :anything, required: true
13
+ end
14
+ end
15
+
16
+ it "provides mass assignment through initialize method" do
17
+ document = @klass.new(title: "Title")
18
+ expect(document.title).to eq("Title")
19
+ end
20
+
21
+ it "provides an accessor" do
22
+ document = @klass.new
23
+ document.title = "Title"
24
+ expect(document.title).to eq("Title")
25
+ end
26
+
27
+ it "does not overwrites the getter is there is already one present" do
28
+ @klass = Class.new do
29
+ def title
30
+ "#{@title} ***"
31
+ end
32
+
33
+ include Saxophone
34
+ element :title
35
+ end
36
+
37
+ document = @klass.new
38
+ document.title = "Title"
39
+ expect(document.title).to eq("Title ***")
40
+ end
41
+
42
+ it "does not overwrites the setter if there is already one present" do
43
+ @klass = Class.new do
44
+ def title=(val)
45
+ @title = "#{val} **"
46
+ end
47
+
48
+ include Saxophone
49
+ element :title
50
+ end
51
+
52
+ document = @klass.new
53
+ document.title = "Title"
54
+ expect(document.title).to eq("Title **")
55
+ end
56
+
57
+ it "does not overwrites the accessor when the element is not present" do
58
+ document = @klass.new
59
+ document.title = "Title"
60
+ document.parse("<foo></foo>")
61
+ expect(document.title).to eq("Title")
62
+ end
63
+
64
+ it "overwrites the value when the element is present" do
65
+ document = @klass.new
66
+ document.title = "Old title"
67
+ document.parse("<title>New title</title>")
68
+ expect(document.title).to eq("New title")
69
+ end
70
+
71
+ it "saves the element text into an accessor" do
72
+ document = @klass.parse("<title>My Title</title>")
73
+ expect(document.title).to eq("My Title")
74
+ end
75
+
76
+ it "keeps the document encoding for elements" do
77
+ data = "<title>My Title</title>"
78
+ data.encode!("utf-8")
79
+
80
+ document = @klass.parse(data)
81
+ expect(document.title.encoding).to eq(data.encoding)
82
+ end
83
+
84
+ it "saves cdata into an accessor" do
85
+ document = @klass.parse("<title><![CDATA[A Title]]></title>")
86
+ expect(document.title).to eq("A Title")
87
+ end
88
+
89
+ it "saves the element text into an accessor when there are multiple elements" do
90
+ document = @klass.parse("<xml><title>My Title</title><foo>bar</foo></xml>")
91
+ expect(document.title).to eq("My Title")
92
+ end
93
+
94
+ it "saves the first element text when there are multiple of the same element" do
95
+ document = @klass.parse("<xml><title>My Title</title><title>bar</title></xml>")
96
+ expect(document.title).to eq("My Title")
97
+ end
98
+
99
+ describe "the introspection" do
100
+ it "allows to get column names" do
101
+ expect(@klass.column_names).to match_array([:title])
102
+ end
103
+
104
+ it "allows to get elements" do
105
+ expect(@klass.sax_config.top_level_elements.values.flatten.map(&:to_s)).to \
106
+ match_array(["name: title dataclass: setter: title= required: value: as:title collection: with: {}"])
107
+ end
108
+
109
+ it "allows to get ancestors" do
110
+ expect(@klass.sax_config.ancestors.map(&:column)).to \
111
+ match_array([:body])
112
+ end
113
+
114
+ it "allows to get values" do
115
+ expect(@klass.sax_config.top_level_element_value.map(&:column)).to \
116
+ match_array([:something])
117
+ expect(@klass.sax_config.top_level_element_value.map(&:required?)).to \
118
+ match_array([false])
119
+ end
120
+
121
+ it "allows to get attributes" do
122
+ expect(@klass.sax_config.top_level_attributes.map(&:column)).to \
123
+ match_array([:anything])
124
+ expect(@klass.sax_config.top_level_attributes.map(&:required?)).to \
125
+ match_array([true])
126
+ expect(@klass.sax_config.top_level_attributes.map(&:collection?)).to \
127
+ match_array([false])
128
+ end
129
+ end
130
+
131
+ describe "the class attribute" do
132
+ before(:each) do
133
+ @klass = Class.new do
134
+ include Saxophone
135
+ element :date, class: DateTime
136
+ end
137
+
138
+ @document = @klass.new
139
+ @document.date = Time.now.iso8601
140
+ end
141
+
142
+ it "is available" do
143
+ expect(@klass.data_class(:date)).to eq(DateTime)
144
+ end
145
+
146
+ describe "string" do
147
+ before do
148
+ class TestString
149
+ include Saxophone
150
+ element :number, class: String
151
+ end
152
+
153
+ class TestStringAttribute
154
+ include Saxophone
155
+ attribute :sub_number, class: String
156
+ end
157
+
158
+ class TestStringWithAttribute
159
+ include Saxophone
160
+ element :number, class: TestStringAttribute
161
+ end
162
+ end
163
+
164
+ it "is handled in an element" do
165
+ document = TestString.parse("<number>5.5</number>")
166
+ expect(document.number).to eq("5.5")
167
+ end
168
+
169
+ it "is handled in an attribute" do
170
+ document = TestStringWithAttribute.parse("<number sub_number='5.5'></number>")
171
+ expect(document.number.sub_number).to eq("5.5")
172
+ end
173
+ end
174
+
175
+ describe "integer" do
176
+ before do
177
+ class TestInteger
178
+ include Saxophone
179
+ element :number, class: Integer
180
+ end
181
+
182
+ class TestIntegerAttribute
183
+ include Saxophone
184
+ attribute :sub_number, class: Integer
185
+ end
186
+
187
+ class TestIntegerWithAttribute
188
+ include Saxophone
189
+ element :number, class: TestIntegerAttribute
190
+ end
191
+
192
+ class IntegerInsideAttribute
193
+ include Saxophone
194
+ element :number, value: :int_attr, as: :int_attr, class: Integer
195
+ end
196
+ end
197
+
198
+ it "is handled in an element" do
199
+ document = TestInteger.parse("<number>5</number>")
200
+ expect(document.number).to eq(5)
201
+ end
202
+
203
+ it "is handled in an attribute" do
204
+ document = TestIntegerWithAttribute.parse("<number sub_number='5'></number>")
205
+ expect(document.number.sub_number).to eq(5)
206
+ end
207
+
208
+ it "is handled in an attribute with value option" do
209
+ document = IntegerInsideAttribute.parse("<number int_attr='2'></number>")
210
+ expect(document.int_attr).to eq(2)
211
+ end
212
+ end
213
+
214
+ describe "float" do
215
+ before do
216
+ class TestFloat
217
+ include Saxophone
218
+ element :number, class: Float
219
+ end
220
+
221
+ class TestFloatAttribute
222
+ include Saxophone
223
+ attribute :sub_number, class: Float
224
+ end
225
+
226
+ class TestFloatWithAttribute
227
+ include Saxophone
228
+ element :number, class: TestFloatAttribute
229
+ end
230
+ end
231
+
232
+ it "is handled in an element with '.' delimiter" do
233
+ document = TestFloat.parse("<number>5.5</number>")
234
+ expect(document.number).to eq(5.5)
235
+ end
236
+
237
+ it "is handled in an element with ',' delimiter" do
238
+ document = TestFloat.parse("<number>5,5</number>")
239
+ expect(document.number).to eq(5.5)
240
+ end
241
+
242
+ it "is handled in an attribute" do
243
+ document = TestFloatWithAttribute.parse("<number sub_number='5.5'>5.5</number>")
244
+ expect(document.number.sub_number).to eq(5.5)
245
+ end
246
+ end
247
+
248
+ describe "symbol" do
249
+ before do
250
+ class TestSymbol
251
+ include Saxophone
252
+ element :symbol, class: Symbol
253
+ end
254
+
255
+ class TestSymbolAttribute
256
+ include Saxophone
257
+ attribute :sub_symbol, class: Symbol
258
+ end
259
+
260
+ class TestSymbolWithAttribute
261
+ include Saxophone
262
+ element :symbol, class: TestSymbolAttribute
263
+ end
264
+ end
265
+
266
+ it "is handled in an element" do
267
+ document = TestSymbol.parse("<symbol>MY_SYMBOL_VALUE</symbol>")
268
+ expect(document.symbol).to eq(:my_symbol_value)
269
+ end
270
+
271
+ it "is handled in an attribute" do
272
+ document = TestSymbolWithAttribute.parse("<symbol sub_symbol='MY_SYMBOL_VALUE'></symbol>")
273
+ expect(document.symbol.sub_symbol).to eq(:my_symbol_value)
274
+ end
275
+ end
276
+
277
+ describe "time" do
278
+ before do
279
+ class TestTime
280
+ include Saxophone
281
+ element :time, class: Time
282
+ end
283
+
284
+ class TestTimeAttribute
285
+ include Saxophone
286
+ attribute :sub_time, class: Time
287
+ end
288
+
289
+ class TestTimeWithAttribute
290
+ include Saxophone
291
+ element :time, class: TestTimeAttribute
292
+ end
293
+ end
294
+
295
+ it "is handled in an element" do
296
+ document = TestTime.parse("<time>1994-02-04T06:20:00Z</time>")
297
+ expect(document.time).to eq(Time.utc(1994, 2, 4, 6, 20, 0, 0))
298
+ end
299
+
300
+ it "is handled in an attribute" do
301
+ document = TestTimeWithAttribute.parse("<time sub_time='1994-02-04T06:20:00Z'>1994-02-04T06:20:00Z</time>")
302
+ expect(document.time.sub_time).to eq(Time.utc(1994, 2, 4, 6, 20, 0, 0))
303
+ end
304
+ end
305
+ end
306
+
307
+ describe "the default attribute" do
308
+ it "is available" do
309
+ @klass = Class.new do
310
+ include Saxophone
311
+ element :number, class: Integer, default: 0
312
+ end
313
+
314
+ document = @klass.parse("<no>number</no>")
315
+ expect(document.number).to eq(0)
316
+
317
+ document = @klass.parse("<number></number>")
318
+ expect(document.number).to eq(0)
319
+ end
320
+
321
+ it "can be a Boolean" do
322
+ @klass = Class.new do
323
+ include Saxophone
324
+ element(:bool, default: false) { |v| !!v }
325
+ end
326
+
327
+ document = @klass.parse("<no>bool</no>")
328
+ expect(document.bool).to be false
329
+
330
+ document = @klass.parse("<bool></bool>")
331
+ expect(document.bool).to be false
332
+
333
+ document = @klass.parse("<bool>1</bool>")
334
+ expect(document.bool).to be true
335
+ end
336
+ end
337
+
338
+ describe "the required attribute" do
339
+ it "is available" do
340
+ @klass = Class.new do
341
+ include Saxophone
342
+ element :date, required: true
343
+ end
344
+ expect(@klass.required?(:date)).to be_truthy
345
+ end
346
+ end
347
+
348
+ describe "the block" do
349
+ before do
350
+ class ElementBlockParser
351
+ include Saxophone
352
+
353
+ ancestor :parent do |parent|
354
+ parent.class.to_s
355
+ end
356
+
357
+ value :text do |text|
358
+ text.downcase
359
+ end
360
+ end
361
+
362
+ class BlockParser
363
+ include Saxophone
364
+
365
+ element :title do |title|
366
+ "#{title}!!!"
367
+ end
368
+
369
+ element :scope do |scope|
370
+ "#{title} #{scope}"
371
+ end
372
+
373
+ attribute :id do |id|
374
+ id.to_i
375
+ end
376
+
377
+ element :nested, class: ElementBlockParser
378
+ elements :message, as: :messages do |message|
379
+ "#{message}!"
380
+ end
381
+ end
382
+ end
383
+
384
+ it "has instance as a block context" do
385
+ document = BlockParser.parse("<root><title>SAX</title><scope>something</scope></root>")
386
+ expect(document.scope).to eq("SAX!!! something")
387
+ end
388
+
389
+ it "uses block for element" do
390
+ document = BlockParser.parse("<title>SAX</title>")
391
+ expect(document.title).to eq("SAX!!!")
392
+ end
393
+
394
+ it 'uses block for attribute' do
395
+ document = BlockParser.parse("<title id='345'>SAX</title>")
396
+ expect(document.id).to eq(345)
397
+ end
398
+
399
+ it "uses block for value" do
400
+ document = BlockParser.parse("<title><nested>tEst</nested></title>")
401
+ expect(document.nested.text).to eq("test")
402
+ end
403
+
404
+ it "uses block for ancestor" do
405
+ document = BlockParser.parse("<title><nested>SAX</nested></title>")
406
+ expect(document.nested.parent).to eq("BlockParser")
407
+ end
408
+
409
+ it "uses block for elements" do
410
+ document = BlockParser.parse("<title><message>hi</message><message>world</message></title>")
411
+ expect(document.messages).to eq(["hi!", "world!"])
412
+ end
413
+ end
414
+ end
415
+
416
+ describe "when parsing multiple elements" do
417
+ before do
418
+ @klass = Class.new do
419
+ include Saxophone
420
+ element :title
421
+ element :name
422
+ end
423
+ end
424
+
425
+ it "saves the element text for a second tag" do
426
+ document = @klass.parse("<xml><title>My Title</title><name>Paul</name></xml>")
427
+ expect(document.name).to eq("Paul")
428
+ expect(document.title).to eq("My Title")
429
+ end
430
+
431
+ it "does not overwrites the getter is there is already one present" do
432
+ @klass = Class.new do
433
+ def items
434
+ []
435
+ end
436
+
437
+ include Saxophone
438
+ elements :items
439
+ end
440
+
441
+ document = @klass.new
442
+ document.items = [1, 2, 3, 4]
443
+ expect(document.items).to eq([])
444
+ end
445
+
446
+ it "does not overwrites the setter if there is already one present" do
447
+ @klass = Class.new do
448
+ def items=(val)
449
+ @items = [1, *val]
450
+ end
451
+
452
+ include Saxophone
453
+ elements :items
454
+ end
455
+
456
+ document = @klass.new
457
+ document.items = [2, 3]
458
+ expect(document.items).to eq([1, 2, 3])
459
+ end
460
+ end
461
+
462
+ describe "when using options for parsing elements" do
463
+ describe "using the 'as' option" do
464
+ before do
465
+ @klass = Class.new do
466
+ include Saxophone
467
+ element :description, as: :summary
468
+ end
469
+ end
470
+
471
+ it "provides an accessor using the 'as' name" do
472
+ document = @klass.new
473
+ document.summary = "a small summary"
474
+ expect(document.summary).to eq("a small summary")
475
+ end
476
+
477
+ it "saves the element text into the 'as' accessor" do
478
+ document = @klass.parse("<description>here is a description</description>")
479
+ expect(document.summary).to eq("here is a description")
480
+ end
481
+ end
482
+
483
+ describe "using the :with option" do
484
+ describe "and the :value option" do
485
+ before do
486
+ @klass = Class.new do
487
+ include Saxophone
488
+ element :link, value: :href, with: { foo: "bar" }
489
+ end
490
+ end
491
+
492
+ it "saves the value of a matching element" do
493
+ document = @klass.parse("<link href='test' foo='bar'>asdf</link>")
494
+ expect(document.link).to eq("test")
495
+ end
496
+
497
+ it "saves the value of the first matching element" do
498
+ document = @klass.parse("<xml><link href='first' foo='bar' /><link href='second' foo='bar' /></xml>")
499
+ expect(document.link).to eq("first")
500
+ end
501
+
502
+ describe "and the :as option" do
503
+ before do
504
+ @klass = Class.new do
505
+ include Saxophone
506
+ element :link, value: :href, as: :url, with: { foo: "bar" }
507
+ element :link, value: :href, as: :second_url, with: { asdf: "jkl" }
508
+ end
509
+ end
510
+
511
+ it "saves the value of the first matching element" do
512
+ document = @klass.parse("<xml><link href='first' foo='bar' /><link href='second' asdf='jkl' /><link href='second' foo='bar' /></xml>")
513
+ expect(document.url).to eq("first")
514
+ expect(document.second_url).to eq("second")
515
+ end
516
+ end
517
+ end
518
+
519
+ describe "with only one element" do
520
+ before do
521
+ @klass = Class.new do
522
+ include Saxophone
523
+ element :link, with: { foo: "bar" }
524
+ end
525
+ end
526
+
527
+ it "saves the text of an element that has matching attributes" do
528
+ document = @klass.parse("<link foo=\"bar\">match</link>")
529
+ expect(document.link).to eq("match")
530
+ end
531
+
532
+ it "does not saves the text of an element that doesn't have matching attributes" do
533
+ document = @klass.parse("<link>no match</link>")
534
+ expect(document.link).to be_nil
535
+ end
536
+
537
+ it "saves the text of an element that has matching attributes when it is the second of that type" do
538
+ document = @klass.parse("<xml><link>no match</link><link foo=\"bar\">match</link></xml>")
539
+ expect(document.link).to eq("match")
540
+ end
541
+
542
+ it "saves the text of an element that has matching attributes plus a few more" do
543
+ document = @klass.parse("<xml><link>no match</link><link asdf='jkl' foo='bar'>match</link>")
544
+ expect(document.link).to eq("match")
545
+ end
546
+ end
547
+
548
+ describe "with multiple elements of same tag" do
549
+ before do
550
+ @klass = Class.new do
551
+ include Saxophone
552
+ element :link, as: :first, with: { foo: "bar" }
553
+ element :link, as: :second, with: { asdf: "jkl" }
554
+ end
555
+ end
556
+
557
+ it "matches the first element" do
558
+ document = @klass.parse("<xml><link>no match</link><link foo=\"bar\">first match</link><link>no match</link></xml>")
559
+ expect(document.first).to eq("first match")
560
+ end
561
+
562
+ it "matches the second element" do
563
+ document = @klass.parse("<xml><link>no match</link><link foo='bar'>first match</link><link asdf='jkl'>second match</link><link>hi</link></xml>")
564
+ expect(document.second).to eq("second match")
565
+ end
566
+ end
567
+
568
+ describe "with only one element as a regular expression" do
569
+ before do
570
+ @klass = Class.new do
571
+ include Saxophone
572
+ element :link, with: { foo: /ar$/ }
573
+ end
574
+ end
575
+
576
+ it "saves the text of an element that has matching attributes" do
577
+ document = @klass.parse("<link foo=\"bar\">match</link>")
578
+ expect(document.link).to eq("match")
579
+ end
580
+
581
+ it "does not saves the text of an element that doesn't have matching attributes" do
582
+ document = @klass.parse("<link>no match</link>")
583
+ expect(document.link).to be_nil
584
+ end
585
+
586
+ it "saves the text of an element that has matching attributes when it is the second of that type" do
587
+ document = @klass.parse("<xml><link>no match</link><link foo=\"bar\">match</link></xml>")
588
+ expect(document.link).to eq("match")
589
+ end
590
+
591
+ it "saves the text of an element that has matching attributes plus a few more" do
592
+ document = @klass.parse("<xml><link>no match</link><link asdf='jkl' foo='bar'>match</link>")
593
+ expect(document.link).to eq("match")
594
+ end
595
+ end
596
+ end
597
+
598
+ describe "using the 'value' option" do
599
+ before do
600
+ @klass = Class.new do
601
+ include Saxophone
602
+ element :link, value: :foo
603
+ end
604
+ end
605
+
606
+ it "saves the attribute value" do
607
+ document = @klass.parse("<link foo='test'>hello</link>")
608
+ expect(document.link).to eq("test")
609
+ end
610
+
611
+ it "saves the attribute value when there is no text enclosed by the tag" do
612
+ document = @klass.parse("<link foo='test'></link>")
613
+ expect(document.link).to eq("test")
614
+ end
615
+
616
+ it "saves the attribute value when the tag close is in the open" do
617
+ document = @klass.parse("<link foo='test'/>")
618
+ expect(document.link).to eq("test")
619
+ end
620
+
621
+ it "saves two different attribute values on a single tag" do
622
+ @klass = Class.new do
623
+ include Saxophone
624
+ element :link, value: :foo, as: :first
625
+ element :link, value: :bar, as: :second
626
+ end
627
+
628
+ document = @klass.parse("<link foo='foo value' bar='bar value'></link>")
629
+ expect(document.first).to eq("foo value")
630
+ expect(document.second).to eq("bar value")
631
+ end
632
+
633
+ it "does not fail if one of the attribute hasn't been defined" do
634
+ @klass = Class.new do
635
+ include Saxophone
636
+ element :link, value: :foo, as: :first
637
+ element :link, value: :bar, as: :second
638
+ end
639
+
640
+ document = @klass.parse("<link foo='foo value'></link>")
641
+ expect(document.first).to eq("foo value")
642
+ expect(document.second).to be_nil
643
+ end
644
+ end
645
+
646
+ describe "when desiring both the content and attributes of an element" do
647
+ before do
648
+ @klass = Class.new do
649
+ include Saxophone
650
+ element :link
651
+ element :link, value: :foo, as: :link_foo
652
+ element :link, value: :bar, as: :link_bar
653
+ end
654
+ end
655
+
656
+ it "parses the element and attribute values" do
657
+ document = @klass.parse("<link foo='test1' bar='test2'>hello</link>")
658
+ expect(document.link).to eq("hello")
659
+ expect(document.link_foo).to eq("test1")
660
+ expect(document.link_bar).to eq("test2")
661
+ end
662
+ end
663
+ end
664
+ end
665
+
666
+ describe "elements" do
667
+ describe "when parsing multiple elements" do
668
+ before do
669
+ @klass = Class.new do
670
+ include Saxophone
671
+ elements :entry, as: :entries
672
+ end
673
+ end
674
+
675
+ it "provides a collection accessor" do
676
+ document = @klass.new
677
+ document.entries << :foo
678
+ expect(document.entries).to eq([:foo])
679
+ end
680
+
681
+ it "parses a single element" do
682
+ document = @klass.parse("<entry>hello</entry>")
683
+ expect(document.entries).to eq(["hello"])
684
+ end
685
+
686
+ it "parses multiple elements" do
687
+ document = @klass.parse("<xml><entry>hello</entry><entry>world</entry></xml>")
688
+ expect(document.entries).to eq(["hello", "world"])
689
+ end
690
+
691
+ it "parses multiple elements when taking an attribute value" do
692
+ attribute_klass = Class.new do
693
+ include Saxophone
694
+ elements :entry, as: :entries, value: :foo
695
+ end
696
+
697
+ doc = attribute_klass.parse("<xml><entry foo='asdf' /><entry foo='jkl' /></xml>")
698
+ expect(doc.entries).to eq(["asdf", "jkl"])
699
+ end
700
+ end
701
+
702
+ describe "when using the with and class options" do
703
+ before do
704
+ class Bar
705
+ include Saxophone
706
+ element :title
707
+ end
708
+
709
+ class Foo
710
+ include Saxophone
711
+ element :title
712
+ end
713
+
714
+ class Item
715
+ include Saxophone
716
+ end
717
+
718
+ @klass = Class.new do
719
+ include Saxophone
720
+ elements :item, as: :items, with: { type: "Bar" }, class: Bar
721
+ elements :item, as: :items, with: { type: /Foo/ }, class: Foo
722
+ end
723
+ end
724
+
725
+ it "casts into the correct class" do
726
+ document = @klass.parse("<items><item type=\"Bar\"><title>Bar title</title></item><item type=\"Foo\"><title>Foo title</title></item></items>")
727
+ expect(document.items.size).to eq(2)
728
+ expect(document.items.first).to be_a(Bar)
729
+ expect(document.items.first.title).to eq("Bar title")
730
+ expect(document.items.last).to be_a(Foo)
731
+ expect(document.items.last.title).to eq("Foo title")
732
+ end
733
+ end
734
+
735
+ describe "when using the class option" do
736
+ before do
737
+ class Foo
738
+ include Saxophone
739
+ element :title
740
+ end
741
+
742
+ @klass = Class.new do
743
+ include Saxophone
744
+ elements :entry, as: :entries, class: Foo
745
+ end
746
+ end
747
+
748
+ it "parses a single element with children" do
749
+ document = @klass.parse("<entry><title>a title</title></entry>")
750
+ expect(document.entries.size).to eq(1)
751
+ expect(document.entries.first.title).to eq("a title")
752
+ end
753
+
754
+ it "parses multiple elements with children" do
755
+ document = @klass.parse("<xml><entry><title>title 1</title></entry><entry><title>title 2</title></entry></xml>")
756
+ expect(document.entries.size).to eq(2)
757
+ expect(document.entries.first.title).to eq("title 1")
758
+ expect(document.entries.last.title).to eq("title 2")
759
+ end
760
+
761
+ it "does not parse a top level element that is specified only in a child" do
762
+ document = @klass.parse("<xml><title>no parse</title><entry><title>correct title</title></entry></xml>")
763
+ expect(document.entries.size).to eq(1)
764
+ expect(document.entries.first.title).to eq("correct title")
765
+ end
766
+
767
+ it "parses elements, and make attributes and inner text available" do
768
+ class Related
769
+ include Saxophone
770
+ element "related", as: :item
771
+ element "related", as: :attr, value: "attr"
772
+ end
773
+
774
+ class Foo
775
+ elements "related", as: "items", class: Related
776
+ end
777
+
778
+ doc = Foo.parse(%{<xml><collection><related attr='baz'>something</related><related>somethingelse</related></collection></xml>})
779
+ expect(doc.items.first).not_to be_nil
780
+ expect(doc.items.size).to eq(2)
781
+ expect(doc.items.first.item).to eq("something")
782
+ expect(doc.items.last.item).to eq("somethingelse")
783
+ end
784
+
785
+ it "parses out an attribute value from the tag that starts the collection" do
786
+ class Foo
787
+ element :entry, value: :href, as: :url
788
+ end
789
+
790
+ document = @klass.parse("<xml><entry href='http://pauldix.net'><title>paul</title></entry></xml>")
791
+ expect(document.entries.size).to eq(1)
792
+ expect(document.entries.first.title).to eq("paul")
793
+ expect(document.entries.first.url).to eq("http://pauldix.net")
794
+ end
795
+ end
796
+ end
797
+
798
+ describe "when dealing with element names containing dashes" do
799
+ it "converts dashes to underscores" do
800
+ class Dashes
801
+ include Saxophone
802
+ element :dashed_element
803
+ end
804
+
805
+ parsed = Dashes.parse("<dashed-element>Text</dashed-element>")
806
+ expect(parsed.dashed_element).to eq "Text"
807
+ end
808
+ end
809
+
810
+ describe "full example" do
811
+ before do
812
+ @xml = File.read("spec/fixtures/atom.xml")
813
+
814
+ class AtomEntry
815
+ include Saxophone
816
+ element :title
817
+ element :name, as: :author
818
+ element "feedburner:origLink", as: :url
819
+ element :link, as: :alternate, value: :href, with: { type: "text/html", rel: "alternate" }
820
+ element :summary
821
+ element :content
822
+ element :published
823
+ end
824
+
825
+ class Atom
826
+ include Saxophone
827
+ element :title
828
+ element :link, value: :href, as: :url, with: { type: "text/html" }
829
+ element :link, value: :href, as: :feed_url, with: { type: "application/atom+xml" }
830
+ elements :entry, as: :entries, class: AtomEntry
831
+ end
832
+
833
+ @feed = Atom.parse(@xml)
834
+ end
835
+
836
+ it "parses the url" do
837
+ expect(@feed.url).to eq("http://www.pauldix.net/")
838
+ end
839
+
840
+ it "parses entry url" do
841
+ expect(@feed.entries.first.url).to eq("http://www.pauldix.net/2008/09/marshal-data-to.html?param1=1&param2=2")
842
+ expect(@feed.entries.first.alternate).to eq("http://feeds.feedburner.com/~r/PaulDixExplainsNothing/~3/383536354/marshal-data-to.html?param1=1&param2=2")
843
+ end
844
+
845
+ it "parses content" do
846
+ expect(@feed.entries.first.content.strip).to eq(File.read("spec/fixtures/atom-content.html").strip)
847
+ end
848
+ end
849
+
850
+ describe "parsing a tree" do
851
+ before do
852
+ @xml = %[
853
+ <categories>
854
+ <category id="1">
855
+ <title>First</title>
856
+ <categories>
857
+ <category id="2">
858
+ <title>Second</title>
859
+ </category>
860
+ </categories>
861
+ </category>
862
+ </categories>
863
+ ]
864
+
865
+ class CategoryCollection; end
866
+
867
+ class Category
868
+ include Saxophone
869
+ attr_accessor :id
870
+ element :category, value: :id, as: :id
871
+ element :title
872
+ element :categories, as: :collection, class: CategoryCollection
873
+ ancestor :ancestor
874
+ end
875
+
876
+ class CategoryCollection
877
+ include Saxophone
878
+ elements :category, as: :categories, class: Category
879
+ end
880
+
881
+ @collection = CategoryCollection.parse(@xml)
882
+ end
883
+
884
+ it "parses the first category" do
885
+ expect(@collection.categories.first.id).to eq("1")
886
+ expect(@collection.categories.first.title).to eq("First")
887
+ expect(@collection.categories.first.ancestor).to eq(@collection)
888
+ end
889
+
890
+ it "parses the nested category" do
891
+ expect(@collection.categories.first.collection.categories.first.id).to eq("2")
892
+ expect(@collection.categories.first.collection.categories.first.title).to eq("Second")
893
+ end
894
+ end
895
+
896
+ describe "parsing a tree without a collection class" do
897
+ before do
898
+ @xml = %[
899
+ <categories>
900
+ <category id="1">
901
+ <title>First</title>
902
+ <categories>
903
+ <category id="2">
904
+ <title>Second</title>
905
+ </category>
906
+ </categories>
907
+ </category>
908
+ </categories>
909
+ ]
910
+
911
+ class CategoryTree
912
+ include Saxophone
913
+ attr_accessor :id
914
+ element :category, value: :id, as: :id
915
+ element :title
916
+ elements :category, as: :categories, class: CategoryTree
917
+ end
918
+
919
+ @collection = CategoryTree.parse(@xml)
920
+ end
921
+
922
+ it "parses the first category" do
923
+ expect(@collection.categories.first.id).to eq("1")
924
+ expect(@collection.categories.first.title).to eq("First")
925
+ end
926
+
927
+ it "parses the nested category" do
928
+ expect(@collection.categories.first.categories.first.id).to eq("2")
929
+ expect(@collection.categories.first.categories.first.title).to eq("Second")
930
+ end
931
+ end
932
+
933
+ describe "with element deeper inside the xml structure" do
934
+ before do
935
+ @xml = %[
936
+ <item id="1">
937
+ <texts>
938
+ <title>Hello</title>
939
+ </texts>
940
+ </item>
941
+ ]
942
+
943
+ @klass = Class.new do
944
+ include Saxophone
945
+ attr_accessor :id
946
+ element :item, value: "id", as: :id
947
+ element :title
948
+ end
949
+
950
+ @item = @klass.parse(@xml)
951
+ end
952
+
953
+ it "has an id" do
954
+ expect(@item.id).to eq("1")
955
+ end
956
+
957
+ it "has a title" do
958
+ expect(@item.title).to eq("Hello")
959
+ end
960
+ end
961
+
962
+ describe "with config to pull multiple attributes" do
963
+ before do
964
+ @xml = %[
965
+ <item id="1">
966
+ <author name="John Doe" role="writer" />
967
+ </item>
968
+ ]
969
+
970
+ class AuthorElement
971
+ include Saxophone
972
+ attribute :name
973
+ attribute :role
974
+ end
975
+
976
+ class ItemElement
977
+ include Saxophone
978
+ element :author, class: AuthorElement
979
+ end
980
+
981
+ @item = ItemElement.parse(@xml)
982
+ end
983
+
984
+ it "has the child element" do
985
+ expect(@item.author).not_to be_nil
986
+ end
987
+
988
+ it "has the author name" do
989
+ expect(@item.author.name).to eq("John Doe")
990
+ end
991
+
992
+ it "has the author role" do
993
+ expect(@item.author.role).to eq("writer")
994
+ end
995
+ end
996
+
997
+ describe "with multiple elements and multiple attributes" do
998
+ before do
999
+ @xml = %[
1000
+ <item id="1">
1001
+ <author name="John Doe" role="writer" />
1002
+ <author name="Jane Doe" role="artist" />
1003
+ </item>
1004
+ ]
1005
+
1006
+ class AuthorElement2
1007
+ include Saxophone
1008
+ attribute :name
1009
+ attribute :role
1010
+ end
1011
+
1012
+ class ItemElement2
1013
+ include Saxophone
1014
+ elements :author, as: :authors, class: AuthorElement2
1015
+ end
1016
+
1017
+ @item = ItemElement2.parse(@xml)
1018
+ end
1019
+
1020
+ it "has the child elements" do
1021
+ expect(@item.authors).not_to be_nil
1022
+ expect(@item.authors.count).to eq(2)
1023
+ end
1024
+
1025
+ it "has the author names" do
1026
+ expect(@item.authors.first.name).to eq("John Doe")
1027
+ expect(@item.authors.last.name).to eq("Jane Doe")
1028
+ end
1029
+
1030
+ it "has the author roles" do
1031
+ expect(@item.authors.first.role).to eq("writer")
1032
+ expect(@item.authors.last.role).to eq("artist")
1033
+ end
1034
+ end
1035
+
1036
+ describe "with mixed attributes and element values" do
1037
+ before do
1038
+ @xml = %[
1039
+ <item id="1">
1040
+ <author role="writer">John Doe</author>
1041
+ </item>
1042
+ ]
1043
+
1044
+ class AuthorElement3
1045
+ include Saxophone
1046
+ value :name
1047
+ attribute :role
1048
+ end
1049
+
1050
+ class ItemElement3
1051
+ include Saxophone
1052
+ element :author, class: AuthorElement3
1053
+ end
1054
+
1055
+ @item = ItemElement3.parse(@xml)
1056
+ end
1057
+
1058
+ it "has the child elements" do
1059
+ expect(@item.author).not_to be_nil
1060
+ end
1061
+
1062
+ it "has the author names" do
1063
+ expect(@item.author.name).to eq("John Doe")
1064
+ end
1065
+
1066
+ it "has the author roles" do
1067
+ expect(@item.author.role).to eq("writer")
1068
+ end
1069
+ end
1070
+
1071
+ describe "with multiple mixed attributes and element values" do
1072
+ before do
1073
+ @xml = %[
1074
+ <item id="1">
1075
+ <title>sweet</title>
1076
+ <author role="writer">John Doe</author>
1077
+ <author role="artist">Jane Doe</author>
1078
+ </item>
1079
+ ]
1080
+
1081
+ class AuthorElement4
1082
+ include Saxophone
1083
+ value :name
1084
+ attribute :role
1085
+ end
1086
+
1087
+ class ItemElement4
1088
+ include Saxophone
1089
+ element :title
1090
+ elements :author, as: :authors, class: AuthorElement4
1091
+
1092
+ def title=(blah)
1093
+ @title = blah
1094
+ end
1095
+ end
1096
+
1097
+ @item = ItemElement4.parse(@xml)
1098
+ end
1099
+
1100
+ it "has the title" do
1101
+ expect(@item.title).to eq("sweet")
1102
+ end
1103
+
1104
+ it "has the child elements" do
1105
+ expect(@item.authors).not_to be_nil
1106
+ expect(@item.authors.count).to eq(2)
1107
+ end
1108
+
1109
+ it "has the author names" do
1110
+ expect(@item.authors.first.name).to eq("John Doe")
1111
+ expect(@item.authors.last.name).to eq("Jane Doe")
1112
+ end
1113
+
1114
+ it "has the author roles" do
1115
+ expect(@item.authors.first.role).to eq("writer")
1116
+ expect(@item.authors.last.role).to eq("artist")
1117
+ end
1118
+ end
1119
+
1120
+ describe "with multiple elements with the same alias" do
1121
+ let(:item) { ItemElement5.parse(xml) }
1122
+
1123
+ before do
1124
+ class ItemElement5
1125
+ include Saxophone
1126
+ element :pubDate, as: :published
1127
+ element :"dc:date", as: :published
1128
+ end
1129
+ end
1130
+
1131
+ describe "only first defined" do
1132
+ let(:xml) { "<item xmlns:dc='http://www.example.com'><pubDate>first value</pubDate></item>" }
1133
+
1134
+ it "has first value" do
1135
+ expect(item.published).to eq("first value")
1136
+ end
1137
+ end
1138
+
1139
+ describe "only last defined" do
1140
+ let(:xml) { "<item xmlns:dc='http://www.example.com'><dc:date>last value</dc:date></item>" }
1141
+
1142
+ it "has last value" do
1143
+ expect(item.published).to eq("last value")
1144
+ end
1145
+ end
1146
+
1147
+ describe "both defined" do
1148
+ let(:xml) { "<item xmlns:dc='http://www.example.com'><pubDate>first value</pubDate><dc:date>last value</dc:date></item>" }
1149
+
1150
+ it "has last value" do
1151
+ expect(item.published).to eq("last value")
1152
+ end
1153
+ end
1154
+
1155
+ describe "both defined but order is reversed" do
1156
+ let(:xml) { "<item xmlns:dc='http://www.example.com'><dc:date>last value</dc:date><pubDate>first value</pubDate></item>" }
1157
+
1158
+ it "has first value" do
1159
+ expect(item.published).to eq("first value")
1160
+ end
1161
+ end
1162
+
1163
+ describe "both defined but last is empty" do
1164
+ let(:xml) { "<item xmlns:dc='http://www.example.com'><pubDate>first value</pubDate><dc:date></dc:date></item>" }
1165
+
1166
+ it "has first value" do
1167
+ expect(item.published).to eq("first value")
1168
+ end
1169
+ end
1170
+ end
1171
+
1172
+ describe "with error handling" do
1173
+ before do
1174
+ @xml = %[
1175
+ <item id="1">
1176
+ <title>sweet</title>
1177
+ ]
1178
+
1179
+ class ItemElement5
1180
+ include Saxophone
1181
+ element :title
1182
+ end
1183
+
1184
+ @errors = []
1185
+ @warnings = []
1186
+ @item = ItemElement5.parse(
1187
+ @xml,
1188
+ ->(x) { @errors << x },
1189
+ ->(x) { @warnings << x },
1190
+ )
1191
+ end
1192
+
1193
+ it "has error" do
1194
+ expect(@errors.uniq.size).to eq(1)
1195
+ end
1196
+
1197
+ it "has no warning" do
1198
+ expect(@warnings.uniq.size).to eq(0)
1199
+ end
1200
+ end
1201
+
1202
+ describe "with io as a input" do
1203
+ before do
1204
+ @io = StringIO.new('<item id="1"><title>sweet</title></item>')
1205
+
1206
+ class IoParser
1207
+ include Saxophone
1208
+ element :title
1209
+ end
1210
+
1211
+ @item = ItemElement5.parse(@io)
1212
+ end
1213
+
1214
+ it "parses" do
1215
+ expect(@item.title).to eq("sweet")
1216
+ end
1217
+ end
1218
+ end