ratom 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +7 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +123 -0
  5. data/Rakefile +4 -0
  6. data/config/hoe.rb +72 -0
  7. data/config/requirements.rb +17 -0
  8. data/lib/atom.rb +566 -0
  9. data/lib/atom/pub.rb +185 -0
  10. data/lib/atom/version.rb +9 -0
  11. data/lib/atom/xml/parser.rb +223 -0
  12. data/setup.rb +1585 -0
  13. data/spec/app/member_entry.atom +31 -0
  14. data/spec/app/service.xml +36 -0
  15. data/spec/atom/pub_spec.rb +289 -0
  16. data/spec/atom_spec.rb +1012 -0
  17. data/spec/conformance/baseuri.atom +19 -0
  18. data/spec/conformance/divtest.atom +32 -0
  19. data/spec/conformance/linktests.xml +93 -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/multiple_entry.atom +0 -0
  38. data/spec/fixtures/simple_single_entry.atom +21 -0
  39. data/spec/paging/first_paged_feed.atom +21 -0
  40. data/spec/paging/last_paged_feed.atom +21 -0
  41. data/spec/paging/middle_paged_feed.atom +22 -0
  42. data/spec/spec.opts +2 -0
  43. data/spec/spec_helper.rb +24 -0
  44. data/tasks/deployment.rake +34 -0
  45. data/tasks/environment.rake +7 -0
  46. data/tasks/rspec.rake +15 -0
  47. data/tasks/website.rake +17 -0
  48. data/website/index.html +11 -0
  49. data/website/index.txt +39 -0
  50. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  51. data/website/stylesheets/screen.css +138 -0
  52. data/website/template.rhtml +48 -0
  53. metadata +126 -0
data/History.txt ADDED
@@ -0,0 +1,7 @@
1
+ == 0.2.1 2008-03-03
2
+
3
+ * Initial release to the public.
4
+
5
+ == < 0.2.1
6
+
7
+ * Internal releases.
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Peerworks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,52 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/atom.rb
9
+ lib/atom/pub.rb
10
+ lib/atom/version.rb
11
+ lib/atom/xml/parser.rb
12
+ setup.rb
13
+ spec/app/member_entry.atom
14
+ spec/app/service.xml
15
+ spec/atom/pub_spec.rb
16
+ spec/atom_spec.rb
17
+ spec/conformance/baseuri.atom
18
+ spec/conformance/divtest.atom
19
+ spec/conformance/linktests.xml
20
+ spec/conformance/nondefaultnamespace-baseline.atom
21
+ spec/conformance/nondefaultnamespace-xhtml.atom
22
+ spec/conformance/nondefaultnamespace.atom
23
+ spec/conformance/ordertest.xml
24
+ spec/conformance/title/html-cdata.atom
25
+ spec/conformance/title/html-entity.atom
26
+ spec/conformance/title/html-ncr.atom
27
+ spec/conformance/title/text-cdata.atom
28
+ spec/conformance/title/text-entity.atom
29
+ spec/conformance/title/text-ncr.atom
30
+ spec/conformance/title/xhtml-entity.atom
31
+ spec/conformance/title/xhtml-ncr.atom
32
+ spec/conformance/unknown-namespace.atom
33
+ spec/conformance/xmlbase.atom
34
+ spec/fixtures/complex_single_entry.atom
35
+ spec/fixtures/created_entry.atom
36
+ spec/fixtures/entry.atom
37
+ spec/fixtures/multiple_entry.atom
38
+ spec/fixtures/simple_single_entry.atom
39
+ spec/paging/first_paged_feed.atom
40
+ spec/paging/last_paged_feed.atom
41
+ spec/paging/middle_paged_feed.atom
42
+ spec/spec.opts
43
+ spec/spec_helper.rb
44
+ tasks/deployment.rake
45
+ tasks/environment.rake
46
+ tasks/rspec.rake
47
+ tasks/website.rake
48
+ website/index.html
49
+ website/index.txt
50
+ website/javascripts/rounded_corners_lite.inc.js
51
+ website/stylesheets/screen.css
52
+ website/template.rhtml
data/README.txt ADDED
@@ -0,0 +1,123 @@
1
+ = rAtom
2
+
3
+ rAtom is a library for working with the Atom Syndication Format and
4
+ the Atom Publishing Protocol (APP).
5
+
6
+ * Built using libxml so it is _much_ faster than a REXML based library.
7
+ * Uses the libxml pull parser so it has much lighter memory usage.
8
+ * Supports {RFC 5005}[http://www.ietf.org/rfc/rfc5005.txt] for feed pagination.
9
+
10
+ rAtom was originally built to support the communication between a number of applications
11
+ built by Peerworks[http://peerworks.org], via the Atom Publishing protocol. However, it
12
+ supports, or aims to support, all the Atom Syndication Format and Publication Protocol
13
+ and can be used to access Atom feeds or to script publishing entries to a blog supporting APP.
14
+
15
+ == Prerequisites
16
+
17
+ * ActiveSupport, >= 2.0.1
18
+ * libxml-ruby, = 0.5.2.0
19
+ * rspec (Only required for tests)
20
+
21
+ libxml-ruby in turn requires the libxml2 library to be installed. libxml2 can be downloaded
22
+ from http://xmlsoft.org/downloads.html or installed using whatever tools are provided by your
23
+ platform. At least version 2.6.31 is required.
24
+
25
+ === Mac OSX
26
+
27
+ Mac OSX by default comes with an old version of libxml2 that will not work with rAtom. You
28
+ will need to install a more recent version. If you are using Macports:
29
+
30
+ port install libxml2
31
+
32
+ == Installation
33
+
34
+ You can install via gem using:
35
+
36
+ gem install ratom
37
+
38
+ == Usage
39
+
40
+ To fetch a parse an Atom Feed you can simply:
41
+
42
+ feed = Atom::Feed.load_feed(URI.parse("http://example.com/feed.atom"))
43
+
44
+ And then iterate over the entries in the feed using:
45
+
46
+ feed.each_entry do |entry|
47
+ # do cool stuff
48
+ end
49
+
50
+ To construct a Feed
51
+
52
+ feed = Atom::Feed.new do |feed|
53
+ feed.title = "My Cool Feed"
54
+ feed.id = "http://example.com/my_feed.atom"
55
+ feed.updated = Time.now
56
+ end
57
+
58
+ To output a Feed as XML use to_xml
59
+
60
+ > puts feed.to_xml
61
+ <?xml version="1.0"?>
62
+ <feed xmlns="http://www.w3.org/2005/Atom">
63
+ <title>My Cool Feed</title>
64
+ <id>http://example.com/my_feed.atom</id>
65
+ <updated>2008-03-03T23:19:44+10:30</updated>
66
+ </feed>
67
+
68
+ See Feed and Entry for details on the methods and attributes of those classes.
69
+
70
+ === Publishing
71
+
72
+ To publish to a remote feed using the Atom Publishing Protocol, first you need to create a collection to publish to:
73
+
74
+ require 'atom/pub'
75
+
76
+ collection = Atom::Pub::Collection.new(:href => 'http://example.org/myblog')
77
+
78
+ Then create a new entry
79
+
80
+ entry = Atom::Entry.new do |entry|
81
+ entry.title = "I have discovered rAtom"
82
+ entry.authors << Atom::Person.new(:name => 'A happy developer')
83
+ entry.updated = Time.now
84
+ entry.id = "http://example.org/myblog/newpost"
85
+ entry.content = Atom::Content::Html.new("<p>rAtom lets me post to my blog using Ruby, how cool!</p>")
86
+ end
87
+
88
+ And publish it to the Collection:
89
+
90
+ published_entry = collection.publish(entry)
91
+
92
+ Publish returns an updated entry filled out with any attributes to server may have set, including information
93
+ required to let us update to the entry. For example, lets change the content and republished:
94
+
95
+ published_entry.content = Atom::Content::Html.new("<p>rAtom lets me post to and edit my blog using Ruby, how cool!</p>")
96
+ published_entry.updated = Time.now
97
+ published_entry.save!
98
+
99
+ You can also delete an entry using the <tt>destroy!</tt> method, but we won't do that will we?.
100
+
101
+
102
+ == TODO
103
+
104
+ * Support partial content responses from the server.
105
+ * Support batching of protocol operations.
106
+ * Examples of editing existing entries.
107
+ * All my tests have been against internal systems, I'd really like feedback from those who have tried rAtom using existing blog software that supports APP.
108
+
109
+ == Source Code
110
+
111
+ The source repository is accessible via GitHub:
112
+
113
+ git clone git://github.com/seangeo/ratom.git
114
+
115
+ == Contact Information
116
+
117
+ The project page is at http://rubyforge.org/projects/ratom. Please file any bugs or feedback
118
+ using the trackers and forums there.
119
+
120
+ == Authors and Contributors
121
+
122
+ rAtom was developed by Peerworks[http://peerworks.org] and written by Sean Geoghegan.
123
+
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
data/config/hoe.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'atom/version'
2
+
3
+ AUTHOR = 'Peerworks' # can also be an array of Authors
4
+ EMAIL = "info@peerworks.org"
5
+ DESCRIPTION = "Atom Syndication and Publication API"
6
+ GEM_NAME = 'ratom' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'ratom' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "sgeo"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = Atom::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'atom documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/test_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
62
+ # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+ p.extra_deps = [['activesupport', '>= 2.0.1'], ['libxml-ruby', '= 0.5.2.0']]
64
+
65
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
66
+
67
+ end
68
+
69
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
70
+ PATH = RUBYFORGE_PROJECT
71
+ hoe.remote_rdoc_dir = PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,'')
72
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
16
+
17
+ require 'atom'
data/lib/atom.rb ADDED
@@ -0,0 +1,566 @@
1
+ # Copyright (c) 2008 The Kaphan Foundation
2
+ #
3
+ # For licensing information see LICENSE.txt.
4
+ #
5
+
6
+ require 'forwardable'
7
+ require 'rubygems'
8
+ require 'xml/libxml'
9
+ require 'activesupport'
10
+ require 'atom/xml/parser.rb'
11
+
12
+ module Atom # :nodoc:
13
+ NAMESPACE = 'http://www.w3.org/2005/Atom' unless defined?(NAMESPACE)
14
+
15
+ # Raised when a Parsing Error occurs.
16
+ class ParseError < StandardError; end
17
+
18
+ # Represents a Generator as defined by the Atom Syndication Format specification.
19
+ #
20
+ # The generator identifies an agent or engine used to a produce a feed.
21
+ #
22
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.generator
23
+ class Generator
24
+ include Xml::Parseable
25
+
26
+ attr_reader :name
27
+ attribute :uri, :version
28
+
29
+ # Initialize a new Generator.
30
+ #
31
+ # +xml+:: An XML::Reader object.
32
+ #
33
+ def initialize(xml)
34
+ @name = xml.read_string.strip
35
+ parse(xml, :once => true)
36
+ end
37
+ end
38
+
39
+ # Represents a Person as defined by the Atom Syndication Format specification.
40
+ #
41
+ # A Person is used for all author and contributor attributes.
42
+ #
43
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#atomPersonConstruct
44
+ #
45
+ class Person
46
+ include Xml::Parseable
47
+ element :name, :uri, :email
48
+
49
+ # Initialize a new person.
50
+ #
51
+ # +o+:: An XML::Reader object or a hash. Valid hash keys are +:name+, +:uri+ and +:email+.
52
+ def initialize(o = {})
53
+ case o
54
+ when XML::Reader
55
+ o.read
56
+ parse(o)
57
+ when Hash
58
+ o.each do |k, v|
59
+ self.send("#{k.to_s}=", v)
60
+ end
61
+ end
62
+ end
63
+
64
+ def inspect
65
+ "<Atom::Person name:'#{name}' uri:'#{uri}' email:'#{email}"
66
+ end
67
+ end
68
+
69
+ class Content # :nodoc:
70
+ def self.parse(xml)
71
+ case xml['type']
72
+ when "xhtml"
73
+ Xhtml.new(xml)
74
+ when "html"
75
+ Html.new(xml)
76
+ else
77
+ Text.new(xml)
78
+ end
79
+ end
80
+
81
+ # This is the base class for all content within an atom document.
82
+ #
83
+ # Content can be Text, Html or Xhtml.
84
+ #
85
+ # A Content object can be treated as a String with type and xml_lang
86
+ # attributes.
87
+ #
88
+ # For a thorough discussion of atom content see
89
+ # http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.content
90
+ class Base < DelegateClass(String)
91
+ include Xml::Parseable
92
+ attribute :type, :'xml:lang'
93
+
94
+ def initialize(c)
95
+ __setobj__(c)
96
+ end
97
+
98
+ def ==(o)
99
+ if o.is_a?(self.class)
100
+ self.type == o.type &&
101
+ self.xml_lang == o.xml_lang &&
102
+ self.to_s == o.to_s
103
+ elsif o.is_a?(String)
104
+ self.to_s == o
105
+ end
106
+ end
107
+
108
+ protected
109
+ def set_content(c) # :nodoc:
110
+ __setobj__(c)
111
+ end
112
+ end
113
+
114
+ # Text content within an Atom document.
115
+ class Text < Base
116
+ def initialize(xml)
117
+ super(xml.read_string)
118
+ parse(xml, :once => true)
119
+ end
120
+ end
121
+
122
+ # Html content within an Atom document.
123
+ class Html < Base
124
+
125
+ # Creates a new Content::Html.
126
+ #
127
+ # +o+:: An XML::Reader or a HTML string.
128
+ #
129
+ def initialize(o)
130
+ case o
131
+ when XML::Reader
132
+ super(o.read_string.gsub(/\s+/, ' ').strip)
133
+ parse(o, :once => true)
134
+ when String
135
+ super(o)
136
+ @type = 'html'
137
+ end
138
+ end
139
+
140
+ def to_xml(nodeonly = true, name = 'content') # :nodoc:
141
+ node = XML::Node.new(name)
142
+ node << self.to_s
143
+ node['type'] = 'html'
144
+ node['xml:lang'] = self.xml_lang
145
+ node
146
+ end
147
+ end
148
+
149
+ # XHTML content within an Atom document.
150
+ class Xhtml < Base
151
+ XHTML = 'http://www.w3.org/1999/xhtml'
152
+
153
+ def initialize(xml)
154
+ parse(xml, :once => true)
155
+ starting_depth = xml.depth
156
+
157
+ # Get the next element - should be a div according to the atom spec
158
+ while xml.read == 1 && xml.node_type != XML::Reader::TYPE_ELEMENT; end
159
+
160
+ if xml.local_name == 'div' && xml.namespace_uri == XHTML
161
+ super(xml.read_inner_xml.strip.gsub(/\s+/, ' '))
162
+ else
163
+ super(xml.read_outer_xml)
164
+ end
165
+
166
+ # get back to the end of the element we were created with
167
+ while xml.read == 1 && xml.depth > starting_depth; end
168
+ end
169
+
170
+ def to_xml(nodeonly = true, name = 'content')
171
+ node = XML::Node.new(name)
172
+ node['type'] = 'xhtml'
173
+ node['xml:lang'] = self.xml_lang
174
+
175
+ div = XML::Node.new('div')
176
+ div['xmlns'] = XHTML
177
+ div
178
+
179
+ p = XML::Parser.string(to_s)
180
+ content = p.parse.root.copy(true)
181
+ div << content
182
+
183
+ node << div
184
+ node
185
+ end
186
+ end
187
+ end
188
+
189
+ # Represents a Source as defined by the Atom Syndication Format specification.
190
+ #
191
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.source
192
+ class Source
193
+ extend Forwardable
194
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures
195
+ include Xml::Parseable
196
+
197
+ element :id
198
+ element :updated, :class => Time, :content_only => true
199
+ element :title, :subtitle, :class => Content
200
+ elements :authors, :contributors, :class => Person
201
+ elements :links
202
+
203
+ def initialize(xml)
204
+ unless current_node_is?(xml, 'source', NAMESPACE)
205
+ raise ArgumentError, "Invalid node for atom:source - #{xml.name}(#{xml.namespace})"
206
+ end
207
+
208
+ @authors, @contributors, @links = [], [], Links.new
209
+
210
+ xml.read
211
+ parse(xml)
212
+ end
213
+ end
214
+
215
+ # Represents a Feed as defined by the Atom Syndication Format specification.
216
+ #
217
+ # A feed is the top level element in an atom document. It is a container for feed level
218
+ # metadata and for each entry in the feed.
219
+ #
220
+ # This supports pagination as defined in RFC 5005, see http://www.ietf.org/rfc/rfc5005.txt
221
+ #
222
+ # == Parsing
223
+ #
224
+ # A feed can be parsed using the Feed.load_feed method. This method accepts a String containing
225
+ # a valid atom document, an IO object, or an URI to a valid atom document. For example:
226
+ #
227
+ # # Using a File
228
+ # feed = Feed.load_feed(File.open("/path/to/myfeed.atom"))
229
+ #
230
+ # # Using a URL
231
+ # feed = Feed.load_feed(URI.parse("http://example.org/afeed.atom"))
232
+ #
233
+ # == Encoding
234
+ #
235
+ # A feed can be converted to XML using, the to_xml method that returns a valid atom document in a String.
236
+ #
237
+ # == References
238
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.feed
239
+ class Feed
240
+ include Xml::Parseable
241
+ extend Forwardable
242
+ def_delegators :@links, :alternate, :self, :via, :first_page, :last_page, :next_page, :prev_page
243
+
244
+ loadable!
245
+
246
+ element :id, :rights
247
+ element :generator, :class => Generator
248
+ element :title, :subtitle, :class => Content
249
+ element :updated, :published, :class => Time, :content_only => true
250
+ elements :links, :entries
251
+
252
+ # Initialize a Feed.
253
+ #
254
+ # This will also yield itself, so a feed can be constructed like this:
255
+ #
256
+ # feed = Feed.new do |feed|
257
+ # feed.title = "My Cool feed"
258
+ # end
259
+ #
260
+ # +o+:: An XML Reader or a Hash of attributes.
261
+ #
262
+ def initialize(o = {})
263
+ @links, @entries = Links.new, []
264
+
265
+ case o
266
+ when XML::Reader
267
+ if next_node_is?(o, 'feed', Atom::NAMESPACE)
268
+ o.read
269
+ parse(o)
270
+ else
271
+ raise ArgumentError, "XML document was missing atom:feed: #{o.read_outer_xml}"
272
+ end
273
+ when Hash
274
+ o.each do |k, v|
275
+ self.send("#{k.to_s}=", v)
276
+ end
277
+ end
278
+
279
+ yield(self) if block_given?
280
+ end
281
+
282
+ # Return true if this is the first feed in a paginated set.
283
+ def first?
284
+ links.self == links.first_page
285
+ end
286
+
287
+ # Returns true if this is the last feed in a paginated set.
288
+ def last?
289
+ links.self == links.last_page
290
+ end
291
+
292
+ # Reloads the feed by fetching the self uri.
293
+ def reload!
294
+ if links.self
295
+ Feed.load_feed(URI.parse(links.self.href))
296
+ end
297
+ end
298
+
299
+ # Iterates over each entry in the feed.
300
+ #
301
+ # ==== Options
302
+ #
303
+ # +paginate+:: If true and the feed supports pagination this will fetch each page of the feed.
304
+ # +since+:: If a Time object is provided each_entry will iterate over all entries that were updated since that time.
305
+ #
306
+ def each_entry(options = {}, &block)
307
+ if options[:paginate]
308
+ since_reached = false
309
+ feed = self
310
+ loop do
311
+ feed.entries.each do |entry|
312
+ if options[:since] && entry.updated && options[:since] > entry.updated
313
+ since_reached = true
314
+ break
315
+ else
316
+ block.call(entry)
317
+ end
318
+ end
319
+
320
+ if since_reached || feed.next_page.nil?
321
+ break
322
+ else feed.next_page
323
+ feed = feed.next_page.fetch
324
+ end
325
+ end
326
+ else
327
+ self.entries.each(&block)
328
+ end
329
+ end
330
+ end
331
+
332
+ # Represents an Entry as defined by the Atom Syndication Format specification.
333
+ #
334
+ # An Entry represents an individual entry within a Feed.
335
+ #
336
+ # == Parsing
337
+ #
338
+ # An Entry can be parsed using the Entry.load_entry method. This method accepts a String containing
339
+ # a valid atom entry document, an IO object, or an URI to a valid atom entry document. For example:
340
+ #
341
+ # # Using a File
342
+ # entry = Entry.load_entry(File.open("/path/to/myfeedentry.atom"))
343
+ #
344
+ # # Using a URL
345
+ # Entry = Entry.load_entry(URI.parse("http://example.org/afeedentry.atom"))
346
+ #
347
+ # The document must contain a stand alone entry element as described in the Atom Syndication Format.
348
+ #
349
+ # == Encoding
350
+ #
351
+ # A Entry can be converted to XML using, the to_xml method that returns a valid atom entry document in a String.
352
+ #
353
+ # == References
354
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.entry
355
+ class Entry
356
+ include Xml::Parseable
357
+ extend Forwardable
358
+ def_delegators :@links, :alternate, :self, :alternates, :enclosures, :edit_link, :via
359
+
360
+ loadable!
361
+ element :title, :id, :summary
362
+ element :updated, :published, :class => Time, :content_only => true
363
+ element :content, :class => Content
364
+ element :source, :class => Source
365
+ elements :links
366
+ elements :authors, :contributors, :class => Person
367
+
368
+ # Initialize an Entry.
369
+ #
370
+ # This will also yield itself, so an Entry can be constructed like this:
371
+ #
372
+ # entry = Entry.new do |entry|
373
+ # entry.title = "My Cool entry"
374
+ # end
375
+ #
376
+ # +o+:: An XML Reader or a Hash of attributes.
377
+ #
378
+ def initialize(o = {})
379
+ @links = Links.new
380
+ @authors = []
381
+ @contributors = []
382
+
383
+ case o
384
+ when XML::Reader
385
+ if current_node_is?(o, 'entry', Atom::NAMESPACE) || next_node_is?(o, 'entry', Atom::NAMESPACE)
386
+ o.read
387
+ parse(o)
388
+ else
389
+ raise ArgumentError, "Entry created with node other than atom:entry: #{o.name}"
390
+ end
391
+ when Hash
392
+ o.each do |k,v|
393
+ send("#{k.to_s}=", v)
394
+ end
395
+ end
396
+
397
+ yield(self) if block_given?
398
+ end
399
+
400
+ # Reload the Entry by fetching the self link.
401
+ def reload!
402
+ if links.self
403
+ Entry.load_entry(URI.parse(links.self.href))
404
+ end
405
+ end
406
+ end
407
+
408
+ # Links provides an Array of Link objects belonging to either a Feed or an Entry.
409
+ #
410
+ # Some additional methods to get specific types of links are provided.
411
+ #
412
+ # == References
413
+ #
414
+ # See also http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link
415
+ # for details on link selection and link attributes.
416
+ #
417
+ class Links < DelegateClass(Array)
418
+ include Enumerable
419
+
420
+ # Initialize an empty Links array.
421
+ def initialize
422
+ super([])
423
+ end
424
+
425
+ # Get the alternate.
426
+ #
427
+ # Returns the first link with rel == 'alternate' that matches the given type.
428
+ def alternate(type = nil)
429
+ detect { |link| (link.rel.nil? || link.rel == Link::Rel::ALTERNATE) && (type.nil? || type == link.type) }
430
+ end
431
+
432
+ # Get all alternates.
433
+ def alternates
434
+ select { |link| link.rel.nil? || link.rel == Link::Rel::ALTERNATE }
435
+ end
436
+
437
+ # Gets the self link.
438
+ def self
439
+ detect { |link| link.rel == Link::Rel::SELF }
440
+ end
441
+
442
+ # Gets the via link.
443
+ def via
444
+ detect { |link| link.rel == Link::Rel::VIA }
445
+ end
446
+
447
+ # Gets all links with rel == 'enclosure'
448
+ def enclosures
449
+ select { |link| link.rel == Link::Rel::ENCLOSURE }
450
+ end
451
+
452
+ # Gets the link with rel == 'first'.
453
+ #
454
+ # This is defined as the first page in a pagination set.
455
+ def first_page
456
+ detect { |link| link.rel == Link::Rel::FIRST }
457
+ end
458
+
459
+ # Gets the link with rel == 'last'.
460
+ #
461
+ # This is defined as the last page in a pagination set.
462
+ def last_page
463
+ detect { |link| link.rel == Link::Rel::LAST }
464
+ end
465
+
466
+ # Gets the link with rel == 'next'.
467
+ #
468
+ # This is defined as the next page in a pagination set.
469
+ def next_page
470
+ detect { |link| link.rel == Link::Rel::NEXT }
471
+ end
472
+
473
+ # Gets the link with rel == 'prev'.
474
+ #
475
+ # This is defined as the previous page in a pagination set.
476
+ def prev_page
477
+ detect { |link| link.rel == Link::Rel::PREVIOUS }
478
+ end
479
+
480
+ # Gets the edit link.
481
+ #
482
+ # This is the link which can be used for posting updates to an item using the Atom Publishing Protocol.
483
+ #
484
+ def edit_link
485
+ detect { |link| link.rel == 'edit' }
486
+ end
487
+ end
488
+
489
+ # Represents a link in an Atom document.
490
+ #
491
+ # A link defines a reference from an Atom document to a web resource.
492
+ #
493
+ # == References
494
+ # See http://www.atomenabled.org/developers/syndication/atom-format-spec.php#element.link for
495
+ # a description of the different types of links.
496
+ #
497
+ class Link
498
+ module Rel # :nodoc:
499
+ ALTERNATE = 'alternate'
500
+ SELF = 'self'
501
+ VIA = 'via'
502
+ ENCLOSURE = 'enclosure'
503
+ FIRST = 'first'
504
+ LAST = 'last'
505
+ PREVIOUS = 'prev'
506
+ NEXT = 'next'
507
+ end
508
+
509
+ include Xml::Parseable
510
+ attribute :href, :rel, :type, :length
511
+
512
+ # Create a link.
513
+ #
514
+ # +o+:: An XML::Reader containing a link element or a Hash of attributes.
515
+ #
516
+ def initialize(o)
517
+ case o
518
+ when XML::Reader
519
+ if current_node_is?(o, 'link')
520
+ parse(o, :once => true)
521
+ else
522
+ raise ArgumentError, "Link created with node other than atom:link: #{o.name}"
523
+ end
524
+ when Hash
525
+ [:href, :rel, :type, :length].each do |attr|
526
+ self.send("#{attr}=", o[attr])
527
+ end
528
+ else
529
+ raise ArgumentError, "Don't know how to handle #{o}"
530
+ end
531
+ end
532
+
533
+ def length=(v)
534
+ @length = v.to_i
535
+ end
536
+
537
+ def to_s
538
+ self.href
539
+ end
540
+
541
+ def ==(o)
542
+ o.respond_to?(:href) && o.href == self.href
543
+ end
544
+
545
+ # This will fetch the URL referenced by the link.
546
+ #
547
+ # If the URL contains a valid feed, a Feed will be returned, otherwise,
548
+ # the body of the response will be returned.
549
+ #
550
+ # TODO: Handle redirects.
551
+ #
552
+ def fetch
553
+ content = Net::HTTP.get_response(URI.parse(self.href)).body
554
+
555
+ begin
556
+ Atom::Feed.load_feed(content)
557
+ rescue ArgumentError, ParseError => ae
558
+ content
559
+ end
560
+ end
561
+
562
+ def inspect
563
+ "<Atom::Link href:'#{href}' type:'#{type}'>"
564
+ end
565
+ end
566
+ end