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.
Files changed (48) hide show
  1. data/README +2 -2
  2. data/Rakefile +1 -1
  3. data/bin/atom-client.rb +71 -45
  4. data/lib/atom/element.rb +7 -2
  5. data/lib/atom/entry.rb +51 -16
  6. data/lib/atom/feed.rb +6 -6
  7. data/lib/atom/http.rb +23 -19
  8. data/lib/atom/service.rb +0 -2
  9. data/lib/atom/text.rb +34 -12
  10. data/lib/atom/xml.rb +26 -17
  11. data/lib/atom/yaml.rb +23 -8
  12. data/test/test_constructs.rb +16 -2
  13. data/test/test_general.rb +22 -9
  14. data/test/test_http.rb +12 -8
  15. data/test/test_xml.rb +66 -20
  16. metadata +4 -40
  17. data/doc/classes/Atom/Author.html +0 -130
  18. data/doc/classes/Atom/Category.html +0 -128
  19. data/doc/classes/Atom/Collection.html +0 -322
  20. data/doc/classes/Atom/Content.html +0 -129
  21. data/doc/classes/Atom/Contributor.html +0 -119
  22. data/doc/classes/Atom/DigestAuth.html +0 -285
  23. data/doc/classes/Atom/Element.html +0 -325
  24. data/doc/classes/Atom/Entry.html +0 -369
  25. data/doc/classes/Atom/Feed.html +0 -595
  26. data/doc/classes/Atom/HTTP.html +0 -436
  27. data/doc/classes/Atom/HTTPResponse.html +0 -149
  28. data/doc/classes/Atom/Link.html +0 -137
  29. data/doc/classes/Atom/Service.html +0 -260
  30. data/doc/classes/Atom/Text.html +0 -245
  31. data/doc/classes/Atom/Workspace.html +0 -121
  32. data/doc/classes/XHTML.html +0 -118
  33. data/doc/created.rid +0 -1
  34. data/doc/files/README.html +0 -213
  35. data/doc/files/lib/atom/collection_rb.html +0 -110
  36. data/doc/files/lib/atom/element_rb.html +0 -109
  37. data/doc/files/lib/atom/entry_rb.html +0 -111
  38. data/doc/files/lib/atom/feed_rb.html +0 -112
  39. data/doc/files/lib/atom/http_rb.html +0 -112
  40. data/doc/files/lib/atom/service_rb.html +0 -111
  41. data/doc/files/lib/atom/text_rb.html +0 -109
  42. data/doc/files/lib/atom/xml_rb.html +0 -110
  43. data/doc/files/lib/atom/yaml_rb.html +0 -109
  44. data/doc/fr_class_index.html +0 -42
  45. data/doc/fr_file_index.html +0 -36
  46. data/doc/fr_method_index.html +0 -69
  47. data/doc/index.html +0 -24
  48. 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
@@ -6,7 +6,7 @@ require "rake/gempackagetask"
6
6
  require "rake/clean"
7
7
 
8
8
  NAME = "atom-tools"
9
- VERS = "0.9.2"
9
+ VERS = "0.9.3"
10
10
 
11
11
  # the following from markaby-0.5's tools/rakehelp
12
12
  def setup_tests
data/bin/atom-client.rb CHANGED
@@ -1,7 +1,29 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
- # syntax: ./atom-client.rb <introspection-url> [username] [password]
4
- # a really simple YAML-and-$EDITOR based Publishing Protocol client
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
- # humans don't care about these things, we can replace it later
82
+
83
+ # human readability
62
84
  yaml.delete "id"
63
85
 
64
86
  if yaml["links"]
65
- yaml["links"].delete(yaml["links"].find { |l| l["rel"] == "edit" })
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 "unchanged content, aborted"
140
+ puts "You didn't edit anything, aborting."
138
141
  exit
139
142
  end
140
143
 
141
- entry = Atom::Entry.from_yaml edited
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
- server = Atom::Service.new(introspection_url, http)
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
- coll = choose_collection server
192
+ puts "which collection?"
193
+
194
+ collections.keys.each_with_index do |name,index|
195
+ puts "#{index}: #{name}"
196
+ end
190
197
 
191
- # XXX the server should *probably* replace this, but who knows yet?
192
- CLIENT_ID = "http://necronomicorp.com/dev/null"
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
- res = coll.put! entry.edit, url
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 based on a space-separated string
84
- def tag_with string
85
- return if string.nil?
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
- string.split.each do |tag|
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
- @etag = res["Etag"] if res["Etag"]
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.2"
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
- # (will be nil if there is no WWW-Authenticate header)
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 Atom::Entry, didn't get it"
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
- nonce = Array.new(10){ rand(0x100000000) }.pack('I*')
233
- nonce_b64 = [nonce].pack("m").chomp
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="#{nonce_b64}", Created="#{now}">
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
- auth_type = $~[1]
268
- self.send("#{auth_type.downcase}_authenticate", req, url, param_string)
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!" unless self.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