atom-tools 0.9.2 → 0.9.3
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/README +2 -2
- data/Rakefile +1 -1
- data/bin/atom-client.rb +71 -45
- data/lib/atom/element.rb +7 -2
- data/lib/atom/entry.rb +51 -16
- data/lib/atom/feed.rb +6 -6
- data/lib/atom/http.rb +23 -19
- data/lib/atom/service.rb +0 -2
- data/lib/atom/text.rb +34 -12
- data/lib/atom/xml.rb +26 -17
- data/lib/atom/yaml.rb +23 -8
- data/test/test_constructs.rb +16 -2
- data/test/test_general.rb +22 -9
- data/test/test_http.rb +12 -8
- data/test/test_xml.rb +66 -20
- metadata +4 -40
- data/doc/classes/Atom/Author.html +0 -130
- data/doc/classes/Atom/Category.html +0 -128
- data/doc/classes/Atom/Collection.html +0 -322
- data/doc/classes/Atom/Content.html +0 -129
- data/doc/classes/Atom/Contributor.html +0 -119
- data/doc/classes/Atom/DigestAuth.html +0 -285
- data/doc/classes/Atom/Element.html +0 -325
- data/doc/classes/Atom/Entry.html +0 -369
- data/doc/classes/Atom/Feed.html +0 -595
- data/doc/classes/Atom/HTTP.html +0 -436
- data/doc/classes/Atom/HTTPResponse.html +0 -149
- data/doc/classes/Atom/Link.html +0 -137
- data/doc/classes/Atom/Service.html +0 -260
- data/doc/classes/Atom/Text.html +0 -245
- data/doc/classes/Atom/Workspace.html +0 -121
- data/doc/classes/XHTML.html +0 -118
- data/doc/created.rid +0 -1
- data/doc/files/README.html +0 -213
- data/doc/files/lib/atom/collection_rb.html +0 -110
- data/doc/files/lib/atom/element_rb.html +0 -109
- data/doc/files/lib/atom/entry_rb.html +0 -111
- data/doc/files/lib/atom/feed_rb.html +0 -112
- data/doc/files/lib/atom/http_rb.html +0 -112
- data/doc/files/lib/atom/service_rb.html +0 -111
- data/doc/files/lib/atom/text_rb.html +0 -109
- data/doc/files/lib/atom/xml_rb.html +0 -110
- data/doc/files/lib/atom/yaml_rb.html +0 -109
- data/doc/fr_class_index.html +0 -42
- data/doc/fr_file_index.html +0 -36
- data/doc/fr_method_index.html +0 -69
- data/doc/index.html +0 -24
- data/doc/rdoc-style.css +0 -208
data/README
CHANGED
@@ -10,7 +10,7 @@ It handles all the nasty XML and HTTP details so that you don't have to.
|
|
10
10
|
|
11
11
|
feed = Atom::Feed.new "http://www.tbray.org/ongoing/ongoing.atom"
|
12
12
|
# => <http://www.tbray.org/ongoing/ongoing.atom entries: 0 title=''>
|
13
|
-
|
13
|
+
|
14
14
|
feed.update!
|
15
15
|
feed.entries.length
|
16
16
|
# => 20
|
@@ -29,7 +29,7 @@ It handles all the nasty XML and HTTP details so that you don't have to.
|
|
29
29
|
|
30
30
|
entry.links.last
|
31
31
|
# => {"href"=>"http://www.tbray.org/ongoing/When/200x/2006/11/07/Munich#comments", "rel"=>"replies", "type" => "application/xhtml+xml"}
|
32
|
-
|
32
|
+
|
33
33
|
entry.summary.to_s
|
34
34
|
# => "That local spelling is nicer than the ugly English [...]"
|
35
35
|
|
data/Rakefile
CHANGED
data/bin/atom-client.rb
CHANGED
@@ -1,7 +1,29 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
options = {}
|
6
|
+
|
7
|
+
OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: #{$0} [options]"
|
9
|
+
|
10
|
+
opts.on("-c", "--coll-url [URL]", "URL of the collection you would like to manipulate") do |url|
|
11
|
+
options[:url] = url
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on("-u", "--user [USERNAME]", "Username to authenticate with") do |user|
|
15
|
+
options[:user] = user
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-p", "--password [PASSWORD]", "Password to authenticate with") do |pass|
|
19
|
+
options[:pass] = pass
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
23
|
+
puts opts
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
end.parse!
|
5
27
|
|
6
28
|
require "tempfile"
|
7
29
|
|
@@ -57,21 +79,20 @@ class Atom::Entry
|
|
57
79
|
|
58
80
|
def edit
|
59
81
|
yaml = YAML.load(self.to_yaml)
|
60
|
-
|
61
|
-
#
|
82
|
+
|
83
|
+
# human readability
|
62
84
|
yaml.delete "id"
|
63
85
|
|
64
86
|
if yaml["links"]
|
65
|
-
yaml["links"].
|
66
|
-
yaml["links"].delete(yaml["links"].find { |l| l["rel"] == "alternate" })
|
87
|
+
yaml["links"].find_all { |l| l["rel"] == "alternate" or l["rel"] == "edit" }.each { |l| yaml["links"].delete(l) }
|
67
88
|
yaml.delete("links") if yaml["links"].empty?
|
68
89
|
end
|
69
|
-
|
70
|
-
entry = write_entry(yaml.to_yaml)
|
90
|
+
|
91
|
+
new_yaml, entry = write_entry(yaml.to_yaml)
|
71
92
|
|
72
93
|
entry.id = self.id
|
73
94
|
|
74
|
-
entry
|
95
|
+
[new_yaml["slug"], entry]
|
75
96
|
end
|
76
97
|
end
|
77
98
|
|
@@ -95,24 +116,6 @@ def choose_from list
|
|
95
116
|
item
|
96
117
|
end
|
97
118
|
|
98
|
-
def choose_collection server
|
99
|
-
puts "which collection?"
|
100
|
-
|
101
|
-
collections = []
|
102
|
-
|
103
|
-
# flatten it out into one big workspace
|
104
|
-
server.workspaces.each do |ws|
|
105
|
-
puts ws.title.to_s + ":"
|
106
|
-
ws.collections.each_with_index do |coll, index|
|
107
|
-
collections << coll
|
108
|
-
|
109
|
-
puts "#{index}: #{coll.title}"
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
choose_from collections
|
114
|
-
end
|
115
|
-
|
116
119
|
def choose_entry_url coll
|
117
120
|
puts "which entry?"
|
118
121
|
|
@@ -134,11 +137,13 @@ def write_entry(editstring = "")
|
|
134
137
|
edited = editstring.edit_externally
|
135
138
|
|
136
139
|
if edited == editstring
|
137
|
-
puts "
|
140
|
+
puts "You didn't edit anything, aborting."
|
138
141
|
exit
|
139
142
|
end
|
140
143
|
|
141
|
-
|
144
|
+
yaml = YAML.load(edited)
|
145
|
+
|
146
|
+
entry = Atom::Entry.from_yaml yaml
|
142
147
|
|
143
148
|
entry.prepare_for_output
|
144
149
|
|
@@ -167,29 +172,46 @@ def write_entry(editstring = "")
|
|
167
172
|
retry
|
168
173
|
end
|
169
174
|
|
170
|
-
entry
|
175
|
+
[yaml, entry]
|
171
176
|
end
|
172
177
|
|
173
178
|
module Atom
|
174
|
-
class InvalidEntry < RuntimeError
|
175
|
-
end
|
179
|
+
class InvalidEntry < RuntimeError; end
|
176
180
|
end
|
177
181
|
|
178
182
|
EDITOR = ENV["EDITOR"] || "env vim"
|
179
183
|
|
180
|
-
# now that i'm supporting -07 the interface has been shittified. apologies.
|
181
|
-
introspection_url = ARGV[0]
|
182
|
-
|
183
184
|
http = Atom::HTTP.new
|
184
|
-
http.user = ARGV[1]
|
185
|
-
http.pass = ARGV[2]
|
186
185
|
|
187
|
-
|
186
|
+
url = if options[:url]
|
187
|
+
options[:url]
|
188
|
+
else
|
189
|
+
yaml = YAML.load(File.read("#{ENV["HOME"]}/.atom-client"))
|
190
|
+
collections = yaml["collections"]
|
188
191
|
|
189
|
-
|
192
|
+
puts "which collection?"
|
193
|
+
|
194
|
+
collections.keys.each_with_index do |name,index|
|
195
|
+
puts "#{index}: #{name}"
|
196
|
+
end
|
190
197
|
|
191
|
-
|
192
|
-
|
198
|
+
tmp = choose_from collections.values
|
199
|
+
|
200
|
+
http.user = tmp["user"] if tmp["user"]
|
201
|
+
http.pass = tmp["pass"] if tmp["pass"]
|
202
|
+
|
203
|
+
tmp["url"]
|
204
|
+
end
|
205
|
+
|
206
|
+
http.user = options[:user] if options[:user]
|
207
|
+
http.pass = options[:pass] if options[:pass]
|
208
|
+
|
209
|
+
# this is where all the Atom stuff starts
|
210
|
+
|
211
|
+
coll = Atom::Collection.new(url, http)
|
212
|
+
|
213
|
+
# XXX generate a real id
|
214
|
+
CLIENT_ID = "http://necronomicorp.com/nil"
|
193
215
|
|
194
216
|
new = lambda do
|
195
217
|
entry = Atom::Entry.new
|
@@ -197,12 +219,12 @@ new = lambda do
|
|
197
219
|
entry.title = ""
|
198
220
|
entry.content = ""
|
199
221
|
|
200
|
-
entry = entry.edit
|
222
|
+
slug, entry = entry.edit
|
201
223
|
|
202
224
|
entry.id = CLIENT_ID
|
203
225
|
entry.published = Time.now.iso8601
|
204
226
|
|
205
|
-
res = coll.post! entry
|
227
|
+
res = coll.post! entry, slug
|
206
228
|
|
207
229
|
# XXX error recovery here, lost updates suck
|
208
230
|
puts res.body
|
@@ -219,9 +241,13 @@ edit = lambda do
|
|
219
241
|
|
220
242
|
url = entry.edit_url
|
221
243
|
|
244
|
+
raise "this entry has no edit link" unless url
|
245
|
+
|
222
246
|
entry = http.get_atom_entry url
|
223
247
|
|
224
|
-
|
248
|
+
slug, new_entry = entry.edit
|
249
|
+
|
250
|
+
res = coll.put! new_entry, url
|
225
251
|
|
226
252
|
# XXX error recovery here, lost updates suck
|
227
253
|
puts res.body
|
@@ -229,7 +255,7 @@ end
|
|
229
255
|
|
230
256
|
delete = lambda do
|
231
257
|
coll.update!
|
232
|
-
|
258
|
+
|
233
259
|
coll.entries.each_with_index do |entry,idx|
|
234
260
|
puts "#{idx} #{entry.title}"
|
235
261
|
end
|
data/lib/atom/element.rb
CHANGED
@@ -32,6 +32,11 @@ module Atom # :nodoc:
|
|
32
32
|
item
|
33
33
|
end
|
34
34
|
|
35
|
+
def << item
|
36
|
+
raise ArgumentError, "this can only hold items of class #{self.class.holds}" unless item.is_a? self.class.holds
|
37
|
+
super(item)
|
38
|
+
end
|
39
|
+
|
35
40
|
def to_element
|
36
41
|
collect do |item| item.to_element end
|
37
42
|
end
|
@@ -42,6 +47,8 @@ module Atom # :nodoc:
|
|
42
47
|
end
|
43
48
|
end
|
44
49
|
|
50
|
+
# The Class' methods provide a DSL for describing Atom's structure
|
51
|
+
# (and more generally for describing simple namespaced XML)
|
45
52
|
class Element < Hash
|
46
53
|
# a REXML::Element that shares this element's extension attributes
|
47
54
|
# and child elements
|
@@ -50,8 +57,6 @@ module Atom # :nodoc:
|
|
50
57
|
# this element's xml:base
|
51
58
|
attr_accessor :base
|
52
59
|
|
53
|
-
# The following is a DSL for describing an atom element.
|
54
|
-
|
55
60
|
# this element's attributes
|
56
61
|
def self.attrs # :nodoc:
|
57
62
|
@attrs || []
|
data/lib/atom/entry.rb
CHANGED
@@ -5,6 +5,7 @@ require "atom/text"
|
|
5
5
|
|
6
6
|
module Atom
|
7
7
|
NS = "http://www.w3.org/2005/Atom"
|
8
|
+
PP_NS = "http://purl.org/atom/app#"
|
8
9
|
|
9
10
|
# An individual entry in a feed. As an Atom::Element, it can be
|
10
11
|
# manipulated using accessors for each of its child elements. You
|
@@ -33,31 +34,31 @@ module Atom
|
|
33
34
|
element :id, String, true
|
34
35
|
element :title, Atom::Text, true
|
35
36
|
element :content, Atom::Content, true
|
36
|
-
|
37
|
+
|
37
38
|
element :rights, Atom::Text
|
38
39
|
# element :source, Atom::Feed # complicated, eg. serialization
|
39
|
-
|
40
|
+
|
40
41
|
element :authors, Atom::Multiple(Atom::Author)
|
41
42
|
element :contributors, Atom::Multiple(Atom::Contributor)
|
42
|
-
|
43
|
+
|
43
44
|
element :categories, Atom::Multiple(Atom::Category)
|
44
45
|
element :links, Atom::Multiple(Atom::Link)
|
45
|
-
|
46
|
+
|
46
47
|
element :published, Atom::Time
|
47
48
|
element :updated, Atom::Time, true
|
48
|
-
|
49
|
+
|
49
50
|
element :summary, Atom::Text
|
50
51
|
|
51
52
|
def initialize # :nodoc:
|
52
53
|
super "entry"
|
53
|
-
|
54
|
+
|
54
55
|
# XXX I don't think I've ever actually used this
|
55
56
|
yield self if block_given?
|
56
57
|
end
|
57
58
|
|
58
59
|
# parses XML into an Atom::Entry
|
59
|
-
#
|
60
|
-
# +base+ is the absolute URI the document was fetched from
|
60
|
+
#
|
61
|
+
# +base+ is the absolute URI the document was fetched from
|
61
62
|
# (if there is one)
|
62
63
|
def self.parse xml, base = ""
|
63
64
|
if xml.respond_to? :to_atom_entry
|
@@ -80,11 +81,12 @@ module Atom
|
|
80
81
|
self.updated = Time.now
|
81
82
|
end
|
82
83
|
|
83
|
-
# categorize the entry
|
84
|
-
|
85
|
-
|
84
|
+
# categorize the entry with each of an array or a space-separated
|
85
|
+
# string
|
86
|
+
def tag_with tags
|
87
|
+
return unless tags
|
86
88
|
|
87
|
-
|
89
|
+
(tags.is_a?(String) ? tags.split : tags).each do |tag|
|
88
90
|
categories.new["term"] = tag
|
89
91
|
end
|
90
92
|
end
|
@@ -102,6 +104,39 @@ module Atom
|
|
102
104
|
end
|
103
105
|
end
|
104
106
|
|
107
|
+
def draft
|
108
|
+
elem = REXML::XPath.first(extensions, "app:control/app:draft", {"app" => PP_NS})
|
109
|
+
|
110
|
+
elem and elem.text == "yes"
|
111
|
+
end
|
112
|
+
|
113
|
+
def draft= is_draft
|
114
|
+
nses = {"app" => PP_NS}
|
115
|
+
draft_e = REXML::XPath.first(extensions, "app:control/app:draft", nses)
|
116
|
+
control_e = REXML::XPath.first(extensions, "app:control", nses)
|
117
|
+
|
118
|
+
if is_draft and not draft
|
119
|
+
unless draft_e
|
120
|
+
unless control_e
|
121
|
+
control_e = REXML::Element.new("control")
|
122
|
+
control_e.add_namespace PP_NS
|
123
|
+
|
124
|
+
extensions << control_e
|
125
|
+
end
|
126
|
+
|
127
|
+
draft_e = REXML::Element.new("draft")
|
128
|
+
control_e << draft_e
|
129
|
+
end
|
130
|
+
|
131
|
+
draft_e.text = "yes"
|
132
|
+
elsif not is_draft and draft
|
133
|
+
draft_e.remove
|
134
|
+
control_e.remove if control_e.elements.empty?
|
135
|
+
end
|
136
|
+
|
137
|
+
is_draft
|
138
|
+
end
|
139
|
+
|
105
140
|
# XXX this needs a test suite before it can be trusted.
|
106
141
|
=begin
|
107
142
|
# tests the entry's validity
|
@@ -126,18 +161,18 @@ module Atom
|
|
126
161
|
|
127
162
|
alternates.each do |link|
|
128
163
|
if alternates.find do |x|
|
129
|
-
not x == link and
|
130
|
-
x["type"] == link["type"] and
|
164
|
+
not x == link and
|
165
|
+
x["type"] == link["type"] and
|
131
166
|
x["hreflang"] == link["hreflang"]
|
132
167
|
end
|
133
|
-
|
168
|
+
|
134
169
|
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.' ]
|
135
170
|
end
|
136
171
|
end
|
137
172
|
|
138
173
|
type = @content["type"]
|
139
174
|
|
140
|
-
base64ed = (not ["", "text", "html", "xhtml"].member? type) and
|
175
|
+
base64ed = (not ["", "text", "html", "xhtml"].member? type) and
|
141
176
|
type.match(/^text\/.*/).nil? and # not text
|
142
177
|
type.match(/.*[\+\/]xml$/).nil? # not XML
|
143
178
|
|
data/lib/atom/feed.rb
CHANGED
@@ -167,11 +167,6 @@ module Atom
|
|
167
167
|
|
168
168
|
res = @http.get(@uri, headers)
|
169
169
|
|
170
|
-
# we'll be forgiving about feed content types.
|
171
|
-
res.validate_content_type(["application/atom+xml",
|
172
|
-
"application/xml",
|
173
|
-
"text/xml"])
|
174
|
-
|
175
170
|
if res.code == "304"
|
176
171
|
# we're already all up to date
|
177
172
|
return self
|
@@ -181,7 +176,12 @@ module Atom
|
|
181
176
|
raise Atom::HTTPException, "Unexpected HTTP response code: #{res.code}"
|
182
177
|
end
|
183
178
|
|
184
|
-
|
179
|
+
# we'll be forgiving about feed content types.
|
180
|
+
res.validate_content_type(["application/atom+xml",
|
181
|
+
"application/xml",
|
182
|
+
"text/xml"])
|
183
|
+
|
184
|
+
@etag = res["ETag"] if res["ETag"]
|
185
185
|
@last_modified = res["Last-Modified"] if res["Last-Modified"]
|
186
186
|
|
187
187
|
xml = res.body
|
data/lib/atom/http.rb
CHANGED
@@ -14,7 +14,7 @@ class String # :nodoc:
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module Atom
|
17
|
-
UA = "atom-tools 0.9.
|
17
|
+
UA = "atom-tools 0.9.3"
|
18
18
|
|
19
19
|
module DigestAuth
|
20
20
|
CNONCE = Digest::MD5.new("%x" % (Time.now.to_i + rand(65535))).hexdigest
|
@@ -172,12 +172,11 @@ module Atom
|
|
172
172
|
#
|
173
173
|
# the default is to use the values of @user and @pass.
|
174
174
|
#
|
175
|
-
# your block will be called with two parameters
|
175
|
+
# your block will be called with two parameters:
|
176
176
|
# abs_url:: the base URL of the request URL
|
177
|
-
# realm:: the realm used in the WWW-Authenticate header
|
178
|
-
#
|
179
|
-
#
|
180
|
-
# it should return a value of the form [username, password]
|
177
|
+
# realm:: the realm used in the WWW-Authenticate header (maybe nil)
|
178
|
+
#
|
179
|
+
# your block should return [username, password], or nil
|
181
180
|
def when_auth &block # :yields: abs_url, realm
|
182
181
|
@get_auth_details = block
|
183
182
|
end
|
@@ -186,14 +185,14 @@ module Atom
|
|
186
185
|
def get_atom_entry(url)
|
187
186
|
res = get(url, "Accept" => "application/atom+xml")
|
188
187
|
|
189
|
-
# be picky for atom:entrys
|
190
|
-
res.validate_content_type( [ "application/atom+xml" ] )
|
191
|
-
|
192
188
|
# XXX handle other HTTP codes
|
193
189
|
if res.code != "200"
|
194
|
-
raise Atom::HTTPException, "expected
|
190
|
+
raise Atom::HTTPException, "failed to fetch entry: expected 200 OK, got #{res.code}"
|
195
191
|
end
|
196
192
|
|
193
|
+
# be picky for atom:entrys
|
194
|
+
res.validate_content_type( [ "application/atom+xml" ] )
|
195
|
+
|
197
196
|
Atom::Entry.parse(res.body, url)
|
198
197
|
end
|
199
198
|
|
@@ -201,10 +200,10 @@ module Atom
|
|
201
200
|
def put_atom_entry(entry, url = entry.edit_url)
|
202
201
|
raise "Cowardly refusing to PUT a non-Atom::Entry (#{entry.class})" unless entry.is_a? Atom::Entry
|
203
202
|
headers = {"Content-Type" => "application/atom+xml" }
|
204
|
-
|
203
|
+
|
205
204
|
put(url, entry.to_s, headers)
|
206
205
|
end
|
207
|
-
|
206
|
+
|
208
207
|
private
|
209
208
|
# parses plain quoted-strings
|
210
209
|
def parse_quoted_wwwauth param_string
|
@@ -229,13 +228,13 @@ module Atom
|
|
229
228
|
def wsse_authenticate(req, url, params = {})
|
230
229
|
user, pass = username_and_password_for_realm(url, params["realm"])
|
231
230
|
|
232
|
-
|
233
|
-
|
231
|
+
# thanks to Sam Ruby
|
232
|
+
nonce = rand(16**32).to_s(16)
|
233
|
+
now = Time.now.gmtime.iso8601
|
234
234
|
|
235
|
-
now = Time.now.iso8601
|
236
235
|
digest = [Digest::SHA1.digest(nonce + now + pass)].pack("m").chomp
|
237
236
|
|
238
|
-
req['X-WSSE'] = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{
|
237
|
+
req['X-WSSE'] = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{nonce}", Created="#{now}">
|
239
238
|
req["Authorization"] = 'WSSE profile="UsernameToken"'
|
240
239
|
end
|
241
240
|
|
@@ -264,8 +263,13 @@ module Atom
|
|
264
263
|
elsif www_authenticate
|
265
264
|
# XXX multiple challenges, multiple headers
|
266
265
|
param_string = www_authenticate.sub!(/^(\w+) /, "")
|
267
|
-
|
268
|
-
|
266
|
+
auth_method = ($~[1].downcase + "_authenticate").to_sym
|
267
|
+
|
268
|
+
if self.respond_to? auth_method, true # includes private methods
|
269
|
+
self.send(auth_method, req, url, param_string)
|
270
|
+
else
|
271
|
+
raise "No support for #{$~[1]} authentication"
|
272
|
+
end
|
269
273
|
end
|
270
274
|
|
271
275
|
http_obj = Net::HTTP.new(url.host, url.port)
|
@@ -317,7 +321,7 @@ module Atom
|
|
317
321
|
module HTTPResponse
|
318
322
|
# this should probably support ranges (eg. text/*)
|
319
323
|
def validate_content_type( valid )
|
320
|
-
raise Atom::HTTPException, "HTTP response contains no Content-Type!"
|
324
|
+
raise Atom::HTTPException, "HTTP response contains no Content-Type!" if not self.content_type or self.content_type.empty?
|
321
325
|
|
322
326
|
media_type = self.content_type.split(";").first
|
323
327
|
|