ratom 0.2.1

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 (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