atom-tools 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|