restfulie 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/README.textile +83 -7
  2. data/Rakefile +98 -13
  3. data/lib/restfulie/client/base.rb +48 -53
  4. data/lib/restfulie/client/configuration.rb +69 -0
  5. data/lib/restfulie/client/http/adapter.rb +487 -0
  6. data/lib/restfulie/client/http/atom_ext.rb +87 -0
  7. data/lib/restfulie/client/http/cache.rb +28 -0
  8. data/lib/restfulie/client/http/error.rb +80 -0
  9. data/lib/restfulie/client/http/marshal.rb +147 -0
  10. data/lib/restfulie/client/http.rb +13 -0
  11. data/lib/restfulie/client.rb +8 -56
  12. data/lib/restfulie/common/builder/builder_base.rb +58 -0
  13. data/lib/restfulie/common/builder/helpers.rb +22 -0
  14. data/lib/restfulie/common/builder/marshalling/atom.rb +197 -0
  15. data/lib/restfulie/common/builder/marshalling/base.rb +12 -0
  16. data/lib/restfulie/common/builder/marshalling/json.rb +2 -0
  17. data/lib/restfulie/common/builder/marshalling.rb +16 -0
  18. data/lib/restfulie/common/builder/rules/collection_rule.rb +10 -0
  19. data/lib/restfulie/common/builder/rules/link.rb +20 -0
  20. data/lib/restfulie/common/builder/rules/links.rb +9 -0
  21. data/lib/restfulie/common/builder/rules/member_rule.rb +8 -0
  22. data/lib/restfulie/common/builder/rules/namespace.rb +25 -0
  23. data/lib/restfulie/common/builder/rules/rules_base.rb +76 -0
  24. data/lib/restfulie/common/builder.rb +16 -0
  25. data/lib/restfulie/common/errors.rb +9 -0
  26. data/lib/restfulie/{logger.rb → common/logger.rb} +3 -5
  27. data/lib/restfulie/common/representation/atom.rb +48 -0
  28. data/lib/restfulie/common/representation/generic.rb +33 -0
  29. data/lib/restfulie/common/representation/xml.rb +24 -0
  30. data/lib/restfulie/common/representation.rb +10 -0
  31. data/lib/restfulie/common.rb +23 -0
  32. data/lib/restfulie/server/action_controller/base.rb +31 -0
  33. data/lib/restfulie/server/action_controller/params_parser.rb +62 -0
  34. data/lib/restfulie/server/action_controller/restful_responder.rb +39 -0
  35. data/lib/restfulie/server/action_controller/routing/restful_route.rb +14 -0
  36. data/lib/restfulie/server/action_controller/routing.rb +12 -0
  37. data/lib/restfulie/server/action_controller.rb +15 -0
  38. data/lib/restfulie/server/action_view/helpers.rb +45 -0
  39. data/lib/restfulie/server/action_view/template_handlers/tokamak.rb +15 -0
  40. data/lib/restfulie/server/action_view/template_handlers.rb +13 -0
  41. data/lib/restfulie/server/action_view.rb +8 -0
  42. data/lib/restfulie/server/configuration.rb +21 -0
  43. data/lib/restfulie/server/core_ext/array.rb +45 -0
  44. data/lib/restfulie/server/core_ext.rb +1 -0
  45. data/lib/restfulie/server/restfulie_controller.rb +1 -17
  46. data/lib/restfulie/server.rb +15 -0
  47. data/lib/restfulie.rb +4 -72
  48. data/lib/vendor/atom/configuration.rb +24 -0
  49. data/lib/vendor/atom/pub.rb +250 -0
  50. data/lib/vendor/atom/xml/parser.rb +373 -0
  51. data/lib/vendor/atom.rb +771 -0
  52. metadata +94 -33
  53. data/lib/restfulie/client/atom_media_type.rb +0 -75
  54. data/lib/restfulie/client/cache.rb +0 -103
  55. data/lib/restfulie/client/entry_point.rb +0 -94
  56. data/lib/restfulie/client/extensions/http.rb +0 -116
  57. data/lib/restfulie/client/helper.rb +0 -28
  58. data/lib/restfulie/client/instance.rb +0 -158
  59. data/lib/restfulie/client/request_execution.rb +0 -321
  60. data/lib/restfulie/client/state.rb +0 -36
  61. data/lib/restfulie/media_type.rb +0 -143
  62. data/lib/restfulie/media_type_control.rb +0 -115
  63. data/lib/restfulie/media_type_defaults.rb +0 -51
  64. data/lib/restfulie/server/atom_media_type.rb +0 -115
  65. data/lib/restfulie/server/base.rb +0 -91
  66. data/lib/restfulie/server/controller.rb +0 -122
  67. data/lib/restfulie/server/instance.rb +0 -102
  68. data/lib/restfulie/server/marshalling.rb +0 -47
  69. data/lib/restfulie/server/opensearch/description.rb +0 -54
  70. data/lib/restfulie/server/opensearch.rb +0 -18
  71. data/lib/restfulie/server/transition.rb +0 -93
  72. data/lib/restfulie/unmarshalling.rb +0 -131
  73. data/lib/vendor/jeokkarak/hashi.rb +0 -65
  74. data/lib/vendor/jeokkarak/jeokkarak.rb +0 -81
@@ -0,0 +1,771 @@
1
+ # Copyright (c) 2008 The Kaphan Foundation
2
+ #
3
+ # For licensing information see LICENSE.txt.
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 'vendor/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
+ case xml['type']
174
+ when "xhtml"
175
+ Xhtml.new(xml)
176
+ when "html"
177
+ Html.new(xml)
178
+ else
179
+ Text.new(xml)
180
+ end
181
+ end
182
+
183
+ # This is the base class for all content within an atom document.
184
+ #
185
+ # Content can be Text, Html or Xhtml.
186
+ #
187
+ # A Content object can be treated as a String with type and xml_lang
188
+ # attributes.
189
+ #
190
+ # For a thorough discussion of atom content see
191
+ # http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.content
192
+ class Base < DelegateClass(String)
193
+ include Xml::Parseable
194
+
195
+ def initialize(c)
196
+ __setobj__(c)
197
+ end
198
+
199
+ def ==(o)
200
+ if o.is_a?(self.class)
201
+ self.type == o.type &&
202
+ self.xml_lang == o.xml_lang &&
203
+ self.to_s == o.to_s
204
+ elsif o.is_a?(String)
205
+ self.to_s == o
206
+ end
207
+ end
208
+
209
+ protected
210
+ def set_content(c) # :nodoc:
211
+ __setobj__(c)
212
+ end
213
+ end
214
+
215
+ # Text content within an Atom document.
216
+ class Text < Base
217
+ attribute :type, :'xml:lang'
218
+ def initialize(o)
219
+ case o
220
+ when String
221
+ super(o)
222
+ @type = "text"
223
+ when XML::Reader
224
+ super(o.read_string)
225
+ parse(o, :once => true)
226
+ else
227
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
228
+ end
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 << self.to_s
234
+ node
235
+ end
236
+ end
237
+
238
+ # Html content within an Atom document.
239
+ class Html < Base
240
+ attribute :type, :'xml:lang'
241
+ # Creates a new Content::Html.
242
+ #
243
+ # +o+:: An XML::Reader or a HTML string.
244
+ #
245
+ def initialize(o)
246
+ case o
247
+ when XML::Reader
248
+ super(o.read_string)
249
+ parse(o, :once => true)
250
+ when String
251
+ super(o)
252
+ @type = 'html'
253
+ else
254
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
255
+ end
256
+ end
257
+
258
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) # :nodoc:
259
+ require 'iconv'
260
+ # Convert from utf-8 to utf-8 as a way of making sure the content is UTF-8.
261
+ #
262
+ # This is a pretty crappy way to do it but if we don't check libxml just
263
+ # fails silently and outputs the content element without any content. At
264
+ # least checking here and raising an exception gives the caller a chance
265
+ # to try and recitfy the situation.
266
+ #
267
+ begin
268
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
269
+ node << Iconv.iconv('utf-8', 'utf-8', self.to_s)
270
+ node['type'] = 'html'
271
+ node['xml:lang'] = self.xml_lang if self.xml_lang
272
+ node
273
+ rescue Iconv::IllegalSequence => e
274
+ raise SerializationError, "Content must be converted to UTF-8 before attempting to serialize to XML: #{e.message}."
275
+ end
276
+ end
277
+ end
278
+
279
+ # XHTML content within an Atom document.
280
+ class Xhtml < Base
281
+ XHTML = 'http://www.w3.org/1999/xhtml'
282
+ attribute :type, :'xml:lang'
283
+
284
+ def initialize(o)
285
+ case o
286
+ when String
287
+ super(o)
288
+ @type = "xhtml"
289
+ when XML::Reader
290
+ super("")
291
+ xml = o
292
+ parse(xml, :once => true)
293
+ starting_depth = xml.depth
294
+
295
+ # Get the next element - should be a div according to the atom spec
296
+ while xml.read && xml.node_type != XML::Reader::TYPE_ELEMENT; end
297
+
298
+ if xml.local_name == 'div' && xml.namespace_uri == XHTML
299
+ set_content(xml.read_inner_xml.strip.gsub(/\s+/, ' '))
300
+ else
301
+ set_content(xml.read_outer_xml)
302
+ end
303
+
304
+ # get back to the end of the element we were created with
305
+ while xml.read == 1 && xml.depth > starting_depth; end
306
+ else
307
+ raise ArgumentError, "Got #{o} which isn't a String or XML::Reader"
308
+ end
309
+ end
310
+
311
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
312
+ node = XML::Node.new("#{namespace_map.prefix(Atom::NAMESPACE, name)}")
313
+ node['type'] = 'xhtml'
314
+ node['xml:lang'] = self.xml_lang.to_s
315
+
316
+ div = XML::Node.new('div')
317
+ div['xmlns'] = XHTML
318
+
319
+ p = XML::Parser.string(to_s)
320
+ content = p.parse.root.copy(true)
321
+ div << content
322
+
323
+ node << div
324
+ node
325
+ end
326
+ end
327
+ end
328
+
329
+ # Represents a Source as defined by the Atom Syndication Format specification.
330
+ #
331
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.source
332
+ class Source
333
+ extend Forwardable
334
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures
335
+ include Xml::Parseable
336
+
337
+ element :id
338
+ element :updated, :class => Time, :content_only => true
339
+ element :title, :subtitle, :class => Content
340
+ elements :authors, :contributors, :class => Person
341
+ elements :links
342
+
343
+ def initialize(o = {})
344
+ @authors, @contributors, @links = [], [], Links.new
345
+
346
+ case o
347
+ when XML::Reader
348
+ unless current_node_is?(o, 'source', NAMESPACE)
349
+ raise ArgumentError, "Invalid node for atom:source - #{o.name}(#{o.namespace})"
350
+ end
351
+
352
+ o.read
353
+ parse(o)
354
+ when Hash
355
+ o.each do |k, v|
356
+ self.send("#{k.to_s}=", v)
357
+ end
358
+ else
359
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
360
+ end
361
+
362
+ yield(self) if block_given?
363
+ end
364
+ end
365
+
366
+ # Represents a Feed as defined by the Atom Syndication Format specification.
367
+ #
368
+ # A feed is the top level element in an atom document. It is a container for feed level
369
+ # metadata and for each entry in the feed.
370
+ #
371
+ # This supports pagination as defined in RFC 5005, see http://www.ietf.org/rfc/rfc5005.txt
372
+ #
373
+ # == Parsing
374
+ #
375
+ # A feed can be parsed using the Feed.load_feed method. This method accepts a String containing
376
+ # a valid atom document, an IO object, or an URI to a valid atom document. For example:
377
+ #
378
+ # # Using a File
379
+ # feed = Feed.load_feed(File.open("/path/to/myfeed.atom"))
380
+ #
381
+ # # Using a URL
382
+ # feed = Feed.load_feed(URI.parse("http://example.org/afeed.atom"))
383
+ #
384
+ # == Encoding
385
+ #
386
+ # A feed can be converted to XML using, the to_xml method that returns a valid atom document in a String.
387
+ #
388
+ # == Attributes
389
+ #
390
+ # A feed has the following attributes:
391
+ #
392
+ # +id+:: A unique id for the feed.
393
+ # +updated+:: The Time the feed was updated.
394
+ # +title+:: The title of the feed.
395
+ # +subtitle+:: The subtitle of the feed.
396
+ # +authors+:: An array of Atom::Person objects that are authors of this feed.
397
+ # +contributors+:: An array of Atom::Person objects that are contributors to this feed.
398
+ # +generator+:: A Atom::Generator.
399
+ # +categories+:: A list of Atom:Category objects for the feed.
400
+ # +rights+:: A string describing the rights associated with this feed.
401
+ # +entries+:: An array of Atom::Entry objects.
402
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
403
+ #
404
+ # == References
405
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.feed
406
+ #
407
+ class Feed
408
+ include Xml::Parseable
409
+ include SimpleExtensions
410
+ extend Forwardable
411
+ def_delegators :@links, :alternate, :self, :via, :first_page, :last_page, :next_page, :prev_page
412
+
413
+ loadable!
414
+
415
+ namespace Atom::NAMESPACE
416
+ element :id, :rights
417
+ element :generator, :class => Generator
418
+ element :title, :subtitle, :class => Content
419
+ element :updated, :class => Time, :content_only => true
420
+ elements :links
421
+ elements :authors, :contributors, :class => Person
422
+ elements :categories
423
+ elements :entries
424
+
425
+ # Initialize a Feed.
426
+ #
427
+ # This will also yield itself, so a feed can be constructed like this:
428
+ #
429
+ # feed = Feed.new do |feed|
430
+ # feed.title = "My Cool feed"
431
+ # end
432
+ #
433
+ # +o+:: An XML Reader or a Hash of attributes.
434
+ #
435
+ def initialize(o = {})
436
+ @links, @entries, @authors, @contributors, @categories = Links.new, [], [], [], []
437
+
438
+ case o
439
+ when XML::Reader
440
+ if next_node_is?(o, 'feed', Atom::NAMESPACE)
441
+ o.read
442
+ parse(o)
443
+ else
444
+ raise ArgumentError, "XML document was missing atom:feed: #{o.read_outer_xml}"
445
+ end
446
+ when Hash
447
+ o.each do |k, v|
448
+ self.send("#{k.to_s}=", v)
449
+ end
450
+ else
451
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
452
+ end
453
+
454
+ yield(self) if block_given?
455
+ end
456
+
457
+ # Return true if this is the first feed in a paginated set.
458
+ def first?
459
+ links.self == links.first_page
460
+ end
461
+
462
+ # Returns true if this is the last feed in a paginated set.
463
+ def last?
464
+ links.self == links.last_page
465
+ end
466
+
467
+ # Reloads the feed by fetching the self uri.
468
+ def reload!(opts = {})
469
+ if links.self
470
+ Feed.load_feed(URI.parse(links.self.href), opts)
471
+ end
472
+ end
473
+
474
+ # Iterates over each entry in the feed.
475
+ #
476
+ # ==== Options
477
+ #
478
+ # +paginate+:: If true and the feed supports pagination this will fetch each page of the feed.
479
+ # +since+:: If a Time object is provided each_entry will iterate over all entries that were updated since that time.
480
+ # +user+:: User name for HTTP Basic Authentication.
481
+ # +pass+:: Password for HTTP Basic Authentication.
482
+ #
483
+ def each_entry(options = {}, &block)
484
+ if options[:paginate]
485
+ since_reached = false
486
+ feed = self
487
+ loop do
488
+ feed.entries.each do |entry|
489
+ if options[:since] && entry.updated && options[:since] > entry.updated
490
+ since_reached = true
491
+ break
492
+ else
493
+ block.call(entry)
494
+ end
495
+ end
496
+
497
+ if since_reached || feed.next_page.nil?
498
+ break
499
+ else feed.next_page
500
+ feed = feed.next_page.fetch(options)
501
+ end
502
+ end
503
+ else
504
+ self.entries.each(&block)
505
+ end
506
+ end
507
+ end
508
+
509
+ # Represents an Entry as defined by the Atom Syndication Format specification.
510
+ #
511
+ # An Entry represents an individual entry within a Feed.
512
+ #
513
+ # == Parsing
514
+ #
515
+ # An Entry can be parsed using the Entry.load_entry method. This method accepts a String containing
516
+ # a valid atom entry document, an IO object, or an URI to a valid atom entry document. For example:
517
+ #
518
+ # # Using a File
519
+ # entry = Entry.load_entry(File.open("/path/to/myfeedentry.atom"))
520
+ #
521
+ # # Using a URL
522
+ # Entry = Entry.load_entry(URI.parse("http://example.org/afeedentry.atom"))
523
+ #
524
+ # The document must contain a stand alone entry element as described in the Atom Syndication Format.
525
+ #
526
+ # == Encoding
527
+ #
528
+ # A Entry can be converted to XML using, the to_xml method that returns a valid atom entry document in a String.
529
+ #
530
+ # == Attributes
531
+ #
532
+ # An entry has the following attributes:
533
+ #
534
+ # +id+:: A unique id for the entry.
535
+ # +updated+:: The Time the entry was updated.
536
+ # +published+:: The Time the entry was published.
537
+ # +title+:: The title of the entry.
538
+ # +summary+:: A short textual summary of the item.
539
+ # +authors+:: An array of Atom::Person objects that are authors of this entry.
540
+ # +contributors+:: An array of Atom::Person objects that are contributors to this entry.
541
+ # +rights+:: A string describing the rights associated with this entry.
542
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
543
+ # +source+:: Metadata of a feed that was the source of this item, for feed aggregators, etc.
544
+ # +categories+:: Array of Atom::Categories.
545
+ # +content+:: The content of the entry. This will be one of Atom::Content::Text, Atom::Content:Html or Atom::Content::Xhtml.
546
+ #
547
+ # == References
548
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.entry for more detailed
549
+ # definitions of the attributes.
550
+ #
551
+ class Entry
552
+ include Xml::Parseable
553
+ include SimpleExtensions
554
+ extend Forwardable
555
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures, :edit_link, :via
556
+
557
+ loadable!
558
+ namespace Atom::NAMESPACE
559
+ element :title, :id, :summary
560
+ element :updated, :published, :class => Time, :content_only => true
561
+ element :source, :class => Source
562
+ elements :links
563
+ elements :authors, :contributors, :class => Person
564
+ elements :categories, :class => Category
565
+ element :content, :class => Content
566
+
567
+ # Initialize an Entry.
568
+ #
569
+ # This will also yield itself, so an Entry can be constructed like this:
570
+ #
571
+ # entry = Entry.new do |entry|
572
+ # entry.title = "My Cool entry"
573
+ # end
574
+ #
575
+ # +o+:: An XML Reader or a Hash of attributes.
576
+ #
577
+ def initialize(o = {})
578
+ @links = Links.new
579
+ @authors = []
580
+ @contributors = []
581
+ @categories = []
582
+
583
+ case o
584
+ when XML::Reader
585
+ if current_node_is?(o, 'entry', Atom::NAMESPACE) || next_node_is?(o, 'entry', Atom::NAMESPACE)
586
+ o.read
587
+ parse(o)
588
+ else
589
+ raise ArgumentError, "Entry created with node other than atom:entry: #{o.name}"
590
+ end
591
+ when Hash
592
+ o.each do |k,v|
593
+ send("#{k.to_s}=", v)
594
+ end
595
+ else
596
+ raise ArgumentError, "Got #{o.class} but expected a Hash or XML::Reader"
597
+ end
598
+
599
+ yield(self) if block_given?
600
+ end
601
+
602
+ # Reload the Entry by fetching the self link.
603
+ def reload!(opts = {})
604
+ if links.self
605
+ Entry.load_entry(URI.parse(links.self.href), opts)
606
+ end
607
+ end
608
+ end
609
+
610
+ # Links provides an Array of Link objects belonging to either a Feed or an Entry.
611
+ #
612
+ # Some additional methods to get specific types of links are provided.
613
+ #
614
+ # == References
615
+ #
616
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link
617
+ # for details on link selection and link attributes.
618
+ #
619
+ class Links < DelegateClass(Array)
620
+ include Enumerable
621
+
622
+ # Initialize an empty Links array.
623
+ def initialize
624
+ super([])
625
+ end
626
+
627
+ # Get the alternate.
628
+ #
629
+ # Returns the first link with rel == 'alternate' that matches the given type.
630
+ def alternate(type = nil)
631
+ detect { |link|
632
+ (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type) && (link.hreflang.nil?)
633
+ } || detect { |link|
634
+ (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type)
635
+ }
636
+ end
637
+
638
+ # Get all alternates.
639
+ def alternates
640
+ select { |link| link.rel.nil? || link.rel == Link::Rel::ALTERNATE }
641
+ end
642
+
643
+ # Gets the self link.
644
+ def self
645
+ detect { |link| link.rel == Link::Rel::SELF }
646
+ end
647
+
648
+ # Gets the via link.
649
+ def via
650
+ detect { |link| link.rel == Link::Rel::VIA }
651
+ end
652
+
653
+ # Gets all links with rel == 'enclosure'
654
+ def enclosures
655
+ select { |link| link.rel == Link::Rel::ENCLOSURE }
656
+ end
657
+
658
+ # Gets the link with rel == 'first'.
659
+ #
660
+ # This is defined as the first page in a pagination set.
661
+ def first_page
662
+ detect { |link| link.rel == Link::Rel::FIRST }
663
+ end
664
+
665
+ # Gets the link with rel == 'last'.
666
+ #
667
+ # This is defined as the last page in a pagination set.
668
+ def last_page
669
+ detect { |link| link.rel == Link::Rel::LAST }
670
+ end
671
+
672
+ # Gets the link with rel == 'next'.
673
+ #
674
+ # This is defined as the next page in a pagination set.
675
+ def next_page
676
+ detect { |link| link.rel == Link::Rel::NEXT }
677
+ end
678
+
679
+ # Gets the link with rel == 'prev'.
680
+ #
681
+ # This is defined as the previous page in a pagination set.
682
+ def prev_page
683
+ detect { |link| link.rel == Link::Rel::PREVIOUS }
684
+ end
685
+
686
+ # Gets the edit link.
687
+ #
688
+ # This is the link which can be used for posting updates to an item using the Atom Publishing Protocol.
689
+ #
690
+ def edit_link
691
+ detect { |link| link.rel == 'edit' }
692
+ end
693
+ end
694
+
695
+ # Represents a link in an Atom document.
696
+ #
697
+ # A link defines a reference from an Atom document to a web resource.
698
+ #
699
+ # == References
700
+ # See http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link for
701
+ # a description of the different types of links.
702
+ #
703
+ class Link
704
+ module Rel # :nodoc:
705
+ ALTERNATE = 'alternate'
706
+ SELF = 'self'
707
+ VIA = 'via'
708
+ ENCLOSURE = 'enclosure'
709
+ FIRST = 'first'
710
+ LAST = 'last'
711
+ PREVIOUS = 'prev'
712
+ NEXT = 'next'
713
+ end
714
+
715
+ include Xml::Parseable
716
+ attribute :href, :rel, :type, :length, :hreflang, :title
717
+
718
+ # Create a link.
719
+ #
720
+ # +o+:: An XML::Reader containing a link element or a Hash of attributes.
721
+ #
722
+ def initialize(o)
723
+ case o
724
+ when XML::Reader
725
+ if current_node_is?(o, 'link')
726
+ parse(o, :once => true)
727
+ else
728
+ raise ArgumentError, "Link created with node other than atom:link: #{o.name}"
729
+ end
730
+ when Hash
731
+ [:href, :rel, :type, :length, :hreflang, :title].each do |attr|
732
+ self.send("#{attr}=", o[attr])
733
+ end
734
+ else
735
+ raise ArgumentError, "Don't know how to handle #{o}"
736
+ end
737
+ end
738
+
739
+ remove_method :length=
740
+ def length=(v)
741
+ @length = v.to_i
742
+ end
743
+
744
+ def to_s
745
+ self.href
746
+ end
747
+
748
+ def ==(o)
749
+ [:href, :rel, :type].all? { |k| o.respond_to?(k) && o.send(k) == self.send(k) }
750
+ end
751
+
752
+ # This will fetch the URL referenced by the link.
753
+ #
754
+ # If the URL contains a valid feed, a Feed will be returned, otherwise,
755
+ # the body of the response will be returned.
756
+ #
757
+ # TODO: Handle redirects.
758
+ #
759
+ def fetch(options = {})
760
+ begin
761
+ Atom::Feed.load_feed(URI.parse(self.href), options)
762
+ rescue ArgumentError
763
+ Net::HTTP.get_response(URI.parse(self.href)).body
764
+ end
765
+ end
766
+
767
+ def inspect
768
+ "<Atom::Link rel:'#{rel}' href:'#{href}' type:'#{type}'>"
769
+ end
770
+ end
771
+ end