ratom-instructure 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/History.txt +135 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +293 -0
  4. data/Rakefile +59 -0
  5. data/VERSION.yml +5 -0
  6. data/lib/atom.rb +796 -0
  7. data/lib/atom/configuration.rb +24 -0
  8. data/lib/atom/pub.rb +251 -0
  9. data/lib/atom/version.rb +7 -0
  10. data/lib/atom/xml/parser.rb +413 -0
  11. data/ratom.gemspec +95 -0
  12. data/spec/app/member_entry.atom +31 -0
  13. data/spec/app/service.xml +36 -0
  14. data/spec/app/service_xml_base.xml +37 -0
  15. data/spec/atom/pub_spec.rb +517 -0
  16. data/spec/atom_spec.rb +1385 -0
  17. data/spec/conformance/baseuri.atom +19 -0
  18. data/spec/conformance/divtest.atom +32 -0
  19. data/spec/conformance/linktests.xml +103 -0
  20. data/spec/conformance/nondefaultnamespace-baseline.atom +25 -0
  21. data/spec/conformance/nondefaultnamespace-xhtml.atom +25 -0
  22. data/spec/conformance/nondefaultnamespace.atom +25 -0
  23. data/spec/conformance/ordertest.xml +112 -0
  24. data/spec/conformance/title/html-cdata.atom +22 -0
  25. data/spec/conformance/title/html-entity.atom +22 -0
  26. data/spec/conformance/title/html-ncr.atom +22 -0
  27. data/spec/conformance/title/text-cdata.atom +22 -0
  28. data/spec/conformance/title/text-entity.atom +21 -0
  29. data/spec/conformance/title/text-ncr.atom +21 -0
  30. data/spec/conformance/title/xhtml-entity.atom +21 -0
  31. data/spec/conformance/title/xhtml-ncr.atom +21 -0
  32. data/spec/conformance/unknown-namespace.atom +25 -0
  33. data/spec/conformance/xmlbase.atom +133 -0
  34. data/spec/fixtures/complex_single_entry.atom +45 -0
  35. data/spec/fixtures/created_entry.atom +31 -0
  36. data/spec/fixtures/entry.atom +30 -0
  37. data/spec/fixtures/entry_with_custom_extensions.atom +8 -0
  38. data/spec/fixtures/entry_with_simple_extensions.atom +31 -0
  39. data/spec/fixtures/entry_with_single_custom_extension.atom +6 -0
  40. data/spec/fixtures/multiple_entry.atom +0 -0
  41. data/spec/fixtures/simple_single_entry.atom +21 -0
  42. data/spec/fixtures/with_stylesheet.atom +8 -0
  43. data/spec/paging/first_paged_feed.atom +21 -0
  44. data/spec/paging/last_paged_feed.atom +21 -0
  45. data/spec/paging/middle_paged_feed.atom +22 -0
  46. data/spec/property.rb +31 -0
  47. data/spec/spec.opts +1 -0
  48. data/spec/spec_helper.rb +46 -0
  49. metadata +147 -0
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :build:
4
+ :minor: 6
5
+ :patch: 9
@@ -0,0 +1,796 @@
1
+ # Copyright (c) 2008 The Kaphan Foundation
2
+ #
3
+ # For licensing information see LICENSE.
4
+ #
5
+ # Please visit http://www.peerworks.org/contact for further information.
6
+ #
7
+
8
+ require 'forwardable'
9
+ require 'delegate'
10
+ require 'rubygems'
11
+ gem 'libxml-ruby', '>= 1.1.2'
12
+ require 'xml/libxml'
13
+ require 'atom/xml/parser.rb'
14
+
15
+ module Atom # :nodoc:
16
+ NAMESPACE = 'http://www.w3.org/2005/Atom' unless defined?(NAMESPACE)
17
+ module Pub
18
+ NAMESPACE = 'http://www.w3.org/2007/app'
19
+ end
20
+
21
+ # Raised when a Serialization Error occurs.
22
+ class SerializationError < StandardError; end
23
+
24
+ # Provides support for reading and writing simple extensions as defined by the Atom Syndication Format.
25
+ #
26
+ # A Simple extension is an element from a non-atom namespace that has no attributes and only contains
27
+ # text content. It is interpreted as a key-value pair when the namespace and the localname of the
28
+ # extension make up the key. Since in XML you can have many instances of an element, the values are
29
+ # represented as an array of strings, so to manipulate the values manipulate the array returned by
30
+ # +[ns, localname]+.
31
+ #
32
+ module SimpleExtensions
33
+ attr_reader :simple_extensions
34
+
35
+ # Gets a simple extension value for a given namespace and local name.
36
+ #
37
+ # +ns+:: The namespace.
38
+ # +localname+:: The local name of the extension element.
39
+ #
40
+ def [](namespace, localname=nil)
41
+ @simple_extensions ||= {}
42
+
43
+ localname.nil? ? namespace_hash(namespace) : element_values(namespace, localname)
44
+ end
45
+
46
+ protected
47
+
48
+ def namespace_hash(namespace)
49
+ namespace_keys = @simple_extensions.keys.select { |key| key =~ /^\{#{namespace},/ }
50
+
51
+ elements = {}
52
+ namespace_keys.each do |key|
53
+ attribute_name = key.match(/\{.*,(.*)\}/)[1]
54
+ elements[attribute_name] = @simple_extensions[key]
55
+ end
56
+ elements
57
+ end
58
+
59
+ def element_values(namespace, localname)
60
+ key = "{#{namespace},#{localname}}"
61
+ @simple_extensions[key] ||= ValueProxy.new
62
+ end
63
+
64
+ class ValueProxy < DelegateClass(Array)
65
+ attr_accessor :as_attribute
66
+ def initialize
67
+ super([])
68
+ @as_attribute = false
69
+ end
70
+ end
71
+ end
72
+
73
+ # Represents a Generator as defined by the Atom Syndication Format specification.
74
+ #
75
+ # The generator identifies an agent or engine used to a produce a feed.
76
+ #
77
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.generator
78
+ class Generator
79
+ include Xml::Parseable
80
+
81
+ attr_accessor :name
82
+ attribute :uri, :version
83
+
84
+ # Initialize a new Generator.
85
+ #
86
+ # +xml+:: An XML::Reader object.
87
+ #
88
+ def initialize(o = {})
89
+ case o
90
+ when XML::Reader
91
+ @name = o.read_string.strip
92
+ parse(o, :once => true)
93
+ when Hash
94
+ o.each do |k, v|
95
+ self.send("#{k.to_s}=", v)
96
+ end
97
+ else
98
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
99
+ end
100
+
101
+ yield(self) if block_given?
102
+ end
103
+
104
+ def to_xml(nodeonly = true, name = 'generator', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
105
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
106
+ node << @name if @name
107
+ node['uri'] = @uri if @uri
108
+ node['version'] = @version if @version
109
+ node
110
+ end
111
+ end
112
+
113
+ # Represents a Category as defined by the Atom Syndication Format specification.
114
+ #
115
+ #
116
+ class Category
117
+ include Atom::Xml::Parseable
118
+ include SimpleExtensions
119
+ attribute :label, :scheme, :term
120
+
121
+ def initialize(o = {})
122
+ case o
123
+ when XML::Reader
124
+ parse(o, :once => true)
125
+ when Hash
126
+ o.each do |k, v|
127
+ self.send("#{k.to_s}=", v)
128
+ end
129
+ else
130
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
131
+ end
132
+
133
+ yield(self) if block_given?
134
+ end
135
+ end
136
+
137
+ # Represents a Person as defined by the Atom Syndication Format specification.
138
+ #
139
+ # A Person is used for all author and contributor attributes.
140
+ #
141
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#atomPersonConstruct
142
+ #
143
+ class Person
144
+ include Xml::Parseable
145
+ element :name, :uri, :email
146
+
147
+ # Initialize a new person.
148
+ #
149
+ # +o+:: An XML::Reader object or a hash. Valid hash keys are +:name+, +:uri+ and +:email+.
150
+ def initialize(o = {})
151
+ case o
152
+ when XML::Reader
153
+ o.read
154
+ parse(o)
155
+ when Hash
156
+ o.each do |k, v|
157
+ self.send("#{k.to_s}=", v)
158
+ end
159
+ else
160
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
161
+ end
162
+
163
+ yield(self) if block_given?
164
+ end
165
+
166
+ def inspect
167
+ "<Atom::Person name:'#{name}' uri:'#{uri}' email:'#{email}"
168
+ end
169
+ end
170
+
171
+ class Content # :nodoc:
172
+ def self.parse(xml)
173
+ if xml['src'] && !xml['src'].empty?
174
+ External.new(xml)
175
+ else
176
+ case xml['type']
177
+ when "xhtml"
178
+ Xhtml.new(xml)
179
+ when "html"
180
+ Html.new(xml)
181
+ else
182
+ Text.new(xml)
183
+ end
184
+ end
185
+ end
186
+
187
+ # This is the base class for all content within an atom document.
188
+ #
189
+ # Content can be Text, Html or Xhtml.
190
+ #
191
+ # A Content object can be treated as a String with type and xml_lang
192
+ # attributes.
193
+ #
194
+ # For a thorough discussion of atom content see
195
+ # http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.content
196
+ class Base < DelegateClass(String)
197
+ include Xml::Parseable
198
+
199
+ def initialize(c)
200
+ __setobj__(c)
201
+ end
202
+
203
+ def ==(o)
204
+ if o.is_a?(self.class)
205
+ self.type == o.type &&
206
+ self.xml_lang == o.xml_lang &&
207
+ self.to_s == o.to_s
208
+ elsif o.is_a?(String)
209
+ self.to_s == o
210
+ end
211
+ end
212
+
213
+ protected
214
+ def set_content(c) # :nodoc:
215
+ __setobj__(c)
216
+ end
217
+ end
218
+
219
+ # External content reference within an Atom document.
220
+ class External < Base
221
+ attribute :type, :src
222
+
223
+ # this one only works with XML::Reader instances, no strings, since the
224
+ # content won't be inline.
225
+ def initialize(o)
226
+ raise ArgumentError, "Got #{o} which isn't an XML::Reader" unless o.kind_of?(XML::Reader)
227
+ super("")
228
+ parse(o, :once => true)
229
+ end
230
+
231
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
232
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
233
+ node['type'] = self.type if self.type
234
+ node['src'] = self.src if self.src
235
+ node
236
+ end
237
+ end
238
+
239
+ # Text content within an Atom document.
240
+ class Text < Base
241
+ attribute :type, :'xml:lang'
242
+ def initialize(o)
243
+ case o
244
+ when String
245
+ super(o)
246
+ @type = "text"
247
+ when XML::Reader
248
+ super(o.read_string)
249
+ parse(o, :once => true)
250
+ else
251
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
252
+ end
253
+ end
254
+
255
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
256
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
257
+ node << self.to_s
258
+ node
259
+ end
260
+ end
261
+
262
+ # Html content within an Atom document.
263
+ class Html < Base
264
+ attribute :type, :'xml:lang'
265
+ # Creates a new Content::Html.
266
+ #
267
+ # +o+:: An XML::Reader or a HTML string.
268
+ #
269
+ def initialize(o)
270
+ case o
271
+ when XML::Reader
272
+ super(o.read_string)
273
+ parse(o, :once => true)
274
+ when String
275
+ super(o)
276
+ @type = 'html'
277
+ else
278
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
279
+ end
280
+ end
281
+
282
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) # :nodoc:
283
+ require 'iconv'
284
+ # Convert from utf-8 to utf-8 as a way of making sure the content is UTF-8.
285
+ #
286
+ # This is a pretty crappy way to do it but if we don't check libxml just
287
+ # fails silently and outputs the content element without any content. At
288
+ # least checking here and raising an exception gives the caller a chance
289
+ # to try and recitfy the situation.
290
+ #
291
+ begin
292
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
293
+ node << Iconv.iconv('utf-8', 'utf-8', self.to_s).first
294
+ node['type'] = 'html'
295
+ node['xml:lang'] = self.xml_lang if self.xml_lang
296
+ node
297
+ rescue Iconv::IllegalSequence => e
298
+ raise SerializationError, "Content must be converted to UTF-8 before attempting to serialize to XML: #{e.message}."
299
+ end
300
+ end
301
+ end
302
+
303
+ # XHTML content within an Atom document.
304
+ class Xhtml < Base
305
+ XHTML = 'http://www.w3.org/1999/xhtml'
306
+ attribute :type, :'xml:lang'
307
+
308
+ def initialize(o)
309
+ case o
310
+ when String
311
+ super(o)
312
+ @type = "xhtml"
313
+ when XML::Reader
314
+ super("")
315
+ xml = o
316
+ parse(xml, :once => true)
317
+ starting_depth = xml.depth
318
+
319
+ # Get the next element - should be a div according to the atom spec
320
+ while xml.read && xml.node_type != XML::Reader::TYPE_ELEMENT; end
321
+
322
+ if xml.local_name == 'div' && xml.namespace_uri == XHTML
323
+ set_content(xml.read_inner_xml.strip.gsub(/\s+/, ' '))
324
+ else
325
+ set_content(xml.read_outer_xml)
326
+ end
327
+
328
+ # get back to the end of the element we were created with
329
+ while xml.read == 1 && xml.depth > starting_depth; end
330
+ else
331
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
332
+ end
333
+ end
334
+
335
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
336
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
337
+ node['type'] = 'xhtml'
338
+ node['xml:lang'] = self.xml_lang.to_s
339
+
340
+ div = XML::Node.new('div')
341
+ div['xmlns'] = XHTML
342
+
343
+ p = XML::Parser.string(to_s)
344
+ content = p.parse.root.copy(true)
345
+ div << content
346
+
347
+ node << div
348
+ node
349
+ end
350
+ end
351
+ end
352
+
353
+ # Represents a Source as defined by the Atom Syndication Format specification.
354
+ #
355
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.source
356
+ class Source
357
+ extend Forwardable
358
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures
359
+ include Xml::Parseable
360
+
361
+ element :id
362
+ element :updated, :class => Time, :content_only => true
363
+ element :title, :subtitle, :class => Content
364
+ elements :authors, :contributors, :class => Person
365
+ elements :links
366
+
367
+ def initialize(o = {})
368
+ @authors, @contributors, @links = [], [], Links.new
369
+
370
+ case o
371
+ when XML::Reader
372
+ unless current_node_is?(o, 'source', NAMESPACE)
373
+ raise ArgumentError, "Invalid node for atom:source - #{o.name}(#{o.namespace})"
374
+ end
375
+
376
+ o.read
377
+ parse(o)
378
+ when Hash
379
+ o.each do |k, v|
380
+ self.send("#{k.to_s}=", v)
381
+ end
382
+ else
383
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
384
+ end
385
+
386
+ yield(self) if block_given?
387
+ end
388
+ end
389
+
390
+ # Represents a Feed as defined by the Atom Syndication Format specification.
391
+ #
392
+ # A feed is the top level element in an atom document. It is a container for feed level
393
+ # metadata and for each entry in the feed.
394
+ #
395
+ # This supports pagination as defined in RFC 5005, see http://www.ietf.org/rfc/rfc5005.txt
396
+ #
397
+ # == Parsing
398
+ #
399
+ # A feed can be parsed using the Feed.load_feed method. This method accepts a String containing
400
+ # a valid atom document, an IO object, or an URI to a valid atom document. For example:
401
+ #
402
+ # # Using a File
403
+ # feed = Feed.load_feed(File.open("/path/to/myfeed.atom"))
404
+ #
405
+ # # Using a URL
406
+ # feed = Feed.load_feed(URI.parse("http://example.org/afeed.atom"))
407
+ #
408
+ # == Encoding
409
+ #
410
+ # A feed can be converted to XML using, the to_xml method that returns a valid atom document in a String.
411
+ #
412
+ # == Attributes
413
+ #
414
+ # A feed has the following attributes:
415
+ #
416
+ # +id+:: A unique id for the feed.
417
+ # +updated+:: The Time the feed was updated.
418
+ # +title+:: The title of the feed.
419
+ # +subtitle+:: The subtitle of the feed.
420
+ # +authors+:: An array of Atom::Person objects that are authors of this feed.
421
+ # +contributors+:: An array of Atom::Person objects that are contributors to this feed.
422
+ # +generator+:: A Atom::Generator.
423
+ # +categories+:: A list of Atom:Category objects for the feed.
424
+ # +rights+:: A string describing the rights associated with this feed.
425
+ # +entries+:: An array of Atom::Entry objects.
426
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
427
+ #
428
+ # == References
429
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.feed
430
+ #
431
+ class Feed
432
+ include Xml::Parseable
433
+ include SimpleExtensions
434
+ extend Forwardable
435
+ def_delegators :@links, :alternate, :self, :via, :first_page, :last_page, :next_page, :prev_page
436
+
437
+ loadable!
438
+
439
+ namespace Atom::NAMESPACE
440
+ element :id, :rights
441
+ element :generator, :class => Generator
442
+ element :title, :subtitle, :class => Content
443
+ element :updated, :class => Time, :content_only => true
444
+ elements :links
445
+ elements :authors, :contributors, :class => Person
446
+ elements :categories
447
+ elements :entries
448
+
449
+ # Initialize a Feed.
450
+ #
451
+ # This will also yield itself, so a feed can be constructed like this:
452
+ #
453
+ # feed = Feed.new do |feed|
454
+ # feed.title = "My Cool feed"
455
+ # end
456
+ #
457
+ # +o+:: An XML Reader or a Hash of attributes.
458
+ #
459
+ def initialize(o = {})
460
+ @links, @entries, @authors, @contributors, @categories = Links.new, [], [], [], []
461
+
462
+ case o
463
+ when XML::Reader
464
+ if next_node_is?(o, 'feed', Atom::NAMESPACE)
465
+ o.read
466
+ parse(o)
467
+ else
468
+ raise ArgumentError, "XML document was missing atom:feed: #{o.read_outer_xml}"
469
+ end
470
+ when Hash
471
+ o.each do |k, v|
472
+ self.send("#{k.to_s}=", v)
473
+ end
474
+ else
475
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
476
+ end
477
+
478
+ yield(self) if block_given?
479
+ end
480
+
481
+ # Return true if this is the first feed in a paginated set.
482
+ def first?
483
+ links.self == links.first_page
484
+ end
485
+
486
+ # Returns true if this is the last feed in a paginated set.
487
+ def last?
488
+ links.self == links.last_page
489
+ end
490
+
491
+ # Reloads the feed by fetching the self uri.
492
+ def reload!(opts = {})
493
+ if links.self
494
+ Feed.load_feed(URI.parse(links.self.href), opts)
495
+ end
496
+ end
497
+
498
+ # Iterates over each entry in the feed.
499
+ #
500
+ # ==== Options
501
+ #
502
+ # +paginate+:: If true and the feed supports pagination this will fetch each page of the feed.
503
+ # +since+:: If a Time object is provided each_entry will iterate over all entries that were updated since that time.
504
+ # +user+:: User name for HTTP Basic Authentication.
505
+ # +pass+:: Password for HTTP Basic Authentication.
506
+ #
507
+ def each_entry(options = {}, &block)
508
+ if options[:paginate]
509
+ since_reached = false
510
+ feed = self
511
+ loop do
512
+ feed.entries.each do |entry|
513
+ if options[:since] && entry.updated && options[:since] > entry.updated
514
+ since_reached = true
515
+ break
516
+ else
517
+ block.call(entry)
518
+ end
519
+ end
520
+
521
+ if since_reached || feed.next_page.nil?
522
+ break
523
+ else feed.next_page
524
+ feed = feed.next_page.fetch(options)
525
+ end
526
+ end
527
+ else
528
+ self.entries.each(&block)
529
+ end
530
+ end
531
+ end
532
+
533
+ # Represents an Entry as defined by the Atom Syndication Format specification.
534
+ #
535
+ # An Entry represents an individual entry within a Feed.
536
+ #
537
+ # == Parsing
538
+ #
539
+ # An Entry can be parsed using the Entry.load_entry method. This method accepts a String containing
540
+ # a valid atom entry document, an IO object, or an URI to a valid atom entry document. For example:
541
+ #
542
+ # # Using a File
543
+ # entry = Entry.load_entry(File.open("/path/to/myfeedentry.atom"))
544
+ #
545
+ # # Using a URL
546
+ # Entry = Entry.load_entry(URI.parse("http://example.org/afeedentry.atom"))
547
+ #
548
+ # The document must contain a stand alone entry element as described in the Atom Syndication Format.
549
+ #
550
+ # == Encoding
551
+ #
552
+ # A Entry can be converted to XML using, the to_xml method that returns a valid atom entry document in a String.
553
+ #
554
+ # == Attributes
555
+ #
556
+ # An entry has the following attributes:
557
+ #
558
+ # +id+:: A unique id for the entry.
559
+ # +updated+:: The Time the entry was updated.
560
+ # +published+:: The Time the entry was published.
561
+ # +title+:: The title of the entry.
562
+ # +summary+:: A short textual summary of the item.
563
+ # +authors+:: An array of Atom::Person objects that are authors of this entry.
564
+ # +contributors+:: An array of Atom::Person objects that are contributors to this entry.
565
+ # +rights+:: A string describing the rights associated with this entry.
566
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
567
+ # +source+:: Metadata of a feed that was the source of this item, for feed aggregators, etc.
568
+ # +categories+:: Array of Atom::Categories.
569
+ # +content+:: The content of the entry. This will be one of Atom::Content::Text, Atom::Content:Html or Atom::Content::Xhtml.
570
+ #
571
+ # == References
572
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.entry for more detailed
573
+ # definitions of the attributes.
574
+ #
575
+ class Entry
576
+ include Xml::Parseable
577
+ include SimpleExtensions
578
+ extend Forwardable
579
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures, :edit_link, :via
580
+
581
+ loadable!
582
+ namespace Atom::NAMESPACE
583
+ element :title, :id, :summary
584
+ element :updated, :published, :class => Time, :content_only => true
585
+ element :source, :class => Source
586
+ elements :links
587
+ elements :authors, :contributors, :class => Person
588
+ elements :categories, :class => Category
589
+ element :content, :class => Content
590
+
591
+ # Initialize an Entry.
592
+ #
593
+ # This will also yield itself, so an Entry can be constructed like this:
594
+ #
595
+ # entry = Entry.new do |entry|
596
+ # entry.title = "My Cool entry"
597
+ # end
598
+ #
599
+ # +o+:: An XML Reader or a Hash of attributes.
600
+ #
601
+ def initialize(o = {})
602
+ @links = Links.new
603
+ @authors = []
604
+ @contributors = []
605
+ @categories = []
606
+
607
+ case o
608
+ when XML::Reader
609
+ if current_node_is?(o, 'entry', Atom::NAMESPACE) || next_node_is?(o, 'entry', Atom::NAMESPACE)
610
+ o.read
611
+ parse(o)
612
+ else
613
+ raise ArgumentError, "Entry created with node other than atom:entry: #{o.name}"
614
+ end
615
+ when Hash
616
+ o.each do |k,v|
617
+ send("#{k.to_s}=", v)
618
+ end
619
+ else
620
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
621
+ end
622
+
623
+ yield(self) if block_given?
624
+ end
625
+
626
+ # Reload the Entry by fetching the self link.
627
+ def reload!(opts = {})
628
+ if links.self
629
+ Entry.load_entry(URI.parse(links.self.href), opts)
630
+ end
631
+ end
632
+ end
633
+
634
+ # Links provides an Array of Link objects belonging to either a Feed or an Entry.
635
+ #
636
+ # Some additional methods to get specific types of links are provided.
637
+ #
638
+ # == References
639
+ #
640
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link
641
+ # for details on link selection and link attributes.
642
+ #
643
+ class Links < DelegateClass(Array)
644
+ include Enumerable
645
+
646
+ # Initialize an empty Links array.
647
+ def initialize
648
+ super([])
649
+ end
650
+
651
+ # Get the alternate.
652
+ #
653
+ # Returns the first link with rel == 'alternate' that matches the given type.
654
+ def alternate(type = nil)
655
+ detect { |link|
656
+ (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type) && (link.hreflang.nil?)
657
+ } || detect { |link|
658
+ (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type)
659
+ }
660
+ end
661
+
662
+ # Get all alternates.
663
+ def alternates
664
+ select { |link| link.rel.nil? || link.rel == Link::Rel::ALTERNATE }
665
+ end
666
+
667
+ # Gets the self link.
668
+ def self
669
+ detect { |link| link.rel == Link::Rel::SELF }
670
+ end
671
+
672
+ # Gets the via link.
673
+ def via
674
+ detect { |link| link.rel == Link::Rel::VIA }
675
+ end
676
+
677
+ # Gets all links with rel == 'enclosure'
678
+ def enclosures
679
+ select { |link| link.rel == Link::Rel::ENCLOSURE }
680
+ end
681
+
682
+ # Gets the link with rel == 'first'.
683
+ #
684
+ # This is defined as the first page in a pagination set.
685
+ def first_page
686
+ detect { |link| link.rel == Link::Rel::FIRST }
687
+ end
688
+
689
+ # Gets the link with rel == 'last'.
690
+ #
691
+ # This is defined as the last page in a pagination set.
692
+ def last_page
693
+ detect { |link| link.rel == Link::Rel::LAST }
694
+ end
695
+
696
+ # Gets the link with rel == 'next'.
697
+ #
698
+ # This is defined as the next page in a pagination set.
699
+ def next_page
700
+ detect { |link| link.rel == Link::Rel::NEXT }
701
+ end
702
+
703
+ # Gets the link with rel == 'prev'.
704
+ #
705
+ # This is defined as the previous page in a pagination set.
706
+ def prev_page
707
+ detect { |link| link.rel == Link::Rel::PREVIOUS }
708
+ end
709
+
710
+ # Gets the edit link.
711
+ #
712
+ # This is the link which can be used for posting updates to an item using the Atom Publishing Protocol.
713
+ #
714
+ def edit_link
715
+ detect { |link| link.rel == 'edit' }
716
+ end
717
+ end
718
+
719
+ # Represents a link in an Atom document.
720
+ #
721
+ # A link defines a reference from an Atom document to a web resource.
722
+ #
723
+ # == References
724
+ # See http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link for
725
+ # a description of the different types of links.
726
+ #
727
+ class Link
728
+ module Rel # :nodoc:
729
+ ALTERNATE = 'alternate'
730
+ SELF = 'self'
731
+ VIA = 'via'
732
+ ENCLOSURE = 'enclosure'
733
+ FIRST = 'first'
734
+ LAST = 'last'
735
+ PREVIOUS = 'prev'
736
+ NEXT = 'next'
737
+ end
738
+
739
+ include Xml::Parseable
740
+ attribute :rel, :type, :length, :hreflang, :title
741
+ uri_attribute :href
742
+
743
+ # Create a link.
744
+ #
745
+ # +o+:: An XML::Reader containing a link element or a Hash of attributes.
746
+ #
747
+ def initialize(o)
748
+ case o
749
+ when XML::Reader
750
+ if current_node_is?(o, 'link')
751
+ parse(o, :once => true)
752
+ else
753
+ raise ArgumentError, "Link created with node other than atom:link: #{o.name}"
754
+ end
755
+ when Hash
756
+ [:href, :rel, :type, :length, :hreflang, :title].each do |attr|
757
+ self.send("#{attr}=", o[attr])
758
+ end
759
+ else
760
+ raise ArgumentError, "Don't know how to handle #{o}"
761
+ end
762
+ end
763
+
764
+ remove_method :length=
765
+ def length=(v)
766
+ @length = v.to_i
767
+ end
768
+
769
+ def to_s
770
+ self.href
771
+ end
772
+
773
+ def ==(o)
774
+ o.respond_to?(:href) && o.href == self.href
775
+ end
776
+
777
+ # This will fetch the URL referenced by the link.
778
+ #
779
+ # If the URL contains a valid feed, a Feed will be returned, otherwise,
780
+ # the body of the response will be returned.
781
+ #
782
+ # TODO: Handle redirects.
783
+ #
784
+ def fetch(options = {})
785
+ begin
786
+ Atom::Feed.load_feed(URI.parse(self.href), options)
787
+ rescue ArgumentError
788
+ Net::HTTP.get_response(URI.parse(self.href)).body
789
+ end
790
+ end
791
+
792
+ def inspect
793
+ "<Atom::Link href:'#{href}' type:'#{type}'>"
794
+ end
795
+ end
796
+ end