atom-tools 0.9.0
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.
- data/COPYING +18 -0
- data/README +103 -0
- data/Rakefile +77 -0
- data/bin/atom-client.rb +246 -0
- data/bin/atom-server.rb~ +71 -0
- data/doc/classes/Atom/App.html +217 -0
- data/doc/classes/Atom/Author.html +130 -0
- data/doc/classes/Atom/Category.html +128 -0
- data/doc/classes/Atom/Collection.html +322 -0
- data/doc/classes/Atom/Content.html +129 -0
- data/doc/classes/Atom/Contributor.html +119 -0
- data/doc/classes/Atom/Element.html +325 -0
- data/doc/classes/Atom/Entry.html +365 -0
- data/doc/classes/Atom/Feed.html +585 -0
- data/doc/classes/Atom/HTTP.html +374 -0
- data/doc/classes/Atom/Link.html +137 -0
- data/doc/classes/Atom/Text.html +229 -0
- data/doc/classes/XHTML.html +118 -0
- data/doc/created.rid +1 -0
- data/doc/files/README.html +213 -0
- data/doc/files/lib/atom/app_rb.html +110 -0
- data/doc/files/lib/atom/collection_rb.html +110 -0
- data/doc/files/lib/atom/element_rb.html +109 -0
- data/doc/files/lib/atom/entry_rb.html +111 -0
- data/doc/files/lib/atom/feed_rb.html +112 -0
- data/doc/files/lib/atom/http_rb.html +109 -0
- data/doc/files/lib/atom/text_rb.html +108 -0
- data/doc/files/lib/atom/xml_rb.html +110 -0
- data/doc/files/lib/atom/yaml_rb.html +109 -0
- data/doc/fr_class_index.html +39 -0
- data/doc/fr_file_index.html +36 -0
- data/doc/fr_method_index.html +62 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/atom/app.rb +87 -0
- data/lib/atom/collection.rb +75 -0
- data/lib/atom/element.rb +277 -0
- data/lib/atom/entry.rb +135 -0
- data/lib/atom/feed.rb +229 -0
- data/lib/atom/http.rb +132 -0
- data/lib/atom/text.rb +163 -0
- data/lib/atom/xml.rb +200 -0
- data/lib/atom/yaml.rb +101 -0
- data/setup.rb +1585 -0
- data/test/conformance/order.rb +117 -0
- data/test/conformance/title.rb +108 -0
- data/test/conformance/updated.rb +33 -0
- data/test/conformance/xhtmlcontentdiv.rb +18 -0
- data/test/conformance/xmlnamespace.rb +54 -0
- data/test/runtests.rb +14 -0
- data/test/test_constructs.rb +91 -0
- data/test/test_feed.rb +128 -0
- data/test/test_general.rb +99 -0
- data/test/test_http.rb +86 -0
- data/test/test_protocol.rb +69 -0
- data/test/test_xml.rb +353 -0
- metadata +107 -0
data/lib/atom/element.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
require "time"
|
2
|
+
require "rexml/element"
|
3
|
+
|
4
|
+
module Atom # :nodoc:
|
5
|
+
class Time < ::Time # :nodoc:
|
6
|
+
def self.new date
|
7
|
+
return if date.nil?
|
8
|
+
|
9
|
+
date = if date.respond_to?(:iso8601)
|
10
|
+
date
|
11
|
+
else
|
12
|
+
Time.parse date.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def date.to_s
|
16
|
+
iso8601
|
17
|
+
end
|
18
|
+
|
19
|
+
date
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# ignore the man behind the curtain.
|
24
|
+
def self.Multiple klass
|
25
|
+
Class.new(Array) do
|
26
|
+
@class = klass
|
27
|
+
|
28
|
+
def new
|
29
|
+
item = self.class.holds.new
|
30
|
+
self << item
|
31
|
+
|
32
|
+
item
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_element
|
36
|
+
collect do |item| item.to_element end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.holds; @class end
|
40
|
+
def self.single?; true end
|
41
|
+
def taguri; end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Element < Hash
|
46
|
+
# a REXML::Element that shares this element's extension attributes
|
47
|
+
# and child elements
|
48
|
+
attr_reader :extensions
|
49
|
+
|
50
|
+
# this element's xml:base
|
51
|
+
attr_accessor :base
|
52
|
+
|
53
|
+
# The following is a DSL for describing an atom element.
|
54
|
+
|
55
|
+
# this element's attributes
|
56
|
+
def self.attrs # :nodoc:
|
57
|
+
@attrs || []
|
58
|
+
end
|
59
|
+
|
60
|
+
# this element's child elements
|
61
|
+
def self.elements # :nodoc:
|
62
|
+
@elements || []
|
63
|
+
end
|
64
|
+
|
65
|
+
# required child elements
|
66
|
+
def self.required # :nodoc:
|
67
|
+
@elements.find { |name,kind,req| req }
|
68
|
+
end
|
69
|
+
|
70
|
+
# copy defined elements and attributes so inheritance works
|
71
|
+
def self.inherited klass # :nodoc:
|
72
|
+
elements.each do |name, kind, req|
|
73
|
+
klass.element name, kind, req
|
74
|
+
end
|
75
|
+
attrs.each do |name, req|
|
76
|
+
klass.attrb name, req
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# define a child element
|
81
|
+
def self.element(name, kind, req = false) # :nodoc:
|
82
|
+
attr_reader name
|
83
|
+
|
84
|
+
@elements ||= []
|
85
|
+
@elements << [name, kind, req]
|
86
|
+
|
87
|
+
unless kind.respond_to? :single?
|
88
|
+
self.define_accessor(name,kind)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# define an attribute
|
93
|
+
def self.attrb(name, req = false) # :nodoc:
|
94
|
+
@attrs ||= []
|
95
|
+
|
96
|
+
@attrs << [name, req]
|
97
|
+
end
|
98
|
+
|
99
|
+
# a little bit of magic
|
100
|
+
def self.define_accessor(name,kind) # :nodoc:
|
101
|
+
define_method "#{name}=".to_sym do |value|
|
102
|
+
return unless value
|
103
|
+
|
104
|
+
i = if kind.ancestors.member? Atom::Element
|
105
|
+
kind.new(value, name.to_s)
|
106
|
+
else
|
107
|
+
kind.new(value)
|
108
|
+
end
|
109
|
+
|
110
|
+
set(name, i)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# get the value of an attribute
|
115
|
+
def [] key
|
116
|
+
test_key key
|
117
|
+
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
# set the value of an attribute
|
122
|
+
def []= key, value
|
123
|
+
test_key key
|
124
|
+
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
# internal junk you probably don't care about
|
129
|
+
def initialize name = nil # :nodoc:
|
130
|
+
@extensions = REXML::Element.new("extensions")
|
131
|
+
@local_name = name
|
132
|
+
|
133
|
+
self.class.elements.each do |name,kind,req|
|
134
|
+
if kind.respond_to? :single?
|
135
|
+
a = kind.new
|
136
|
+
set(name, kind.new)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# eg. "feed" or "entry" or "updated" or "title" or ...
|
142
|
+
def local_name # :nodoc:
|
143
|
+
@local_name || self.class.name.split("::").last.downcase
|
144
|
+
end
|
145
|
+
|
146
|
+
# convert to a REXML::Element (with no namespace)
|
147
|
+
def to_element
|
148
|
+
elem = REXML::Element.new(local_name)
|
149
|
+
|
150
|
+
self.class.elements.each do |name,kind,req|
|
151
|
+
v = get(name)
|
152
|
+
next if v.nil?
|
153
|
+
|
154
|
+
if v.respond_to? :to_element
|
155
|
+
e = v.to_element
|
156
|
+
e = [ e ] unless e.is_a? Array
|
157
|
+
|
158
|
+
e.each do |bit|
|
159
|
+
elem << bit
|
160
|
+
end
|
161
|
+
else
|
162
|
+
e = REXML::Element.new(name.to_s, elem).text = get(name)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
self.class.attrs.each do |name,req|
|
167
|
+
value = self[name.to_s]
|
168
|
+
elem.attributes[name.to_s] = value if value
|
169
|
+
end
|
170
|
+
|
171
|
+
self.extensions.children.each do |element|
|
172
|
+
elem << element.dup # otherwise they get removed from @extensions
|
173
|
+
end
|
174
|
+
|
175
|
+
if self.base and not self.base.empty?
|
176
|
+
elem.attributes["xml:base"] = self.base
|
177
|
+
end
|
178
|
+
|
179
|
+
elem
|
180
|
+
end
|
181
|
+
|
182
|
+
# convert to a REXML::Document (properly namespaced)
|
183
|
+
def to_xml
|
184
|
+
doc = REXML::Document.new
|
185
|
+
root = to_element
|
186
|
+
root.add_namespace Atom::NS
|
187
|
+
doc << root
|
188
|
+
doc
|
189
|
+
end
|
190
|
+
|
191
|
+
# convert to an XML string
|
192
|
+
def to_s
|
193
|
+
to_xml.to_s
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
# like +valid_key?+ but raises on failure
|
199
|
+
def test_key key
|
200
|
+
unless valid_key? key
|
201
|
+
raise RuntimeError, "this element (#{local_name}) doesn't have that attribute '#{key}'"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# tests that an attribute 'key' has been defined
|
206
|
+
def valid_key? key
|
207
|
+
self.class.attrs.find { |name,req| name.to_s == key }
|
208
|
+
end
|
209
|
+
|
210
|
+
def get name
|
211
|
+
instance_variable_get "@#{name}"
|
212
|
+
end
|
213
|
+
|
214
|
+
def set name, value
|
215
|
+
instance_variable_set "@#{name}", value
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# this facilitates YAML output
|
220
|
+
class AttrEl < Atom::Element # :nodoc:
|
221
|
+
end
|
222
|
+
|
223
|
+
# A link has the following attributes:
|
224
|
+
#
|
225
|
+
# href (required):: the link's IRI
|
226
|
+
# rel:: the relationship of the linked item to the current item
|
227
|
+
# type:: a hint about the media type of the linked item
|
228
|
+
# hreflang:: the language of the linked item (RFC3066)
|
229
|
+
# title:: human-readable information about the link
|
230
|
+
# length:: a hint about the length (in octets) of the linked item
|
231
|
+
class Link < Atom::AttrEl
|
232
|
+
attrb :href, true
|
233
|
+
attrb :rel
|
234
|
+
attrb :type
|
235
|
+
attrb :hreflang
|
236
|
+
attrb :title
|
237
|
+
attrb :length
|
238
|
+
|
239
|
+
def initialize name = nil # :nodoc:
|
240
|
+
super name
|
241
|
+
|
242
|
+
# just setting a default
|
243
|
+
self["rel"] = "alternate"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# A category has the following attributes:
|
248
|
+
#
|
249
|
+
# term (required):: a string that identifies the category
|
250
|
+
# scheme:: an IRI that identifies a categorization scheme
|
251
|
+
# label:: a human-readable label
|
252
|
+
class Category < Atom::AttrEl
|
253
|
+
attrb :term, true
|
254
|
+
attrb :scheme
|
255
|
+
attrb :label
|
256
|
+
end
|
257
|
+
|
258
|
+
# A person construct has the following child elements:
|
259
|
+
#
|
260
|
+
# name (required):: a human-readable name
|
261
|
+
# uri:: an IRI associated with the person
|
262
|
+
# email:: an email address associated with the person
|
263
|
+
class Author < Atom::Element
|
264
|
+
element :name, String, true
|
265
|
+
element :uri, String
|
266
|
+
element :email, String
|
267
|
+
end
|
268
|
+
|
269
|
+
# same as Atom::Author
|
270
|
+
class Contributor < Atom::Element
|
271
|
+
# Author and Contributor should probably inherit from Person, but
|
272
|
+
# oh well.
|
273
|
+
element :name, String, true
|
274
|
+
element :uri, String
|
275
|
+
element :email, String
|
276
|
+
end
|
277
|
+
end
|
data/lib/atom/entry.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
|
3
|
+
require "atom/element"
|
4
|
+
require "atom/text"
|
5
|
+
|
6
|
+
module Atom
|
7
|
+
NS = "http://www.w3.org/2005/Atom"
|
8
|
+
|
9
|
+
# An individual entry in a feed. As an Atom::Element, it can be
|
10
|
+
# manipulated using accessors for each of its child elements. You
|
11
|
+
# should be able to set them using an instance of any class that
|
12
|
+
# makes sense
|
13
|
+
#
|
14
|
+
# Entries have the following children:
|
15
|
+
#
|
16
|
+
# id:: a universally unique IRI which permanently identifies the entry
|
17
|
+
# title:: a human-readable title (Atom::Text)
|
18
|
+
# content:: contains or links to the content of an entry (Atom::Content)
|
19
|
+
# rights:: information about rights held in and over an entry (Atom::Text)
|
20
|
+
# source:: the source feed's metadata (unimplemented)
|
21
|
+
# published:: a Time "early in the life cycle of an entry"
|
22
|
+
# updated:: the most recent Time an entry was modified in a way the publisher considers significant
|
23
|
+
# summary:: a summary, abstract or excerpt of an entry (Atom::Text)
|
24
|
+
#
|
25
|
+
# There are also +categories+, +links+, +authors+ and +contributors+,
|
26
|
+
# each of which is an Array of its respective type and can be used
|
27
|
+
# thusly:
|
28
|
+
#
|
29
|
+
# author = entry.authors.new
|
30
|
+
# author.name = "Captain Kangaroo"
|
31
|
+
class Entry < Atom::Element
|
32
|
+
# the master list of standard children and the types they map to
|
33
|
+
element :id, String, true
|
34
|
+
element :title, Atom::Text, true
|
35
|
+
element :content, Atom::Content, true
|
36
|
+
|
37
|
+
element :rights, Atom::Text
|
38
|
+
# element :source, Atom::Feed # complicated, eg. serialization
|
39
|
+
|
40
|
+
element :authors, Atom::Multiple(Atom::Author)
|
41
|
+
element :contributors, Atom::Multiple(Atom::Contributor)
|
42
|
+
|
43
|
+
element :categories, Atom::Multiple(Atom::Category)
|
44
|
+
element :links, Atom::Multiple(Atom::Link)
|
45
|
+
|
46
|
+
element :published, Atom::Time
|
47
|
+
element :updated, Atom::Time, true
|
48
|
+
|
49
|
+
element :summary, Atom::Text
|
50
|
+
|
51
|
+
def initialize # :nodoc:
|
52
|
+
super "entry"
|
53
|
+
|
54
|
+
# XXX I don't think I've ever actually used this
|
55
|
+
yield self if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
# parses XML fetched from +base+ into an Atom::Entry
|
59
|
+
def self.parse xml, base = ""
|
60
|
+
if xml.respond_to? :to_atom_entry
|
61
|
+
xml.to_atom_entry(base)
|
62
|
+
else
|
63
|
+
REXML::Document.new(xml.to_s).to_atom_entry(base)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspect # :nodoc:
|
68
|
+
"#<Atom::Entry id:'#{self.id}'>"
|
69
|
+
end
|
70
|
+
|
71
|
+
# declare that this entry has updated
|
72
|
+
def updated!
|
73
|
+
self.updated = Time.now
|
74
|
+
end
|
75
|
+
|
76
|
+
# categorize the entry based on a space-separated string
|
77
|
+
def tag_with string
|
78
|
+
return if string.nil?
|
79
|
+
|
80
|
+
string.split.each do |tag|
|
81
|
+
categories.new["term"] = tag
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# XXX this needs a test suite before it can be trusted.
|
86
|
+
=begin
|
87
|
+
# tests the entry's validity
|
88
|
+
def valid?
|
89
|
+
self.class.required.each do |element|
|
90
|
+
unless instance_variable_get "@#{element}"
|
91
|
+
return [ false, "required element atom:#{element} missing" ]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if @authors.length == 0
|
96
|
+
return [ false, "required element atom:author missing" ]
|
97
|
+
end
|
98
|
+
|
99
|
+
alternates = @links.find_all do |link|
|
100
|
+
link["rel"] == "alternate"
|
101
|
+
end
|
102
|
+
|
103
|
+
unless @content or alternates
|
104
|
+
return [ false, "no atom:content or atom:link[rel='alternate']" ]
|
105
|
+
end
|
106
|
+
|
107
|
+
alternates.each do |link|
|
108
|
+
if alternates.find do |x|
|
109
|
+
not x == link and
|
110
|
+
x["type"] == link["type"] and
|
111
|
+
x["hreflang"] == link["hreflang"]
|
112
|
+
end
|
113
|
+
|
114
|
+
return [ false, 'more than one atom:link with a rel attribute value of "alternate" that has the same combination of type and hreflang attribute values.' ]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
type = @content["type"]
|
119
|
+
|
120
|
+
base64ed = (not ["", "text", "html", "xhtml"].member? type) and
|
121
|
+
type.match(/^text\/.*/).nil? and # not text
|
122
|
+
type.match(/.*[\+\/]xml$/).nil? # not XML
|
123
|
+
|
124
|
+
if (@content["src"] or base64ed) and not summary
|
125
|
+
return [ false, "out-of-line or base64ed atom:content and no atom:summary" ]
|
126
|
+
end
|
127
|
+
|
128
|
+
true
|
129
|
+
end
|
130
|
+
=end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# this is here solely so that you don't have to require it
|
135
|
+
require "atom/xml"
|
data/lib/atom/feed.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require "atom/element"
|
2
|
+
require "atom/text"
|
3
|
+
require "atom/entry"
|
4
|
+
|
5
|
+
require "atom/http"
|
6
|
+
|
7
|
+
module Atom
|
8
|
+
class HTTPException < RuntimeError # :nodoc:
|
9
|
+
end
|
10
|
+
class FeedGone < RuntimeError # :nodoc:
|
11
|
+
end
|
12
|
+
|
13
|
+
# A feed of entries. As an Atom::Element, it can be manipulated using
|
14
|
+
# accessors for each of its child elements. You can set them with any
|
15
|
+
# object that makes sense; they will be returned in the types listed.
|
16
|
+
#
|
17
|
+
# Feeds have the following children:
|
18
|
+
#
|
19
|
+
# id:: a universally unique IRI which permanently identifies the feed
|
20
|
+
# title:: a human-readable title (Atom::Text)
|
21
|
+
# subtitle:: a human-readable description or subtitle (Atom::Text)
|
22
|
+
# updated:: the most recent Time the feed was modified in a way the publisher considers significant
|
23
|
+
# generator:: the agent used to generate a feed
|
24
|
+
# icon:: an IRI identifying an icon which visually identifies a feed (1:1 aspect ratio, looks OK small)
|
25
|
+
# logo:: an IRI identifying an image which visually identifies a feed (2:1 aspect ratio)
|
26
|
+
# rights:: rights held in and over a feed (Atom::Text)
|
27
|
+
#
|
28
|
+
# There are also +links+, +categories+, +authors+, +contributors+
|
29
|
+
# and +entries+, each of which is an Array of its respective type and
|
30
|
+
# can be used thusly:
|
31
|
+
#
|
32
|
+
# entry = feed.entries.new
|
33
|
+
# entry.title = "blah blah blah"
|
34
|
+
class Feed < Atom::Element
|
35
|
+
attr_reader :uri
|
36
|
+
|
37
|
+
# the Atom::Feed pointed to by link[@rel='previous']
|
38
|
+
attr_reader :prev
|
39
|
+
# the Atom::Feed pointed to by link[@rel='next']
|
40
|
+
attr_reader :next
|
41
|
+
|
42
|
+
# conditional get information from the last fetch
|
43
|
+
attr_reader :etag, :last_modified
|
44
|
+
|
45
|
+
element :id, String, true
|
46
|
+
element :title, Atom::Text, true
|
47
|
+
element :subtitle, Atom::Text
|
48
|
+
|
49
|
+
element :updated, Atom::Time, true
|
50
|
+
|
51
|
+
element :links, Atom::Multiple(Atom::Link)
|
52
|
+
element :categories, Atom::Multiple(Atom::Category)
|
53
|
+
|
54
|
+
element :authors, Atom::Multiple(Atom::Author)
|
55
|
+
element :contributors, Atom::Multiple(Atom::Contributor)
|
56
|
+
|
57
|
+
element :generator, String # XXX with uri and version attributes!
|
58
|
+
element :icon, String
|
59
|
+
element :logo, String
|
60
|
+
|
61
|
+
element :rights, Atom::Text
|
62
|
+
|
63
|
+
element :entries, Atom::Multiple(Atom::Entry)
|
64
|
+
|
65
|
+
include Enumerable
|
66
|
+
|
67
|
+
def inspect # :nodoc:
|
68
|
+
"<#{@uri} entries: #{entries.length} title='#{title}'>"
|
69
|
+
end
|
70
|
+
|
71
|
+
# parses XML fetched from +base+ into an Atom::Feed
|
72
|
+
def self.parse xml, base = ""
|
73
|
+
if xml.respond_to? :to_atom_entry
|
74
|
+
xml.to_atom_feed(base)
|
75
|
+
else
|
76
|
+
REXML::Document.new(xml.to_s).to_atom_feed(base)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Create a new Feed that can be found at feed_uri and retrieved
|
81
|
+
# using an Atom::HTTP object http
|
82
|
+
def initialize feed_uri = nil, http = Atom::HTTP.new
|
83
|
+
@entries = []
|
84
|
+
@http = http
|
85
|
+
|
86
|
+
if feed_uri
|
87
|
+
@uri = feed_uri.to_uri
|
88
|
+
self.base = feed_uri
|
89
|
+
end
|
90
|
+
|
91
|
+
super "feed"
|
92
|
+
end
|
93
|
+
|
94
|
+
# iterates over a feed's entries
|
95
|
+
def each &block
|
96
|
+
@entries.each &block
|
97
|
+
end
|
98
|
+
|
99
|
+
# gets everything in the logical feed (could be a lot of stuff)
|
100
|
+
# (see <http://www.ietf.org/internet-drafts/draft-nottingham-atompub-feed-history-05.txt>)
|
101
|
+
def get_everything!
|
102
|
+
self.update!
|
103
|
+
|
104
|
+
prev = @prev
|
105
|
+
while prev
|
106
|
+
prev.update!
|
107
|
+
|
108
|
+
self.merge_entries! prev
|
109
|
+
prev = prev.prev
|
110
|
+
end
|
111
|
+
|
112
|
+
nxt = @next
|
113
|
+
while nxt
|
114
|
+
nxt.update!
|
115
|
+
|
116
|
+
self.merge_entries! nxt
|
117
|
+
nxt = nxt.next
|
118
|
+
end
|
119
|
+
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
# merges the entries from another feed into this one
|
124
|
+
def merge_entries! other_feed
|
125
|
+
other_feed.each do |entry|
|
126
|
+
# TODO: add atom:source elements
|
127
|
+
self << entry
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# like #merge, but in place
|
132
|
+
def merge! other_feed
|
133
|
+
[:id, :title, :subtitle, :updated, :rights].each { |p|
|
134
|
+
self.send("#{p}=", other_feed.send("#{p}"))
|
135
|
+
}
|
136
|
+
|
137
|
+
[:links, :categories, :authors, :contributors].each do |p|
|
138
|
+
other_feed.send("#{p}").each do |e|
|
139
|
+
self.send("#{p}") << e
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
merge_entries! other_feed
|
144
|
+
end
|
145
|
+
|
146
|
+
# merges "important" properties of this feed with another one,
|
147
|
+
# returning a new feed
|
148
|
+
def merge other_feed
|
149
|
+
feed = self.clone
|
150
|
+
|
151
|
+
feed.merge! other_feed
|
152
|
+
|
153
|
+
feed
|
154
|
+
end
|
155
|
+
|
156
|
+
# fetches this feed's URL, parses the result and #merge!s
|
157
|
+
# changes, new entries, &c.
|
158
|
+
def update!
|
159
|
+
raise(RuntimeError, "can't fetch without a uri.") unless @uri
|
160
|
+
|
161
|
+
headers = {}
|
162
|
+
headers["If-None-Match"] = @etag if @etag
|
163
|
+
headers["If-Modified-Since"] = @last_modified if @last_modified
|
164
|
+
|
165
|
+
res = @http.get(@uri, headers)
|
166
|
+
|
167
|
+
if res.code == "304"
|
168
|
+
# we're already all up to date
|
169
|
+
return self
|
170
|
+
elsif res.code == "410"
|
171
|
+
raise Atom::FeedGone, "410 Gone (#{@uri})"
|
172
|
+
elsif res.code != "200"
|
173
|
+
raise Atom::HTTPException, "Unexpected HTTP response code: #{res.code}"
|
174
|
+
end
|
175
|
+
|
176
|
+
unless res.content_type.match(/^application\/atom\+xml/)
|
177
|
+
raise Atom::HTTPException, "Unexpected HTTP response Content-Type: #{res.content_type} (wanted application/atom+xml)"
|
178
|
+
end
|
179
|
+
|
180
|
+
@etag = res["Etag"] if res["Etag"]
|
181
|
+
@last_modified = res["Last-Modified"] if res["Last-Modified"]
|
182
|
+
|
183
|
+
xml = res.body
|
184
|
+
|
185
|
+
coll = REXML::Document.new(xml)
|
186
|
+
|
187
|
+
update_time = Time.parse(REXML::XPath.first(coll, "/atom:feed/atom:updated", { "atom" => Atom::NS } ).text)
|
188
|
+
|
189
|
+
# the feed hasn't been updated, don't bother
|
190
|
+
if self.updated and self.updated >= update_time
|
191
|
+
return self
|
192
|
+
end
|
193
|
+
|
194
|
+
coll = Atom::Feed.parse(coll, self.base.to_s)
|
195
|
+
merge! coll
|
196
|
+
|
197
|
+
link = coll.links.find { |l| l["rel"] = "next" and l["type"] == "application/atom+xml" }
|
198
|
+
if link
|
199
|
+
abs_uri = @uri + link["href"]
|
200
|
+
@next = Feed.new(abs_uri.to_s, @http)
|
201
|
+
end
|
202
|
+
|
203
|
+
link = coll.links.find { |l| l["rel"] = "previous" and l["type"] == "application/atom+xml" }
|
204
|
+
if link
|
205
|
+
abs_uri = @uri + link["href"]
|
206
|
+
@prev = Feed.new(abs_uri.to_s, @http)
|
207
|
+
end
|
208
|
+
|
209
|
+
self
|
210
|
+
end
|
211
|
+
|
212
|
+
# adds an entry to this feed. if this feed already contains an
|
213
|
+
# entry with the same id, the newest one is used.
|
214
|
+
def << entry
|
215
|
+
existing = entries.find do |e|
|
216
|
+
e.id == entry.id
|
217
|
+
end
|
218
|
+
|
219
|
+
if not existing
|
220
|
+
@entries << entry
|
221
|
+
elsif not existing.updated or (existing.updated and entry.updated and entry.updated >= existing.updated)
|
222
|
+
@entries[@entries.index(existing)] = entry
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# this is here solely so you don't have to require it
|
229
|
+
require "atom/xml"
|