saxophone 1.0.0

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