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/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"
|