vpim 0.619 → 0.658

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,63 @@
1
+ 0.657 - 2008-08-14
2
+
3
+ - Date#to_time renamed to Date#vpim_to_time. Apparently Rails also patches the core.
4
+
5
+ - Debian doesn't like RFCs. I think there is a problem with the DFG, not the
6
+ IETF. The value of a standard is that it can't be modified (if anybody could
7
+ publish a modified version of an RFC without restriction chaos would ensue).
8
+ Still, we lost that argument, and I'll try and help packagers out.
9
+
10
+ - View, gives various views of calendars. Experimental.
11
+
12
+ - Fixed doc comment refs to common Property module.
13
+
14
+ - Vtodo#duration and #due now work correctly (the missing one will be calculated
15
+ from the other).
16
+
17
+ - Vevent::Maker#add_rrule, #set_rrule are new, using Rrule::Maker.
18
+
19
+ - There is now (half-of) an Rrule::Maker class to help construct RRULE values.
20
+
21
+ - Vjournal can be encoded now (it was broken).
22
+
23
+ - Better code coverage by the tests.
24
+
25
+ - Recurrence#occurrences now returns an Enumerator instead of an Rrule. Both
26
+ have #each, and both yield the same thing, so that isn't an API change. However,
27
+ Rrule had an each_until, and the Enumerator doesn't. Use the dountil argument
28
+ to #occurrences to get the same effect. The asymetry of this API caused me
29
+ (non-aesthetic) trouble, now Rrule is an internal implementation detail.
30
+ Also, this would have had to change eventually, because occurrences need to
31
+ be the union of multiple RRULE and RDATE fields.
32
+
33
+ - Recurrence#rrule, returns an Rrule for the first RRULE field. Can be used
34
+ as transition.
35
+
36
+ - Icalendar#calscale was broken, and is now unit tested, along with #version
37
+ and #protocol?.
38
+
39
+ - Icalendar#{each,events,todos,journals} all yield components, or return an
40
+ enumerator.
41
+
42
+ - Icalendar is enumerable.
43
+
44
+ - Repo and Repo::Calendar are enumerable.
45
+
46
+ - #occurrences will call Rrule#each if a block is provided
47
+
48
+ - Recurrence rules with DTSTART in UTC will now sortof work (thanks to Max
49
+ Werner for providing the patch).
50
+
51
+ - Added convenience methods for setting and getting title and org fiels (thanks
52
+ to Jade Meskill for providing the patch).
53
+
54
+ - Modified Icalendar#create2 so only prodid can be supplied and cal is yielded
55
+ so events/etc. can be pushed.
56
+
57
+ - Support Highrisehq.com's broken google talk field (see test_vcard.rb for
58
+ examples, thanks to Terry Tong for reporting).
59
+
60
+
1
61
  0.619 - 2008-03-30
2
62
 
3
63
  - Fixed some problems with rescue statements not being specific enough.
data/README CHANGED
@@ -1,11 +1,36 @@
1
- Author:: Sam Roberts <sroberts@uniserve.com>
1
+ Author:: Sam Roberts <vieuxtech@gmail.com>
2
2
  Copyright:: Copyright (C) 2008 Sam Roberts
3
3
  License:: May be distributed under the same terms as Ruby
4
4
  Homepage:: http://vpim.rubyforge.org
5
5
  Download:: http://rubyforge.org/projects/vpim
6
+ Install:: sudo gem install vpim
6
7
 
7
- This is a pure-ruby library for decoding and encoding vCard and iCalendar data
8
- ("personal information") called vPim.
8
+ vPim provides calendaring, scheduling, and contact support for Ruby through the
9
+ standard iCalendar and vCard data formats for "personal information" exchange.
10
+
11
+ = Thanks
12
+
13
+ - http://ZipDX.com: for sponsoring development of FREQ=weekly and BYSETPOS in
14
+ recurrence rules.
15
+ - http://RubyForge.org: for their generous hosting of this project.
16
+
17
+ = Installation
18
+
19
+ There is a vPim package installable using ruby-gems:
20
+
21
+ # sudo gem install vpim (may require root privilege)
22
+
23
+ It is also installable in the standard way. Untar the package, and do:
24
+
25
+ $ ruby setup.rb --help
26
+
27
+ or do:
28
+
29
+ $ ruby setup.rb config
30
+ $ ruby setup.rb setup
31
+ # ruby setup.rb install (may require root privilege)
32
+
33
+ = Overview
9
34
 
10
35
  vCard (RFC 2426) is a format for personal information, see Vpim::Vcard and
11
36
  Vpim::Maker::Vcard.
@@ -23,23 +48,6 @@ instantaneous turnaround, but I might be able to suggest another approach, and
23
48
  features requested by users of vPim go to the top of the todo list. If you need
24
49
  a feature for a commercial project, consider sponsoring development.
25
50
 
26
- = Project Information
27
-
28
- The latest release can be downloaded from the Ruby Forge project page:
29
-
30
- - http://rubyforge.org/projects/vpim
31
-
32
- For notifications about new releases, or asking questions about vPim, please
33
- subscribe to "vpim-talk":
34
-
35
- - http://rubyforge.org/mailman/listinfo/vpim-talk
36
-
37
- = Thanks
38
-
39
- - http://RubyForge.org: for their generous hosting of this project.
40
- - http://ZipDX.com: for sponsoring development of FREQ=weekly and BYSETPOS in
41
- recurrence rules.
42
-
43
51
  = Examples
44
52
 
45
53
  Here's an example to give a sense for how iCalendars are encoded and decoded:
@@ -156,20 +164,19 @@ iCalendar examples are:
156
164
  Apple's iCal calendars
157
165
  - link:rrule.txt: utility for printing recurrence rules
158
166
  - link:ics-dump.txt: utility for dumping contents of .ics files
159
- module Vpim
160
- end
161
167
 
162
- = Installation
168
+ = Project Information
163
169
 
164
- There is a vPim package installable using ruby-gems.
170
+ vPim can be downloaded from the Ruby Forge project page:
165
171
 
166
- It is also installable in the standard way. Untar the package, and see:
172
+ - http://rubyforge.org/projects/vpim
167
173
 
168
- ruby setup.rb --help
174
+ or installed as a gem:
169
175
 
170
- or do:
176
+ - sudo gem install vpim
171
177
 
172
- $ ruby setup.rb config
173
- $ ruby setup.rb setup
174
- # ruby setup.rb install (may require root privilege)
178
+ For notifications about new releases, or to ask questions about vPim, please
179
+ subscribe to "vpim-talk":
180
+
181
+ - http://rubyforge.org/mailman/listinfo/vpim-talk
175
182
 
@@ -118,7 +118,7 @@ end
118
118
  puts
119
119
 
120
120
  def start_of_first_occurrence(t0, t1, e)
121
- e.occurrences.each_until(t1).each do |t|
121
+ e.occurrences(t1) do |t|
122
122
  # An event might start before t0, but end after it..., in which case
123
123
  # we are still interested.
124
124
  if (t + (e.duration || 0)) >= t0
@@ -0,0 +1,728 @@
1
+ # Copyright (c) 2008 The Kaphan Foundation
2
+ #
3
+ # For licensing information see LICENSE.txt.
4
+ =begin License.txt
5
+ Copyright (c) 2008 Peerworks
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ =end
26
+
27
+ require 'forwardable'
28
+ require 'delegate'
29
+ require 'rubygems'
30
+ require 'xml/libxml'
31
+ require 'atom/xml/parser.rb'
32
+
33
+ module Atom # :nodoc:
34
+ NAMESPACE = 'http://www.w3.org/2005/Atom' unless defined?(NAMESPACE)
35
+ module Pub
36
+ NAMESPACE = 'http://www.w3.org/2007/app'
37
+ end
38
+ # Raised when a Parsing Error occurs.
39
+ class ParseError < StandardError; end
40
+ # Raised when a Serialization Error occurs.
41
+ class SerializationError < StandardError; end
42
+
43
+ # Provides support for reading and writing simple extensions as defined by the Atom Syndication Format.
44
+ #
45
+ # A Simple extension is an element from a non-atom namespace that has no attributes and only contains
46
+ # text content. It is interpreted as a key-value pair when the namespace and the localname of the
47
+ # extension make up the key. Since in XML you can have many instances of an element, the values are
48
+ # represented as an array of strings, so to manipulate the values manipulate the array returned by
49
+ # +[ns, localname]+.
50
+ #
51
+ module SimpleExtensions
52
+ attr_reader :simple_extensions
53
+
54
+ # Gets a simple extension value for a given namespace and local name.
55
+ #
56
+ # +ns+:: The namespace.
57
+ # +localname+:: The local name of the extension element.
58
+ #
59
+ def [](ns, localname)
60
+ if !defined?(@simple_extensions) || @simple_extensions.nil?
61
+ @simple_extensions = {}
62
+ end
63
+
64
+ key = "{#{ns},#{localname}}"
65
+ (@simple_extensions[key] or @simple_extensions[key] = ValueProxy.new)
66
+ end
67
+
68
+ class ValueProxy < DelegateClass(Array)
69
+ attr_accessor :as_attribute
70
+ def initialize
71
+ super([])
72
+ @as_attribute = false
73
+ end
74
+ end
75
+ end
76
+
77
+ # Represents a Generator as defined by the Atom Syndication Format specification.
78
+ #
79
+ # The generator identifies an agent or engine used to a produce a feed.
80
+ #
81
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.generator
82
+ class Generator
83
+ include Xml::Parseable
84
+
85
+ attr_accessor :name
86
+ attribute :uri, :version
87
+
88
+ # Initialize a new Generator.
89
+ #
90
+ # +xml+:: An XML::Reader object.
91
+ #
92
+ def initialize(o = nil)
93
+ case o
94
+ when XML::Reader
95
+ @name = o.read_string.strip
96
+ parse(o, :once => true)
97
+ when Hash
98
+ o.each do |k, v|
99
+ self.send("#{k.to_s}=", v)
100
+ end
101
+ end
102
+
103
+ yield(self) if block_given?
104
+ end
105
+ end
106
+
107
+ # Represents a Category as defined by the Atom Syndication Format specification.
108
+ #
109
+ #
110
+ class Category
111
+ include Atom::Xml::Parseable
112
+ include SimpleExtensions
113
+ attribute :label, :scheme, :term
114
+
115
+ def initialize(o = nil)
116
+ case o
117
+ when XML::Reader
118
+ parse(o, :once => true)
119
+ when Hash
120
+ o.each do |k, v|
121
+ self.send("#{k.to_s}=", v)
122
+ end
123
+ end
124
+
125
+ yield(self) if block_given?
126
+ end
127
+ end
128
+
129
+ # Represents a Person as defined by the Atom Syndication Format specification.
130
+ #
131
+ # A Person is used for all author and contributor attributes.
132
+ #
133
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#atomPersonConstruct
134
+ #
135
+ class Person
136
+ include Xml::Parseable
137
+ element :name, :uri, :email
138
+
139
+ # Initialize a new person.
140
+ #
141
+ # +o+:: An XML::Reader object or a hash. Valid hash keys are +:name+, +:uri+ and +:email+.
142
+ def initialize(o = {})
143
+ case o
144
+ when XML::Reader
145
+ o.read
146
+ parse(o)
147
+ when Hash
148
+ o.each do |k, v|
149
+ self.send("#{k.to_s}=", v)
150
+ end
151
+ end
152
+ end
153
+
154
+ def inspect
155
+ "<Atom::Person name:'#{name}' uri:'#{uri}' email:'#{email}"
156
+ end
157
+ end
158
+
159
+ class Content # :nodoc:
160
+ def self.parse(xml)
161
+ case xml['type']
162
+ when "xhtml"
163
+ Xhtml.new(xml)
164
+ when "html"
165
+ Html.new(xml)
166
+ else
167
+ Text.new(xml)
168
+ end
169
+ end
170
+
171
+ # This is the base class for all content within an atom document.
172
+ #
173
+ # Content can be Text, Html or Xhtml.
174
+ #
175
+ # A Content object can be treated as a String with type and xml_lang
176
+ # attributes.
177
+ #
178
+ # For a thorough discussion of atom content see
179
+ # http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.content
180
+ class Base < DelegateClass(String)
181
+ include Xml::Parseable
182
+
183
+ def initialize(c)
184
+ __setobj__(c)
185
+ end
186
+
187
+ def ==(o)
188
+ if o.is_a?(self.class)
189
+ self.type == o.type &&
190
+ self.xml_lang == o.xml_lang &&
191
+ self.to_s == o.to_s
192
+ elsif o.is_a?(String)
193
+ self.to_s == o
194
+ end
195
+ end
196
+
197
+ protected
198
+ def set_content(c) # :nodoc:
199
+ __setobj__(c)
200
+ end
201
+ end
202
+
203
+ # Text content within an Atom document.
204
+ class Text < Base
205
+ attribute :type, :'xml:lang'
206
+ def initialize(xml)
207
+ super(xml.read_string)
208
+ parse(xml, :once => true)
209
+ end
210
+
211
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
212
+ node = XML::Node.new("#{namespace_map.get(Atom::NAMESPACE)}:#{name}")
213
+ node << self.to_s
214
+ node
215
+ end
216
+ end
217
+
218
+ # Html content within an Atom document.
219
+ class Html < Base
220
+ attribute :type, :'xml:lang'
221
+ # Creates a new Content::Html.
222
+ #
223
+ # +o+:: An XML::Reader or a HTML string.
224
+ #
225
+ def initialize(o)
226
+ case o
227
+ when XML::Reader
228
+ super(o.read_string.gsub(/\s+/, ' ').strip)
229
+ parse(o, :once => true)
230
+ when String
231
+ super(o)
232
+ @type = 'html'
233
+ end
234
+ end
235
+
236
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new) # :nodoc:
237
+ require 'iconv'
238
+ # Convert from utf-8 to utf-8 as a way of making sure the content is UTF-8.
239
+ #
240
+ # This is a pretty crappy way to do it but if we don't check libxml just
241
+ # fails silently and outputs the content element without any content. At
242
+ # least checking here and raising an exception gives the caller a chance
243
+ # to try and recitfy the situation.
244
+ #
245
+ begin
246
+ node = XML::Node.new("#{namespace_map.get(Atom::NAMESPACE)}:#{name}")
247
+ node << Iconv.iconv('utf-8', 'utf-8', self.to_s, namespace_map = nil)
248
+ node['type'] = 'html'
249
+ node['xml:lang'] = self.xml_lang
250
+ node
251
+ rescue Iconv::IllegalSequence => e
252
+ raise SerializationError, "Content must be converted to UTF-8 before attempting to serialize to XML: #{e.message}."
253
+ end
254
+ end
255
+ end
256
+
257
+ # XHTML content within an Atom document.
258
+ class Xhtml < Base
259
+ XHTML = 'http://www.w3.org/1999/xhtml'
260
+ attribute :type, :'xml:lang'
261
+
262
+ def initialize(xml)
263
+ super("")
264
+ parse(xml, :once => true)
265
+ starting_depth = xml.depth
266
+
267
+ # Get the next element - should be a div according to the atom spec
268
+ while xml.read == 1 && xml.node_type != XML::Reader::TYPE_ELEMENT; end
269
+
270
+ if xml.local_name == 'div' && xml.namespace_uri == XHTML
271
+ set_content(xml.read_inner_xml.strip.gsub(/\s+/, ' '))
272
+ else
273
+ set_content(xml.read_outer_xml)
274
+ end
275
+
276
+ # get back to the end of the element we were created with
277
+ while xml.read == 1 && xml.depth > starting_depth; end
278
+ end
279
+
280
+ def to_xml(nodeonly = true, name = 'content', namespace = nil, namespace_map = Atom::Xml::NamespaceMap.new)
281
+ node = XML::Node.new("#{namespace_map.get(Atom::NAMESPACE)}:#{name}")
282
+ node['type'] = 'xhtml'
283
+ node['xml:lang'] = self.xml_lang
284
+
285
+ div = XML::Node.new('div')
286
+ div['xmlns'] = XHTML
287
+
288
+ p = XML::Parser.string(to_s)
289
+ content = p.parse.root.copy(true)
290
+ div << content
291
+
292
+ node << div
293
+ node
294
+ end
295
+ end
296
+ end
297
+
298
+ # Represents a Source as defined by the Atom Syndication Format specification.
299
+ #
300
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.source
301
+ class Source
302
+ extend Forwardable
303
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures
304
+ include Xml::Parseable
305
+
306
+ element :id
307
+ element :updated, :class => Time, :content_only => true
308
+ element :title, :subtitle, :class => Content
309
+ elements :authors, :contributors, :class => Person
310
+ elements :links
311
+
312
+ def initialize(o = nil)
313
+ @authors, @contributors, @links = [], [], Links.new
314
+
315
+ case o
316
+ when XML::Reader
317
+ unless current_node_is?(o, 'source', NAMESPACE)
318
+ raise ArgumentError, "Invalid node for atom:source - #{o.name}(#{o.namespace})"
319
+ end
320
+
321
+ o.read
322
+ parse(o)
323
+ when Hash
324
+ o.each do |k, v|
325
+ self.send("#{k.to_s}=", v)
326
+ end
327
+ end
328
+
329
+ yield(self) if block_given?
330
+ end
331
+ end
332
+
333
+ # Represents a Feed as defined by the Atom Syndication Format specification.
334
+ #
335
+ # A feed is the top level element in an atom document. It is a container for feed level
336
+ # metadata and for each entry in the feed.
337
+ #
338
+ # This supports pagination as defined in RFC 5005, see http://www.ietf.org/rfc/rfc5005.txt
339
+ #
340
+ # == Parsing
341
+ #
342
+ # A feed can be parsed using the Feed.load_feed method. This method accepts a String containing
343
+ # a valid atom document, an IO object, or an URI to a valid atom document. For example:
344
+ #
345
+ # # Using a File
346
+ # feed = Feed.load_feed(File.open("/path/to/myfeed.atom"))
347
+ #
348
+ # # Using a URL
349
+ # feed = Feed.load_feed(URI.parse("http://example.org/afeed.atom"))
350
+ #
351
+ # == Encoding
352
+ #
353
+ # A feed can be converted to XML using, the to_xml method that returns a valid atom document in a String.
354
+ #
355
+ # == Attributes
356
+ #
357
+ # A feed has the following attributes:
358
+ #
359
+ # +id+:: A unique id for the feed.
360
+ # +updated+:: The Time the feed was updated.
361
+ # +title+:: The title of the feed.
362
+ # +subtitle+:: The subtitle of the feed.
363
+ # +authors+:: An array of Atom::Person objects that are authors of this feed.
364
+ # +contributors+:: An array of Atom::Person objects that are contributors to this feed.
365
+ # +generator+:: A Atom::Generator.
366
+ # +rights+:: A string describing the rights associated with this feed.
367
+ # +entries+:: An array of Atom::Entry objects.
368
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
369
+ #
370
+ # == References
371
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.feed
372
+ #
373
+ class Feed
374
+ include Xml::Parseable
375
+ include SimpleExtensions
376
+ extend Forwardable
377
+ def_delegators :@links, :alternate, :self, :via, :first_page, :last_page, :next_page, :prev_page
378
+
379
+ loadable!
380
+
381
+ namespace Atom::NAMESPACE
382
+ element :id, :rights
383
+ element :generator, :class => Generator
384
+ element :title, :subtitle, :class => Content
385
+ element :updated, :class => Time, :content_only => true
386
+ elements :links
387
+ elements :authors, :contributors, :class => Person
388
+ elements :entries
389
+
390
+ # Initialize a Feed.
391
+ #
392
+ # This will also yield itself, so a feed can be constructed like this:
393
+ #
394
+ # feed = Feed.new do |feed|
395
+ # feed.title = "My Cool feed"
396
+ # end
397
+ #
398
+ # +o+:: An XML Reader or a Hash of attributes.
399
+ #
400
+ def initialize(o = {})
401
+ @links, @entries, @authors, @contributors = Links.new, [], [], []
402
+
403
+ case o
404
+ when XML::Reader
405
+ if next_node_is?(o, 'feed', Atom::NAMESPACE)
406
+ o.read
407
+ parse(o)
408
+ else
409
+ raise ArgumentError, "XML document was missing atom:feed: #{o.read_outer_xml}"
410
+ end
411
+ when Hash
412
+ o.each do |k, v|
413
+ self.send("#{k.to_s}=", v)
414
+ end
415
+ end
416
+
417
+ yield(self) if block_given?
418
+ end
419
+
420
+ # Return true if this is the first feed in a paginated set.
421
+ def first?
422
+ links.self == links.first_page
423
+ end
424
+
425
+ # Returns true if this is the last feed in a paginated set.
426
+ def last?
427
+ links.self == links.last_page
428
+ end
429
+
430
+ # Reloads the feed by fetching the self uri.
431
+ def reload!
432
+ if links.self
433
+ Feed.load_feed(URI.parse(links.self.href))
434
+ end
435
+ end
436
+
437
+ # Iterates over each entry in the feed.
438
+ #
439
+ # ==== Options
440
+ #
441
+ # +paginate+:: If true and the feed supports pagination this will fetch each page of the feed.
442
+ # +since+:: If a Time object is provided each_entry will iterate over all entries that were updated since that time.
443
+ #
444
+ def each_entry(options = {}, &block)
445
+ if options[:paginate]
446
+ since_reached = false
447
+ feed = self
448
+ loop do
449
+ feed.entries.each do |entry|
450
+ if options[:since] && entry.updated && options[:since] > entry.updated
451
+ since_reached = true
452
+ break
453
+ else
454
+ block.call(entry)
455
+ end
456
+ end
457
+
458
+ if since_reached || feed.next_page.nil?
459
+ break
460
+ else feed.next_page
461
+ feed = feed.next_page.fetch
462
+ end
463
+ end
464
+ else
465
+ self.entries.each(&block)
466
+ end
467
+ end
468
+ end
469
+
470
+ # Represents an Entry as defined by the Atom Syndication Format specification.
471
+ #
472
+ # An Entry represents an individual entry within a Feed.
473
+ #
474
+ # == Parsing
475
+ #
476
+ # An Entry can be parsed using the Entry.load_entry method. This method accepts a String containing
477
+ # a valid atom entry document, an IO object, or an URI to a valid atom entry document. For example:
478
+ #
479
+ # # Using a File
480
+ # entry = Entry.load_entry(File.open("/path/to/myfeedentry.atom"))
481
+ #
482
+ # # Using a URL
483
+ # Entry = Entry.load_entry(URI.parse("http://example.org/afeedentry.atom"))
484
+ #
485
+ # The document must contain a stand alone entry element as described in the Atom Syndication Format.
486
+ #
487
+ # == Encoding
488
+ #
489
+ # A Entry can be converted to XML using, the to_xml method that returns a valid atom entry document in a String.
490
+ #
491
+ # == Attributes
492
+ #
493
+ # An entry has the following attributes:
494
+ #
495
+ # +id+:: A unique id for the entry.
496
+ # +updated+:: The Time the entry was updated.
497
+ # +published+:: The Time the entry was published.
498
+ # +title+:: The title of the entry.
499
+ # +summary+:: A short textual summary of the item.
500
+ # +authors+:: An array of Atom::Person objects that are authors of this entry.
501
+ # +contributors+:: An array of Atom::Person objects that are contributors to this entry.
502
+ # +rights+:: A string describing the rights associated with this entry.
503
+ # +links+:: An array of Atom:Link objects. (This is actually an Atom::Links array which is an Array with some sugar).
504
+ # +source+:: Metadata of a feed that was the source of this item, for feed aggregators, etc.
505
+ # +categories+:: Array of Atom::Categories.
506
+ # +content+:: The content of the entry. This will be one of Atom::Content::Text, Atom::Content:Html or Atom::Content::Xhtml.
507
+ #
508
+ # == References
509
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.entry for more detailed
510
+ # definitions of the attributes.
511
+ #
512
+ class Entry
513
+ include Xml::Parseable
514
+ include SimpleExtensions
515
+ extend Forwardable
516
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures, :edit_link, :via
517
+
518
+ loadable!
519
+ namespace Atom::NAMESPACE
520
+ element :title, :id, :summary
521
+ element :updated, :published, :class => Time, :content_only => true
522
+ element :content, :class => Content
523
+ element :source, :class => Source
524
+ elements :links
525
+ elements :authors, :contributors, :class => Person
526
+ elements :categories, :class => Category
527
+
528
+ # Initialize an Entry.
529
+ #
530
+ # This will also yield itself, so an Entry can be constructed like this:
531
+ #
532
+ # entry = Entry.new do |entry|
533
+ # entry.title = "My Cool entry"
534
+ # end
535
+ #
536
+ # +o+:: An XML Reader or a Hash of attributes.
537
+ #
538
+ def initialize(o = {})
539
+ @links = Links.new
540
+ @authors = []
541
+ @contributors = []
542
+ @categories = []
543
+
544
+ case o
545
+ when XML::Reader
546
+ if current_node_is?(o, 'entry', Atom::NAMESPACE) || next_node_is?(o, 'entry', Atom::NAMESPACE)
547
+ o.read
548
+ parse(o)
549
+ else
550
+ raise ArgumentError, "Entry created with node other than atom:entry: #{o.name}"
551
+ end
552
+ when Hash
553
+ o.each do |k,v|
554
+ send("#{k.to_s}=", v)
555
+ end
556
+ end
557
+
558
+ yield(self) if block_given?
559
+ end
560
+
561
+ # Reload the Entry by fetching the self link.
562
+ def reload!
563
+ if links.self
564
+ Entry.load_entry(URI.parse(links.self.href))
565
+ end
566
+ end
567
+ end
568
+
569
+ # Links provides an Array of Link objects belonging to either a Feed or an Entry.
570
+ #
571
+ # Some additional methods to get specific types of links are provided.
572
+ #
573
+ # == References
574
+ #
575
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link
576
+ # for details on link selection and link attributes.
577
+ #
578
+ class Links < DelegateClass(Array)
579
+ include Enumerable
580
+
581
+ # Initialize an empty Links array.
582
+ def initialize
583
+ super([])
584
+ end
585
+
586
+ # Get the alternate.
587
+ #
588
+ # Returns the first link with rel == 'alternate' that matches the given type.
589
+ def alternate(type = nil)
590
+ detect { |link| (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type) }
591
+ end
592
+
593
+ # Get all alternates.
594
+ def alternates
595
+ select { |link| link.rel.nil? || link.rel == Link::Rel::ALTERNATE }
596
+ end
597
+
598
+ # Gets the self link.
599
+ def self
600
+ detect { |link| link.rel == Link::Rel::SELF }
601
+ end
602
+
603
+ # Gets the via link.
604
+ def via
605
+ detect { |link| link.rel == Link::Rel::VIA }
606
+ end
607
+
608
+ # Gets all links with rel == 'enclosure'
609
+ def enclosures
610
+ select { |link| link.rel == Link::Rel::ENCLOSURE }
611
+ end
612
+
613
+ # Gets the link with rel == 'first'.
614
+ #
615
+ # This is defined as the first page in a pagination set.
616
+ def first_page
617
+ detect { |link| link.rel == Link::Rel::FIRST }
618
+ end
619
+
620
+ # Gets the link with rel == 'last'.
621
+ #
622
+ # This is defined as the last page in a pagination set.
623
+ def last_page
624
+ detect { |link| link.rel == Link::Rel::LAST }
625
+ end
626
+
627
+ # Gets the link with rel == 'next'.
628
+ #
629
+ # This is defined as the next page in a pagination set.
630
+ def next_page
631
+ detect { |link| link.rel == Link::Rel::NEXT }
632
+ end
633
+
634
+ # Gets the link with rel == 'prev'.
635
+ #
636
+ # This is defined as the previous page in a pagination set.
637
+ def prev_page
638
+ detect { |link| link.rel == Link::Rel::PREVIOUS }
639
+ end
640
+
641
+ # Gets the edit link.
642
+ #
643
+ # This is the link which can be used for posting updates to an item using the Atom Publishing Protocol.
644
+ #
645
+ def edit_link
646
+ detect { |link| link.rel == 'edit' }
647
+ end
648
+ end
649
+
650
+ # Represents a link in an Atom document.
651
+ #
652
+ # A link defines a reference from an Atom document to a web resource.
653
+ #
654
+ # == References
655
+ # See http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link for
656
+ # a description of the different types of links.
657
+ #
658
+ class Link
659
+ module Rel # :nodoc:
660
+ ALTERNATE = 'alternate'
661
+ SELF = 'self'
662
+ VIA = 'via'
663
+ ENCLOSURE = 'enclosure'
664
+ FIRST = 'first'
665
+ LAST = 'last'
666
+ PREVIOUS = 'prev'
667
+ NEXT = 'next'
668
+ end
669
+
670
+ include Xml::Parseable
671
+ attribute :href, :rel, :type, :length
672
+
673
+ # Create a link.
674
+ #
675
+ # +o+:: An XML::Reader containing a link element or a Hash of attributes.
676
+ #
677
+ def initialize(o)
678
+ case o
679
+ when XML::Reader
680
+ if current_node_is?(o, 'link')
681
+ parse(o, :once => true)
682
+ else
683
+ raise ArgumentError, "Link created with node other than atom:link: #{o.name}"
684
+ end
685
+ when Hash
686
+ [:href, :rel, :type, :length].each do |attr|
687
+ self.send("#{attr}=", o[attr])
688
+ end
689
+ else
690
+ raise ArgumentError, "Don't know how to handle #{o}"
691
+ end
692
+ end
693
+
694
+ remove_method :length=
695
+ def length=(v)
696
+ @length = v.to_i
697
+ end
698
+
699
+ def to_s
700
+ self.href
701
+ end
702
+
703
+ def ==(o)
704
+ o.respond_to?(:href) && o.href == self.href
705
+ end
706
+
707
+ # This will fetch the URL referenced by the link.
708
+ #
709
+ # If the URL contains a valid feed, a Feed will be returned, otherwise,
710
+ # the body of the response will be returned.
711
+ #
712
+ # TODO: Handle redirects.
713
+ #
714
+ def fetch
715
+ content = Net::HTTP.get_response(URI.parse(self.href)).body
716
+
717
+ begin
718
+ Atom::Feed.load_feed(content)
719
+ rescue ArgumentError, ParseError => ae
720
+ content
721
+ end
722
+ end
723
+
724
+ def inspect
725
+ "<Atom::Link href:'#{href}' type:'#{type}'>"
726
+ end
727
+ end
728
+ end