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.
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