atom-tools 1.0.0 → 2.0.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/lib/atom/entry.rb CHANGED
@@ -4,8 +4,61 @@ require "atom/element"
4
4
  require "atom/text"
5
5
 
6
6
  module Atom
7
- NS = "http://www.w3.org/2005/Atom"
8
- PP_NS = "http://www.w3.org/2007/app"
7
+ class Control < Atom::Element
8
+ attr_accessor :draft
9
+
10
+ is_element PP_NS, :control
11
+
12
+ on_parse [PP_NS, 'draft'] do |e,x|
13
+ e.set(:draft, x.text == 'yes')
14
+ end
15
+
16
+ on_build do |e,x|
17
+ unless (v = e.get(:draft)).nil?
18
+ el = e.append_elem(x, ['app', PP_NS], 'draft')
19
+ el.text = (v ? 'yes' : 'no')
20
+ end
21
+ end
22
+ end
23
+
24
+ module HasCategories
25
+ def HasCategories.included(klass)
26
+ klass.atom_elements :category, :categories, Atom::Category
27
+ end
28
+
29
+ # categorize the entry with each of an array or a space-separated
30
+ # string
31
+ def tag_with(tags, delimiter = ' ')
32
+ return if not tags or tags.empty?
33
+
34
+ tag_list = unless tags.is_a?(String)
35
+ tags
36
+ else
37
+ tags = tags.split(delimiter)
38
+ tags.map! { |t| t.strip }
39
+ tags.reject! { |t| t.empty? }
40
+ tags.uniq
41
+ end
42
+
43
+ tag_list.each do |tag|
44
+ unless categories.any? { |c| c.term == tag }
45
+ categories.new :term => tag
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ module HasLinks
52
+ def HasLinks.included(klass)
53
+ klass.atom_elements :link, :links, Atom::Link
54
+ end
55
+
56
+ def find_link(criteria)
57
+ self.links.find do |l|
58
+ criteria.all? { |k,v| l.send(k) == v }
59
+ end
60
+ end
61
+ end
9
62
 
10
63
  # An individual entry in a feed. As an Atom::Element, it can be
11
64
  # manipulated using accessors for each of its child elements. You
@@ -27,168 +80,55 @@ module Atom
27
80
  # each of which is an Array of its respective type and can be used
28
81
  # thusly:
29
82
  #
30
- # author = entry.authors.new
31
- # author.name = "Captain Kangaroo"
83
+ # author = entry.authors.new :name => "Captain Kangaroo", :email => "kanga@example.net"
84
+ #
32
85
  class Entry < Atom::Element
86
+ is_atom_element :entry
87
+
33
88
  # the master list of standard children and the types they map to
34
- element :id, String, true
35
- element :title, Atom::Text, true
36
- element :content, Atom::Content, true
89
+ atom_string :id
37
90
 
38
- element :rights, Atom::Text
39
- # element :source, Atom::Feed # complicated, eg. serialization
91
+ atom_element :title, Atom::Title
92
+ atom_element :summary, Atom::Summary
93
+ atom_element :content, Atom::Content
40
94
 
41
- element :authors, Atom::Multiple(Atom::Author)
42
- element :contributors, Atom::Multiple(Atom::Contributor)
95
+ atom_element :rights, Atom::Rights
43
96
 
44
- element :categories, Atom::Multiple(Atom::Category)
45
- element :links, Atom::Multiple(Atom::Link)
97
+ # element :source, Atom::Feed # XXX complicated, eg. serialization
46
98
 
47
- element :published, Atom::Time
48
- element :updated, Atom::Time, true
99
+ atom_time :published
100
+ atom_time :updated
101
+ time ['app', PP_NS], :edited
49
102
 
50
- element :summary, Atom::Text
103
+ atom_elements :author, :authors, Atom::Author
104
+ atom_elements :contributor, :contributors, Atom::Contributor
51
105
 
52
- def initialize # :nodoc:
53
- super "entry"
106
+ element ['app', PP_NS], :control, Atom::Control
54
107
 
55
- # XXX I don't think I've ever actually used this
56
- yield self if block_given?
57
- end
108
+ include HasCategories
109
+ include HasLinks
58
110
 
59
- # parses XML into an Atom::Entry
60
- #
61
- # +base+ is the absolute URI the document was fetched from
62
- # (if there is one)
63
- def self.parse xml, base = ""
64
- if xml.respond_to? :to_atom_entry
65
- xml.to_atom_entry(base)
66
- elsif xml.respond_to? :read
67
- self.parse(xml.read)
68
- else
69
- begin
70
- REXML::Document.new(xml.to_s).to_atom_entry(base)
71
- rescue REXML::ParseException
72
- raise Atom::ParseError
73
- end
74
- end
75
- end
111
+ atom_link :edit_url, :rel => 'edit'
76
112
 
77
113
  def inspect # :nodoc:
78
114
  "#<Atom::Entry id:'#{self.id}'>"
79
115
  end
80
116
 
81
- # declare that this entry has updated.
82
- #
83
- # (note that this is different from Atom::Feed#update!)
84
- def updated!
85
- self.updated = Time.now
86
- end
87
-
88
- # categorize the entry with each of an array or a space-separated
89
- # string
90
- def tag_with tags
91
- return unless tags
92
-
93
- (tags.is_a?(String) ? tags.split : tags).each do |tag|
94
- categories.new["term"] = tag
95
- end
96
- end
97
-
98
- # the @href of an entry's link[@rel="edit"]
99
- def edit_url
100
- begin
101
- edit_link = self.links.find do |link|
102
- link["rel"] == "edit"
103
- end
104
-
105
- edit_link["href"]
106
- rescue
107
- nil
108
- end
109
- end
110
-
111
117
  def draft
112
- elem = REXML::XPath.first(extensions, "app:control/app:draft", {"app" => PP_NS})
113
-
114
- elem and elem.text == "yes"
118
+ control and control.draft
115
119
  end
116
120
 
117
- def draft= is_draft
118
- nses = {"app" => PP_NS}
119
- draft_e = REXML::XPath.first(extensions, "app:control/app:draft", nses)
120
- control_e = REXML::XPath.first(extensions, "app:control", nses)
121
+ alias :draft? :draft
121
122
 
122
- if is_draft and not draft
123
- unless draft_e
124
- unless control_e
125
- control_e = REXML::Element.new("control")
126
- control_e.add_namespace PP_NS
127
-
128
- extensions << control_e
129
- end
130
-
131
- draft_e = REXML::Element.new("draft")
132
- control_e << draft_e
133
- end
134
-
135
- draft_e.text = "yes"
136
- elsif not is_draft and draft
137
- draft_e.remove
138
- control_e.remove if control_e.elements.empty?
139
- end
140
-
141
- is_draft
123
+ def draft!
124
+ self.draft = true
142
125
  end
143
126
 
144
- # XXX this needs a test suite before it can be trusted.
145
- =begin
146
- # tests the entry's validity
147
- def valid?
148
- self.class.required.each do |element|
149
- unless instance_variable_get "@#{element}"
150
- return [ false, "required element atom:#{element} missing" ]
151
- end
152
- end
153
-
154
- if @authors.length == 0
155
- return [ false, "required element atom:author missing" ]
156
- end
157
-
158
- alternates = @links.find_all do |link|
159
- link["rel"] == "alternate"
160
- end
161
-
162
- unless @content or alternates
163
- return [ false, "no atom:content or atom:link[rel='alternate']" ]
164
- end
165
-
166
- alternates.each do |link|
167
- if alternates.find do |x|
168
- not x == link and
169
- x["type"] == link["type"] and
170
- x["hreflang"] == link["hreflang"]
171
- end
172
-
173
- 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.' ]
174
- end
175
- end
176
-
177
- type = @content["type"]
178
-
179
- base64ed = (not ["", "text", "html", "xhtml"].member? type) and
180
- type.match(/^text\/.*/).nil? and # not text
181
- type.match(/.*[\+\/]xml$/).nil? # not XML
182
-
183
- if (@content["src"] or base64ed) and not summary
184
- return [ false, "out-of-line or base64ed atom:content and no atom:summary" ]
127
+ def draft= is_draft
128
+ unless control
129
+ instance_variable_set '@control', Atom::Control.new
185
130
  end
186
-
187
- true
131
+ control.draft = is_draft
188
132
  end
189
- =end
190
133
  end
191
134
  end
192
-
193
- # this is here solely so that you don't have to require it
194
- require "atom/xml"
data/lib/atom/feed.rb CHANGED
@@ -21,44 +21,47 @@ module Atom
21
21
  # generator:: the agent used to generate a feed
22
22
  # icon:: an IRI identifying an icon which visually identifies a feed (1:1 aspect ratio, looks OK small)
23
23
  # logo:: an IRI identifying an image which visually identifies a feed (2:1 aspect ratio)
24
- # rights:: rights held in and over a feed (Atom::Text)
24
+ # rights:: rights held in and over a feed (Atom::Text)
25
25
  #
26
- # There are also +links+, +categories+, +authors+, +contributors+
26
+ # There are also +links+, +categories+, +authors+, +contributors+
27
27
  # and +entries+, each of which is an Array of its respective type and
28
28
  # can be used thusly:
29
29
  #
30
30
  # entry = feed.entries.new
31
31
  # entry.title = "blah blah blah"
32
+ #
32
33
  class Feed < Atom::Element
34
+ is_atom_element :feed
35
+
33
36
  attr_reader :uri
34
37
 
35
38
  # the Atom::Feed pointed to by link[@rel='previous']
36
39
  attr_reader :prev
37
40
  # the Atom::Feed pointed to by link[@rel='next']
38
41
  attr_reader :next
39
-
42
+
40
43
  # conditional get information from the last fetch
41
44
  attr_reader :etag, :last_modified
42
45
 
43
- element :id, String, true
44
- element :title, Atom::Text, true
45
- element :subtitle, Atom::Text
46
-
47
- element :updated, Atom::Time, true
46
+ atom_string :id
47
+ atom_element :title, Atom::Title
48
+ atom_element :subtitle, Atom::Subtitle
49
+
50
+ atom_time :updated
51
+
52
+ include HasLinks
53
+ include HasCategories
48
54
 
49
- element :links, Atom::Multiple(Atom::Link)
50
- element :categories, Atom::Multiple(Atom::Category)
55
+ atom_elements :author, :authors, Atom::Author
56
+ atom_elements :contributor, :contributors, Atom::Contributor
51
57
 
52
- element :authors, Atom::Multiple(Atom::Author)
53
- element :contributors, Atom::Multiple(Atom::Contributor)
58
+ atom_string :generator # XXX with uri and version attributes!
59
+ atom_string :icon
60
+ atom_string :logo
54
61
 
55
- element :generator, String # XXX with uri and version attributes!
56
- element :icon, String
57
- element :logo, String
62
+ atom_element :rights, Atom::Rights
58
63
 
59
- element :rights, Atom::Text
60
-
61
- element :entries, Atom::Multiple(Atom::Entry)
64
+ atom_elements :entry, :entries, Atom::Entry
62
65
 
63
66
  include Enumerable
64
67
 
@@ -66,21 +69,6 @@ module Atom
66
69
  "<#{@uri} entries: #{entries.length} title='#{title}'>"
67
70
  end
68
71
 
69
- # parses XML fetched from +base+ into an Atom::Feed
70
- def self.parse xml, base = ""
71
- if xml.respond_to? :to_atom_entry
72
- xml.to_atom_feed(base)
73
- elsif xml.respond_to? :read
74
- self.parse(xml.read)
75
- else
76
- begin
77
- REXML::Document.new(xml.to_s).to_atom_feed(base)
78
- rescue REXML::ParseException
79
- raise Atom::ParseError
80
- end
81
- end
82
- end
83
-
84
72
  # Create a new Feed that can be found at feed_uri and retrieved
85
73
  # using an Atom::HTTP object http
86
74
  def initialize feed_uri = nil, http = Atom::HTTP.new
@@ -92,7 +80,7 @@ module Atom
92
80
  self.base = feed_uri
93
81
  end
94
82
 
95
- super "feed"
83
+ super()
96
84
  end
97
85
 
98
86
  # iterates over a feed's entries
@@ -108,7 +96,7 @@ module Atom
108
96
  # (see <http://www.ietf.org/internet-drafts/draft-nottingham-atompub-feed-history-05.txt>)
109
97
  def get_everything!
110
98
  self.update!
111
-
99
+
112
100
  prev = @prev
113
101
  while prev
114
102
  prev.update!
@@ -136,18 +124,22 @@ module Atom
136
124
  end
137
125
  end
138
126
 
139
- # like #merge, but in place
127
+ # like #merge, but in place
140
128
  def merge! other_feed
141
- [:id, :title, :subtitle, :updated, :rights, :logo, :icon].each { |p|
142
- self.send("#{p}=", other_feed.send("#{p}"))
143
- }
129
+ [:id, :title, :subtitle, :updated, :rights, :logo, :icon].each do |p|
130
+ if (v = other_feed.get(p))
131
+ set p, v
132
+ end
133
+ end
144
134
 
145
135
  [:links, :categories, :authors, :contributors].each do |p|
146
- other_feed.send("#{p}").each do |e|
147
- self.send("#{p}") << e
136
+ other_feed.get(p).each do |e|
137
+ get(p) << e
148
138
  end
149
139
  end
150
140
 
141
+ @extensions = other_feed.extensions
142
+
151
143
  merge_entries! other_feed
152
144
  end
153
145
 
@@ -157,7 +149,7 @@ module Atom
157
149
  feed = self.clone
158
150
 
159
151
  feed.merge! other_feed
160
-
152
+
161
153
  feed
162
154
  end
163
155
 
@@ -167,15 +159,10 @@ module Atom
167
159
  # (note that this is different from Atom::Entry#updated!
168
160
  def update!
169
161
  raise(RuntimeError, "can't fetch without a uri.") unless @uri
170
-
171
- headers = {}
172
- headers["Accept"] = "application/atom+xml"
173
- headers["If-None-Match"] = @etag if @etag
174
- headers["If-Modified-Since"] = @last_modified if @last_modified
175
162
 
176
- res = @http.get(@uri, headers)
163
+ res = @http.get(@uri, "Accept" => "application/atom+xml")
177
164
 
178
- if res.code == "304"
165
+ if @etag and res['etag'] == @etag
179
166
  # we're already all up to date
180
167
  return self
181
168
  elsif res.code == "410"
@@ -183,14 +170,13 @@ module Atom
183
170
  elsif res.code != "200"
184
171
  raise Atom::HTTPException, "Unexpected HTTP response code: #{res.code}"
185
172
  end
186
-
173
+
187
174
  # we'll be forgiving about feed content types.
188
- res.validate_content_type(["application/atom+xml",
189
- "application/xml",
175
+ res.validate_content_type(["application/atom+xml",
176
+ "application/xml",
190
177
  "text/xml"])
191
178
 
192
179
  @etag = res["ETag"] if res["ETag"]
193
- @last_modified = res["Last-Modified"] if res["Last-Modified"]
194
180
 
195
181
  xml = res.body
196
182
 
@@ -203,25 +189,24 @@ module Atom
203
189
  return self
204
190
  end
205
191
 
206
- coll = Atom::Feed.parse(coll, self.base.to_s)
192
+ coll = self.class.parse(coll.root, self.base.to_s)
207
193
  merge! coll
208
194
 
209
- link = coll.links.find { |l| l["rel"] == "next" and l["type"] == "application/atom+xml" }
210
- if link
211
- abs_uri = @uri + link["href"]
212
- @next = Feed.new(abs_uri.to_s, @http)
195
+ if abs_uri = next_link
196
+ @next = self.class.new(abs_uri.to_s, @http)
213
197
  end
214
198
 
215
- link = coll.links.find { |l| l["rel"] == "previous" and l["type"] == "application/atom+xml" }
216
- if link
217
- abs_uri = @uri + link["href"]
218
- @prev = Feed.new(abs_uri.to_s, @http)
199
+ if abs_uri = previous_link
200
+ @prev = self.class.new(abs_uri.to_s, @http)
219
201
  end
220
202
 
221
203
  self
222
204
  end
223
205
 
224
- # adds an entry to this feed. if this feed already contains an
206
+ atom_link :previous_link, :rel => 'previous'
207
+ atom_link :next_link, :rel => 'next'
208
+
209
+ # adds an entry to this feed. if this feed already contains an
225
210
  # entry with the same id, the newest one is used.
226
211
  def << entry
227
212
  existing = entries.find do |e|
@@ -236,6 +221,3 @@ module Atom
236
221
  end
237
222
  end
238
223
  end
239
-
240
- # this is here solely so you don't have to require it
241
- require "atom/xml"