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/COPYING +3 -3
- data/README +4 -44
- data/Rakefile +9 -2
- data/bin/atom-cp +159 -0
- data/bin/atom-grep +78 -0
- data/bin/atom-post +72 -0
- data/bin/atom-purge +82 -0
- data/lib/atom/cache.rb +178 -0
- data/lib/atom/collection.rb +77 -17
- data/lib/atom/element.rb +520 -166
- data/lib/atom/entry.rb +82 -142
- data/lib/atom/feed.rb +48 -66
- data/lib/atom/http.rb +115 -35
- data/lib/atom/service.rb +56 -113
- data/lib/atom/text.rb +79 -63
- data/lib/atom/tools.rb +163 -0
- data/test/conformance/order.rb +11 -10
- data/test/conformance/title.rb +9 -9
- data/test/test_constructs.rb +23 -10
- data/test/test_feed.rb +0 -44
- data/test/test_general.rb +0 -40
- data/test/test_http.rb +18 -0
- data/test/test_protocol.rb +60 -22
- data/test/test_xml.rb +73 -41
- metadata +47 -37
- data/bin/atom-client.rb +0 -275
- data/lib/atom/xml.rb +0 -213
- data/lib/atom/yaml.rb +0 -116
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
|
-
|
8
|
-
|
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
|
-
#
|
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
|
-
|
35
|
-
element :title, Atom::Text, true
|
36
|
-
element :content, Atom::Content, true
|
89
|
+
atom_string :id
|
37
90
|
|
38
|
-
|
39
|
-
|
91
|
+
atom_element :title, Atom::Title
|
92
|
+
atom_element :summary, Atom::Summary
|
93
|
+
atom_element :content, Atom::Content
|
40
94
|
|
41
|
-
|
42
|
-
element :contributors, Atom::Multiple(Atom::Contributor)
|
95
|
+
atom_element :rights, Atom::Rights
|
43
96
|
|
44
|
-
element :
|
45
|
-
element :links, Atom::Multiple(Atom::Link)
|
97
|
+
# element :source, Atom::Feed # XXX complicated, eg. serialization
|
46
98
|
|
47
|
-
|
48
|
-
|
99
|
+
atom_time :published
|
100
|
+
atom_time :updated
|
101
|
+
time ['app', PP_NS], :edited
|
49
102
|
|
50
|
-
|
103
|
+
atom_elements :author, :authors, Atom::Author
|
104
|
+
atom_elements :contributor, :contributors, Atom::Contributor
|
51
105
|
|
52
|
-
|
53
|
-
super "entry"
|
106
|
+
element ['app', PP_NS], :control, Atom::Control
|
54
107
|
|
55
|
-
|
56
|
-
|
57
|
-
end
|
108
|
+
include HasCategories
|
109
|
+
include HasLinks
|
58
110
|
|
59
|
-
|
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
|
-
|
113
|
-
|
114
|
-
elem and elem.text == "yes"
|
118
|
+
control and control.draft
|
115
119
|
end
|
116
120
|
|
117
|
-
|
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
|
-
|
123
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
55
|
+
atom_elements :author, :authors, Atom::Author
|
56
|
+
atom_elements :contributor, :contributors, Atom::Contributor
|
51
57
|
|
52
|
-
|
53
|
-
|
58
|
+
atom_string :generator # XXX with uri and version attributes!
|
59
|
+
atom_string :icon
|
60
|
+
atom_string :logo
|
54
61
|
|
55
|
-
|
56
|
-
element :icon, String
|
57
|
-
element :logo, String
|
62
|
+
atom_element :rights, Atom::Rights
|
58
63
|
|
59
|
-
|
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
|
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
|
142
|
-
|
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.
|
147
|
-
|
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,
|
163
|
+
res = @http.get(@uri, "Accept" => "application/atom+xml")
|
177
164
|
|
178
|
-
if res
|
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 =
|
192
|
+
coll = self.class.parse(coll.root, self.base.to_s)
|
207
193
|
merge! coll
|
208
194
|
|
209
|
-
|
210
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
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"
|