atom-tools 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"