atom-tools 0.9.0 → 2.0.5

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 (93) hide show
  1. data/COPYING +3 -3
  2. data/README +9 -49
  3. data/Rakefile +65 -62
  4. data/bin/atom-cp +159 -0
  5. data/bin/atom-grep +78 -0
  6. data/bin/atom-post +73 -0
  7. data/bin/atom-purge +82 -0
  8. data/doc/classes/Atom/AttrEl.html +163 -0
  9. data/doc/classes/Atom/Author.html +2 -19
  10. data/doc/classes/Atom/AutodiscoveryFailure.html +111 -0
  11. data/doc/classes/Atom/Categories.html +168 -0
  12. data/doc/classes/Atom/Category.html +10 -1
  13. data/doc/classes/Atom/Collection.html +194 -91
  14. data/doc/classes/Atom/Content.html +42 -1
  15. data/doc/classes/Atom/Contributor.html +2 -8
  16. data/doc/classes/Atom/Control.html +126 -0
  17. data/doc/classes/Atom/Converters.html +571 -0
  18. data/doc/classes/Atom/DigestAuth.html +285 -0
  19. data/doc/classes/Atom/Element.html +673 -106
  20. data/doc/classes/Atom/FileCache.html +287 -0
  21. data/doc/classes/Atom/HTTP.html +211 -95
  22. data/doc/classes/Atom/HTTPResponse.html +190 -0
  23. data/doc/classes/Atom/HasCategories.html +187 -0
  24. data/doc/classes/Atom/HasLinks.html +168 -0
  25. data/doc/classes/Atom/Link.html +80 -3
  26. data/doc/classes/Atom/NilCache.html +202 -0
  27. data/doc/classes/Atom/ParseError.html +111 -0
  28. data/doc/classes/Atom/Parsers.html +311 -0
  29. data/doc/classes/Atom/Person.html +130 -0
  30. data/doc/classes/Atom/Rights.html +113 -0
  31. data/doc/classes/Atom/Service.html +298 -0
  32. data/doc/{files/lib/atom/yaml_rb.html → classes/Atom/Source.html} +26 -24
  33. data/doc/classes/Atom/Subtitle.html +113 -0
  34. data/doc/classes/Atom/Summary.html +113 -0
  35. data/doc/classes/Atom/Text.html +196 -53
  36. data/doc/classes/Atom/Title.html +113 -0
  37. data/doc/classes/Atom/Tools.html +525 -0
  38. data/doc/classes/Atom/Workspace.html +121 -0
  39. data/doc/classes/Object.html +204 -0
  40. data/doc/created.rid +1 -1
  41. data/doc/files/README.html +9 -49
  42. data/doc/files/lib/atom/cache_rb.html +302 -0
  43. data/doc/files/lib/atom/collection_rb.html +1 -2
  44. data/doc/files/lib/atom/element_rb.html +2 -1
  45. data/doc/files/lib/atom/entry_rb.html +2 -2
  46. data/doc/files/lib/atom/feed_rb.html +1 -2
  47. data/doc/files/lib/atom/http_rb.html +6 -1
  48. data/doc/files/lib/atom/{xml_rb.html → service_rb.html} +8 -6
  49. data/doc/files/lib/atom/text_rb.html +2 -1
  50. data/doc/files/lib/atom/{app_rb.html → tools_rb.html} +4 -6
  51. data/doc/fr_class_index.html +23 -3
  52. data/doc/fr_file_index.html +3 -3
  53. data/doc/fr_method_index.html +111 -36
  54. data/lib/atom/cache.rb +178 -0
  55. data/lib/atom/collection.rb +90 -40
  56. data/lib/atom/element.rb +569 -161
  57. data/lib/atom/entry.rb +63 -89
  58. data/lib/atom/feed.rb +65 -67
  59. data/lib/atom/http.rb +421 -49
  60. data/lib/atom/service.rb +106 -0
  61. data/lib/atom/text.rb +138 -61
  62. data/lib/atom/tools.rb +163 -0
  63. data/spec/entry_spec.rb +364 -0
  64. data/spec/ext_spec.rb +42 -0
  65. data/spec/feed_spec.rb +39 -0
  66. data/spec/fixtures/contacts-feed.xml +34 -0
  67. data/spec/fixtures/entry-w-ext.xml +15 -0
  68. data/spec/fixtures/entry-w-xml.xml +24 -0
  69. data/spec/fixtures/entry.xml +42 -0
  70. data/spec/fixtures/feed-w-ext.xml +33 -0
  71. data/spec/fixtures/service-w-xhtml-ns.xml +21 -0
  72. data/spec/fixtures/service.xml +36 -0
  73. data/spec/service_spec.rb +108 -0
  74. data/spec/spec_helper.rb +9 -0
  75. data/test/conformance/order.rb +11 -10
  76. data/test/conformance/title.rb +9 -9
  77. data/test/conformance/updated.rb +2 -1
  78. data/test/runtests.rb +0 -0
  79. data/test/test_constructs.rb +78 -8
  80. data/test/test_feed.rb +35 -29
  81. data/test/test_general.rb +3 -30
  82. data/test/test_http.rb +248 -34
  83. data/test/test_protocol.rb +143 -24
  84. data/test/test_xml.rb +145 -53
  85. metadata +116 -62
  86. data/bin/atom-client.rb +0 -246
  87. data/bin/atom-server.rb~ +0 -71
  88. data/doc/classes/Atom/App.html +0 -217
  89. data/doc/classes/Atom/Entry.html +0 -365
  90. data/doc/classes/Atom/Feed.html +0 -585
  91. data/lib/atom/app.rb +0 -87
  92. data/lib/atom/xml.rb +0 -200
  93. data/lib/atom/yaml.rb +0 -101
data/lib/atom/entry.rb CHANGED
@@ -3,8 +3,37 @@ require "rexml/document"
3
3
  require "atom/element"
4
4
  require "atom/text"
5
5
 
6
+ require 'atom/feed'
7
+
6
8
  module Atom
7
- NS = "http://www.w3.org/2005/Atom"
9
+ # this is just a forward declaration since atom/entry includes atom/feed and vice-versa.
10
+ class Feed < Atom::Element # :nodoc:
11
+ end
12
+
13
+ class Source < Atom::Feed
14
+ is_atom_element :source
15
+
16
+ # TODO: this shouldn't be necessary, but on_init doesn't get inherited the
17
+ # way I would like it to.
18
+ @on_init = Atom::Feed.instance_variable_get '@on_init'
19
+ end
20
+
21
+ class Control < Atom::Element
22
+ attr_accessor :draft
23
+
24
+ is_element PP_NS, :control
25
+
26
+ on_parse [PP_NS, 'draft'] do |e,x|
27
+ e.set(:draft, x.text == 'yes')
28
+ end
29
+
30
+ on_build do |e,x|
31
+ unless (v = e.get(:draft)).nil?
32
+ el = e.append_elem(x, ['app', PP_NS], 'draft')
33
+ el.text = (v ? 'yes' : 'no')
34
+ end
35
+ end
36
+ end
8
37
 
9
38
  # An individual entry in a feed. As an Atom::Element, it can be
10
39
  # manipulated using accessors for each of its child elements. You
@@ -26,110 +55,55 @@ module Atom
26
55
  # each of which is an Array of its respective type and can be used
27
56
  # thusly:
28
57
  #
29
- # author = entry.authors.new
30
- # author.name = "Captain Kangaroo"
58
+ # author = entry.authors.new :name => "Captain Kangaroo", :email => "kanga@example.net"
59
+ #
31
60
  class Entry < Atom::Element
61
+ is_atom_element :entry
62
+
32
63
  # the master list of standard children and the types they map to
33
- element :id, String, true
34
- element :title, Atom::Text, true
35
- element :content, Atom::Content, true
36
-
37
- element :rights, Atom::Text
38
- # element :source, Atom::Feed # complicated, eg. serialization
39
-
40
- element :authors, Atom::Multiple(Atom::Author)
41
- element :contributors, Atom::Multiple(Atom::Contributor)
42
-
43
- element :categories, Atom::Multiple(Atom::Category)
44
- element :links, Atom::Multiple(Atom::Link)
45
-
46
- element :published, Atom::Time
47
- element :updated, Atom::Time, true
48
-
49
- element :summary, Atom::Text
50
-
51
- def initialize # :nodoc:
52
- super "entry"
53
-
54
- # XXX I don't think I've ever actually used this
55
- yield self if block_given?
56
- end
64
+ atom_string :id
57
65
 
58
- # parses XML fetched from +base+ into an Atom::Entry
59
- def self.parse xml, base = ""
60
- if xml.respond_to? :to_atom_entry
61
- xml.to_atom_entry(base)
62
- else
63
- REXML::Document.new(xml.to_s).to_atom_entry(base)
64
- end
65
- end
66
+ atom_element :title, Atom::Title
67
+ atom_element :summary, Atom::Summary
68
+ atom_element :content, Atom::Content
66
69
 
67
- def inspect # :nodoc:
68
- "#<Atom::Entry id:'#{self.id}'>"
69
- end
70
+ atom_element :rights, Atom::Rights
70
71
 
71
- # declare that this entry has updated
72
- def updated!
73
- self.updated = Time.now
74
- end
72
+ atom_element :source, Atom::Source
75
73
 
76
- # categorize the entry based on a space-separated string
77
- def tag_with string
78
- return if string.nil?
74
+ atom_time :published
75
+ atom_time :updated
76
+ time ['app', PP_NS], :edited
79
77
 
80
- string.split.each do |tag|
81
- categories.new["term"] = tag
82
- end
83
- end
78
+ atom_elements :author, :authors, Atom::Author
79
+ atom_elements :contributor, :contributors, Atom::Contributor
84
80
 
85
- # XXX this needs a test suite before it can be trusted.
86
- =begin
87
- # tests the entry's validity
88
- def valid?
89
- self.class.required.each do |element|
90
- unless instance_variable_get "@#{element}"
91
- return [ false, "required element atom:#{element} missing" ]
92
- end
93
- end
81
+ element ['app', PP_NS], :control, Atom::Control
94
82
 
95
- if @authors.length == 0
96
- return [ false, "required element atom:author missing" ]
97
- end
83
+ include HasCategories
84
+ include HasLinks
98
85
 
99
- alternates = @links.find_all do |link|
100
- link["rel"] == "alternate"
101
- end
86
+ atom_link :edit_url, :rel => 'edit'
102
87
 
103
- unless @content or alternates
104
- return [ false, "no atom:content or atom:link[rel='alternate']" ]
105
- end
88
+ def inspect # :nodoc:
89
+ "#<Atom::Entry id:'#{self.id}'>"
90
+ end
106
91
 
107
- alternates.each do |link|
108
- if alternates.find do |x|
109
- not x == link and
110
- x["type"] == link["type"] and
111
- x["hreflang"] == link["hreflang"]
112
- end
113
-
114
- 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.' ]
115
- end
116
- end
92
+ def draft
93
+ control and control.draft
94
+ end
117
95
 
118
- type = @content["type"]
96
+ alias :draft? :draft
119
97
 
120
- base64ed = (not ["", "text", "html", "xhtml"].member? type) and
121
- type.match(/^text\/.*/).nil? and # not text
122
- type.match(/.*[\+\/]xml$/).nil? # not XML
98
+ def draft!
99
+ self.draft = true
100
+ end
123
101
 
124
- if (@content["src"] or base64ed) and not summary
125
- return [ false, "out-of-line or base64ed atom:content and no atom:summary" ]
102
+ def draft= is_draft
103
+ unless control
104
+ instance_variable_set '@control', Atom::Control.new
126
105
  end
127
-
128
- true
106
+ control.draft = is_draft
129
107
  end
130
- =end
131
108
  end
132
109
  end
133
-
134
- # this is here solely so that you don't have to require it
135
- require "atom/xml"
data/lib/atom/feed.rb CHANGED
@@ -5,8 +5,10 @@ require "atom/entry"
5
5
  require "atom/http"
6
6
 
7
7
  module Atom
8
- class HTTPException < RuntimeError # :nodoc:
8
+ # this is just a forward declaration since atom/entry includes atom/feed and vice-versa.
9
+ class Entry < Atom::Element # :nodoc:
9
10
  end
11
+
10
12
  class FeedGone < RuntimeError # :nodoc:
11
13
  end
12
14
 
@@ -23,44 +25,47 @@ module Atom
23
25
  # generator:: the agent used to generate a feed
24
26
  # icon:: an IRI identifying an icon which visually identifies a feed (1:1 aspect ratio, looks OK small)
25
27
  # logo:: an IRI identifying an image which visually identifies a feed (2:1 aspect ratio)
26
- # rights:: rights held in and over a feed (Atom::Text)
28
+ # rights:: rights held in and over a feed (Atom::Text)
27
29
  #
28
- # There are also +links+, +categories+, +authors+, +contributors+
30
+ # There are also +links+, +categories+, +authors+, +contributors+
29
31
  # and +entries+, each of which is an Array of its respective type and
30
32
  # can be used thusly:
31
33
  #
32
34
  # entry = feed.entries.new
33
35
  # entry.title = "blah blah blah"
36
+ #
34
37
  class Feed < Atom::Element
38
+ is_atom_element :feed
39
+
35
40
  attr_reader :uri
36
41
 
37
42
  # the Atom::Feed pointed to by link[@rel='previous']
38
43
  attr_reader :prev
39
44
  # the Atom::Feed pointed to by link[@rel='next']
40
45
  attr_reader :next
41
-
46
+
42
47
  # conditional get information from the last fetch
43
48
  attr_reader :etag, :last_modified
44
49
 
45
- element :id, String, true
46
- element :title, Atom::Text, true
47
- element :subtitle, Atom::Text
48
-
49
- element :updated, Atom::Time, true
50
+ atom_string :id
51
+ atom_element :title, Atom::Title
52
+ atom_element :subtitle, Atom::Subtitle
53
+
54
+ atom_time :updated
55
+
56
+ include HasLinks
57
+ include HasCategories
50
58
 
51
- element :links, Atom::Multiple(Atom::Link)
52
- element :categories, Atom::Multiple(Atom::Category)
59
+ atom_elements :author, :authors, Atom::Author
60
+ atom_elements :contributor, :contributors, Atom::Contributor
53
61
 
54
- element :authors, Atom::Multiple(Atom::Author)
55
- element :contributors, Atom::Multiple(Atom::Contributor)
62
+ atom_string :generator # XXX with uri and version attributes!
63
+ atom_string :icon
64
+ atom_string :logo
56
65
 
57
- element :generator, String # XXX with uri and version attributes!
58
- element :icon, String
59
- element :logo, String
66
+ atom_element :rights, Atom::Rights
60
67
 
61
- element :rights, Atom::Text
62
-
63
- element :entries, Atom::Multiple(Atom::Entry)
68
+ atom_elements :entry, :entries, Atom::Entry
64
69
 
65
70
  include Enumerable
66
71
 
@@ -68,15 +73,6 @@ module Atom
68
73
  "<#{@uri} entries: #{entries.length} title='#{title}'>"
69
74
  end
70
75
 
71
- # parses XML fetched from +base+ into an Atom::Feed
72
- def self.parse xml, base = ""
73
- if xml.respond_to? :to_atom_entry
74
- xml.to_atom_feed(base)
75
- else
76
- REXML::Document.new(xml.to_s).to_atom_feed(base)
77
- end
78
- end
79
-
80
76
  # Create a new Feed that can be found at feed_uri and retrieved
81
77
  # using an Atom::HTTP object http
82
78
  def initialize feed_uri = nil, http = Atom::HTTP.new
@@ -88,7 +84,7 @@ module Atom
88
84
  self.base = feed_uri
89
85
  end
90
86
 
91
- super "feed"
87
+ super()
92
88
  end
93
89
 
94
90
  # iterates over a feed's entries
@@ -96,11 +92,15 @@ module Atom
96
92
  @entries.each &block
97
93
  end
98
94
 
95
+ def empty?
96
+ @entries.empty?
97
+ end
98
+
99
99
  # gets everything in the logical feed (could be a lot of stuff)
100
- # (see <http://www.ietf.org/internet-drafts/draft-nottingham-atompub-feed-history-05.txt>)
100
+ # (see RFC 5005)
101
101
  def get_everything!
102
102
  self.update!
103
-
103
+
104
104
  prev = @prev
105
105
  while prev
106
106
  prev.update!
@@ -128,18 +128,22 @@ module Atom
128
128
  end
129
129
  end
130
130
 
131
- # like #merge, but in place
131
+ # like #merge, but in place
132
132
  def merge! other_feed
133
- [:id, :title, :subtitle, :updated, :rights].each { |p|
134
- self.send("#{p}=", other_feed.send("#{p}"))
135
- }
133
+ [:id, :title, :subtitle, :updated, :rights, :logo, :icon].each do |p|
134
+ if (v = other_feed.get(p))
135
+ set p, v
136
+ end
137
+ end
136
138
 
137
139
  [:links, :categories, :authors, :contributors].each do |p|
138
- other_feed.send("#{p}").each do |e|
139
- self.send("#{p}") << e
140
+ other_feed.get(p).each do |e|
141
+ get(p) << e
140
142
  end
141
143
  end
142
144
 
145
+ @extensions = other_feed.extensions
146
+
143
147
  merge_entries! other_feed
144
148
  end
145
149
 
@@ -149,22 +153,20 @@ module Atom
149
153
  feed = self.clone
150
154
 
151
155
  feed.merge! other_feed
152
-
156
+
153
157
  feed
154
158
  end
155
159
 
156
160
  # fetches this feed's URL, parses the result and #merge!s
157
161
  # changes, new entries, &c.
162
+ #
163
+ # (note that this is different from Atom::Entry#updated!
158
164
  def update!
159
165
  raise(RuntimeError, "can't fetch without a uri.") unless @uri
160
-
161
- headers = {}
162
- headers["If-None-Match"] = @etag if @etag
163
- headers["If-Modified-Since"] = @last_modified if @last_modified
164
166
 
165
- res = @http.get(@uri, headers)
167
+ res = @http.get(@uri, "Accept" => "application/atom+xml")
166
168
 
167
- if res.code == "304"
169
+ if @etag and res['etag'] == @etag
168
170
  # we're already all up to date
169
171
  return self
170
172
  elsif res.code == "410"
@@ -172,44 +174,43 @@ module Atom
172
174
  elsif res.code != "200"
173
175
  raise Atom::HTTPException, "Unexpected HTTP response code: #{res.code}"
174
176
  end
175
-
176
- unless res.content_type.match(/^application\/atom\+xml/)
177
- raise Atom::HTTPException, "Unexpected HTTP response Content-Type: #{res.content_type} (wanted application/atom+xml)"
178
- end
179
177
 
180
- @etag = res["Etag"] if res["Etag"]
181
- @last_modified = res["Last-Modified"] if res["Last-Modified"]
178
+ # we'll be forgiving about feed content types.
179
+ res.validate_content_type(["application/atom+xml",
180
+ "application/xml",
181
+ "text/xml"])
182
+
183
+ @etag = res["ETag"] if res["ETag"]
182
184
 
183
185
  xml = res.body
184
186
 
185
187
  coll = REXML::Document.new(xml)
186
188
 
187
- update_time = Time.parse(REXML::XPath.first(coll, "/atom:feed/atom:updated", { "atom" => Atom::NS } ).text)
189
+ update_el = REXML::XPath.first(coll, "/atom:feed/atom:updated", { "atom" => Atom::NS } )
188
190
 
189
- # the feed hasn't been updated, don't bother
190
- if self.updated and self.updated >= update_time
191
+ # the feed hasn't been updated, don't do anything.
192
+ if update_el and self.updated and self.updated >= Time.parse(update_el.text)
191
193
  return self
192
194
  end
193
195
 
194
- coll = Atom::Feed.parse(coll, self.base.to_s)
196
+ coll = self.class.parse(coll.root, self.base.to_s)
195
197
  merge! coll
196
-
197
- link = coll.links.find { |l| l["rel"] = "next" and l["type"] == "application/atom+xml" }
198
- if link
199
- abs_uri = @uri + link["href"]
200
- @next = Feed.new(abs_uri.to_s, @http)
198
+
199
+ if abs_uri = next_link
200
+ @next = self.class.new(abs_uri.to_s, @http)
201
201
  end
202
202
 
203
- link = coll.links.find { |l| l["rel"] = "previous" and l["type"] == "application/atom+xml" }
204
- if link
205
- abs_uri = @uri + link["href"]
206
- @prev = Feed.new(abs_uri.to_s, @http)
203
+ if abs_uri = previous_link
204
+ @prev = self.class.new(abs_uri.to_s, @http)
207
205
  end
208
206
 
209
207
  self
210
208
  end
211
209
 
212
- # adds an entry to this feed. if this feed already contains an
210
+ atom_link :previous_link, :rel => 'previous'
211
+ atom_link :next_link, :rel => 'next'
212
+
213
+ # adds an entry to this feed. if this feed already contains an
213
214
  # entry with the same id, the newest one is used.
214
215
  def << entry
215
216
  existing = entries.find do |e|
@@ -224,6 +225,3 @@ module Atom
224
225
  end
225
226
  end
226
227
  end
227
-
228
- # this is here solely so you don't have to require it
229
- require "atom/xml"