atom-tools 0.9.0

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 (57) hide show
  1. data/COPYING +18 -0
  2. data/README +103 -0
  3. data/Rakefile +77 -0
  4. data/bin/atom-client.rb +246 -0
  5. data/bin/atom-server.rb~ +71 -0
  6. data/doc/classes/Atom/App.html +217 -0
  7. data/doc/classes/Atom/Author.html +130 -0
  8. data/doc/classes/Atom/Category.html +128 -0
  9. data/doc/classes/Atom/Collection.html +322 -0
  10. data/doc/classes/Atom/Content.html +129 -0
  11. data/doc/classes/Atom/Contributor.html +119 -0
  12. data/doc/classes/Atom/Element.html +325 -0
  13. data/doc/classes/Atom/Entry.html +365 -0
  14. data/doc/classes/Atom/Feed.html +585 -0
  15. data/doc/classes/Atom/HTTP.html +374 -0
  16. data/doc/classes/Atom/Link.html +137 -0
  17. data/doc/classes/Atom/Text.html +229 -0
  18. data/doc/classes/XHTML.html +118 -0
  19. data/doc/created.rid +1 -0
  20. data/doc/files/README.html +213 -0
  21. data/doc/files/lib/atom/app_rb.html +110 -0
  22. data/doc/files/lib/atom/collection_rb.html +110 -0
  23. data/doc/files/lib/atom/element_rb.html +109 -0
  24. data/doc/files/lib/atom/entry_rb.html +111 -0
  25. data/doc/files/lib/atom/feed_rb.html +112 -0
  26. data/doc/files/lib/atom/http_rb.html +109 -0
  27. data/doc/files/lib/atom/text_rb.html +108 -0
  28. data/doc/files/lib/atom/xml_rb.html +110 -0
  29. data/doc/files/lib/atom/yaml_rb.html +109 -0
  30. data/doc/fr_class_index.html +39 -0
  31. data/doc/fr_file_index.html +36 -0
  32. data/doc/fr_method_index.html +62 -0
  33. data/doc/index.html +24 -0
  34. data/doc/rdoc-style.css +208 -0
  35. data/lib/atom/app.rb +87 -0
  36. data/lib/atom/collection.rb +75 -0
  37. data/lib/atom/element.rb +277 -0
  38. data/lib/atom/entry.rb +135 -0
  39. data/lib/atom/feed.rb +229 -0
  40. data/lib/atom/http.rb +132 -0
  41. data/lib/atom/text.rb +163 -0
  42. data/lib/atom/xml.rb +200 -0
  43. data/lib/atom/yaml.rb +101 -0
  44. data/setup.rb +1585 -0
  45. data/test/conformance/order.rb +117 -0
  46. data/test/conformance/title.rb +108 -0
  47. data/test/conformance/updated.rb +33 -0
  48. data/test/conformance/xhtmlcontentdiv.rb +18 -0
  49. data/test/conformance/xmlnamespace.rb +54 -0
  50. data/test/runtests.rb +14 -0
  51. data/test/test_constructs.rb +91 -0
  52. data/test/test_feed.rb +128 -0
  53. data/test/test_general.rb +99 -0
  54. data/test/test_http.rb +86 -0
  55. data/test/test_protocol.rb +69 -0
  56. data/test/test_xml.rb +353 -0
  57. metadata +107 -0
data/lib/atom/http.rb ADDED
@@ -0,0 +1,132 @@
1
+ require "net/http"
2
+ require 'uri'
3
+
4
+ module URI # :nodoc: all
5
+ class Generic; def to_uri; self; end; end
6
+ end
7
+
8
+ class String # :nodoc:
9
+ def to_uri; URI.parse(self); end
10
+ end
11
+
12
+ module Atom
13
+ UA = "atom-tools 0.9.0"
14
+ class Unauthorized < RuntimeError # :nodoc:
15
+ end
16
+
17
+ # An object which handles the details of HTTP - particularly
18
+ # authentication and caching (neither of which are fully implemented).
19
+ #
20
+ # This object can be used on its own, or passed to an Atom::App,
21
+ # Atom::Collection or Atom::Feed, where it will be used for requests.
22
+ #
23
+ # All its HTTP methods return a Net::HTTPResponse
24
+ class HTTP
25
+ # used by the default #when_auth
26
+ attr_accessor :user, :pass
27
+
28
+ def initialize # :nodoc:
29
+ @get_auth_details = lambda do |abs_url, realm|
30
+ if @user and @pass
31
+ [@user, @pass]
32
+ else
33
+ nil
34
+ end
35
+ end
36
+ end
37
+
38
+ # GETs an url
39
+ def get url, headers = {}
40
+ http_request(url, Net::HTTP::Get, nil, headers)
41
+ end
42
+
43
+ # POSTs body to an url
44
+ def post url, body, headers = {}
45
+ http_request(url, Net::HTTP::Post, body, headers)
46
+ end
47
+
48
+ # PUTs body to an url
49
+ def put url, body, headers = {}
50
+ http_request(url, Net::HTTP::Put, body, headers)
51
+ end
52
+
53
+ # DELETEs to url
54
+ def delete url, body = nil, headers = {}
55
+ http_request(url, Net::HTTP::Delete, body, headers)
56
+ end
57
+
58
+ # a block that will be called when a remote server responds with
59
+ # 401 Unauthorized, so that your application can prompt for
60
+ # authentication details
61
+ #
62
+ # it will be called with the base URL of the requested URL, and the realm used in the WWW-Authenticate header.
63
+ #
64
+ # it should return a value of the form [username, password]
65
+ def when_auth &block
66
+ @get_auth_details = block
67
+ end
68
+
69
+ private
70
+ def parse_wwwauth www_authenticate
71
+ auth_type = www_authenticate.split[0] # "Digest" or "Basic"
72
+ auth_params = {}
73
+
74
+ www_authenticate =~ /^(\w+) (.*)/
75
+
76
+ $2.gsub(/(\w+)="(.*?)"/) { auth_params[$1] = $2 }
77
+
78
+ [ auth_type, auth_params ]
79
+ end
80
+
81
+ # performs an authenticated http request
82
+ def authenticated_request(url_string, method, wwwauth, body = nil, init_headers = {})
83
+
84
+ auth_type, params = parse_wwwauth(wwwauth)
85
+ req, url = new_request(url_string, method, init_headers)
86
+
87
+ realm = params["realm"]
88
+ abs_url = (url + "/").to_s
89
+
90
+ user, pass = @get_auth_details.call(abs_url, realm)
91
+
92
+ raise Unauthorized unless user and pass
93
+
94
+ if auth_type == "Basic"
95
+ req.basic_auth user, pass
96
+ else
97
+ # TODO: implement Digest auth
98
+ raise "atom-tools only supports Basic authentication"
99
+ end
100
+
101
+ res = Net::HTTP.start(url.host, url.port) { |h| h.request(req, body) }
102
+
103
+ raise Unauthorized if res.kind_of? Net::HTTPUnauthorized
104
+ res
105
+ end
106
+
107
+ # performs a regular http request. if it responds 401
108
+ # then it retries using @user and @pass for authentication
109
+ def http_request(url_string, method, body = nil, init_headers = {})
110
+ req, url = new_request(url_string, method, init_headers)
111
+
112
+ res = Net::HTTP.start(url.host, url.port) { |h| h.request(req, body) }
113
+
114
+ if res.kind_of? Net::HTTPUnauthorized
115
+ res = authenticated_request(url, method, res["WWW-Authenticate"], body, init_headers)
116
+ end
117
+
118
+ res
119
+ end
120
+
121
+ def new_request(url_string, method, init_headers = {})
122
+ headers = { "User-Agent" => UA }.merge(init_headers)
123
+
124
+ url = url_string.to_uri
125
+
126
+ rel = url.path
127
+ rel += "?" + url.query if url.query
128
+
129
+ [method.new(rel, headers), url]
130
+ end
131
+ end
132
+ end
data/lib/atom/text.rb ADDED
@@ -0,0 +1,163 @@
1
+ require "atom/element"
2
+
3
+ module XHTML
4
+ NS = "http://www.w3.org/1999/xhtml"
5
+ end
6
+
7
+ module Atom
8
+ # An Atom::Element representing a text construct.
9
+ # It has a single attribute, "type", which accepts values
10
+ # "text", "html" and "xhtml"
11
+
12
+ class Text < Atom::Element
13
+ attrb :type
14
+
15
+ def initialize value, name # :nodoc:
16
+ @content = value
17
+ @content ||= "" # in case of nil
18
+ self["type"] = "text"
19
+
20
+ super name
21
+ end
22
+
23
+ def to_s
24
+ if self["type"] == "xhtml"
25
+ @content.children.to_s
26
+ else
27
+ @content.to_s
28
+ end
29
+ end
30
+
31
+ # returns a string suitable for dumping into an HTML document
32
+ def html
33
+ if self["type"] == "xhtml" or self["type"] == "html"
34
+ to_s
35
+ elsif self["type"] == "text"
36
+ REXML::Text.new(to_s).to_s
37
+ end
38
+ end
39
+
40
+ # attepts to parse the content and return it as an array of REXML::Elements
41
+ def xml
42
+ if self["type"] == "xhtml"
43
+ @content.children
44
+ elsif self["type"] == "text"
45
+ [self.to_s]
46
+ else
47
+ # XXX - hpricot goes here?
48
+ raise "I haven't implemented this yet"
49
+ end
50
+ end
51
+
52
+ def inspect # :nodoc:
53
+ "'#{to_s}'##{self['type']}"
54
+ end
55
+
56
+ def []= key, value # :nodoc:
57
+ if key == "type"
58
+ unless valid_type? value
59
+ raise "atomTextConstruct type '#{value}' is meaningless"
60
+ end
61
+
62
+ if value == "xhtml"
63
+ begin
64
+ parse_xhtml_content
65
+ rescue REXML::ParseException
66
+ raise "#{@content.inspect} can't be parsed as XML"
67
+ end
68
+ end
69
+ end
70
+
71
+ super(key, value)
72
+ end
73
+
74
+ def to_element # :nodoc:
75
+ e = super
76
+
77
+ if self["type"] == "text"
78
+ e.attributes.delete "type"
79
+ end
80
+
81
+ # this should be done via inheritance
82
+ unless self.class == Atom::Content and self["src"]
83
+ c = convert_contents e
84
+
85
+ if c.is_a? String
86
+ e.text = c
87
+ elsif c.is_a? REXML::Element
88
+ e << c.dup
89
+ else
90
+ raise RuntimeError, "atom:#{local_name} can't contain type #{@content.class}"
91
+ end
92
+ end
93
+
94
+ e
95
+ end
96
+
97
+ private
98
+ def convert_contents e
99
+ if self["type"] == "xhtml"
100
+ @content
101
+ elsif self["type"] == "text" or self["type"].nil?
102
+ REXML::Text.normalize(@content.to_s)
103
+ elsif self["type"] == "html"
104
+ @content.to_s
105
+ end
106
+ end
107
+
108
+ def valid_type? type
109
+ ["text", "xhtml", "html"].member? type
110
+ end
111
+
112
+ def parse_xhtml_content xhtml = nil
113
+ xhtml ||= @content
114
+
115
+ @content = if xhtml.is_a? REXML::Element
116
+ if xhtml.name == "div" and xhtml.namespace == XHTML::NS
117
+ xhtml.dup
118
+ else
119
+ elem = REXML::Element.new("div")
120
+ elem.add_namespace(XHTML::NS)
121
+
122
+ elem << xhtml.dup
123
+
124
+ elem
125
+ end
126
+ elsif xhtml.is_a? REXML::Document
127
+ parse_xhtml_content xhtml.root
128
+ else
129
+ div = REXML::Document.new("<div>#{@content}</div>")
130
+ div.root.add_namespace(XHTML::NS)
131
+
132
+ div.root
133
+ end
134
+ end
135
+ end
136
+
137
+ # Atom::Content behaves the same as an Atom::Text, but for two things:
138
+ #
139
+ # * the "type" attribute can be an arbitrary media type
140
+ # * there is a "src" attribute which is an IRI that points to the content of the entry (in which case the content element will be empty)
141
+ class Content < Atom::Text
142
+ attrb :src
143
+
144
+ private
145
+ def valid_type? type
146
+ super or type.match(/\//)
147
+ end
148
+
149
+ def convert_contents e
150
+ s = super
151
+
152
+ s ||= if @content.is_a? REXML::Document
153
+ @content.root
154
+ elsif @content.is_a? REXML::Element
155
+ @content
156
+ else
157
+ REXML::Text.normalize(@content.to_s)
158
+ end
159
+
160
+ s
161
+ end
162
+ end
163
+ end
data/lib/atom/xml.rb ADDED
@@ -0,0 +1,200 @@
1
+ require "atom/entry"
2
+ require "atom/feed"
3
+ require "uri"
4
+
5
+ # Two big, interrelated problems:
6
+ #
7
+ # * I probably shouldn't be playing around in REXML's namespace
8
+ # * REXML isn't a great parser, and other options would be nice
9
+ #
10
+ # This shouldn't be hard to do, it's just tedious and non-critical
11
+ module REXML # :nodoc: all
12
+ class Document
13
+ def to_atom_entry base = ""
14
+ self.root.to_atom_entry base
15
+ end
16
+ def to_atom_feed base = ""
17
+ self.root.to_atom_feed base
18
+ end
19
+ end
20
+ class Element
21
+ def get_atom_element name
22
+ XPath.first(self, "./atom:#{name}", { "atom" => Atom::NS })
23
+ end
24
+
25
+ def each_atom_element name
26
+ XPath.each(self, "./atom:#{name}", { "atom" => Atom::NS }) do |elem|
27
+ yield elem
28
+ end
29
+ end
30
+
31
+ def get_extensions
32
+ # XXX also look for attributes
33
+ children.find_all { |child| child.respond_to? :namespace and child.namespace != Atom::NS }
34
+ end
35
+
36
+ # get the text content of a descendant element in the Atom namespace
37
+ def get_atom_text name
38
+ elem = get_atom_element name
39
+ if elem
40
+ elem.text
41
+ else
42
+ nil
43
+ end
44
+ end
45
+
46
+ # a workaround for the odd way in which REXML handles namespaces
47
+ # returns the value of the attribute +name+ that's in the same namespace as this element
48
+ def ns_attr name
49
+ if not self.prefix.empty?
50
+ attr = self.prefix + ":" + name
51
+ else
52
+ attr = name
53
+ end
54
+
55
+ self.attributes[attr]
56
+ end
57
+
58
+ def fill_text_construct(entry, name)
59
+ text = get_atom_element(name)
60
+ if text
61
+ type = text.ns_attr("type")
62
+ src = text.ns_attr("src")
63
+
64
+ if src and name == :content
65
+ # the only content is out of line
66
+ entry.send( "#{name}=".to_sym, "")
67
+ entry.send(name.to_sym)["src"] = src
68
+ elsif type == "xhtml"
69
+ div = XPath.first(text, "./xhtml:div", { "xhtml" => XHTML::NS })
70
+ unless div
71
+ raise "Refusing to parse type='xhtml' with no <div/> wrapper"
72
+ end
73
+
74
+ # content is the serialized content of the <div> wrapper
75
+ entry.send( "#{name}=".to_sym, div )
76
+ else
77
+ raw = text.text
78
+ entry.send( "#{name}=", raw )
79
+ end
80
+
81
+ if text.attributes["xml:base"]
82
+ entry.send(name.to_sym).base = text.attributes["xml:base"]
83
+ end
84
+
85
+ if type and type != "text"
86
+ entry.send(name.to_sym)["type"] = type
87
+ end
88
+ end
89
+ end
90
+
91
+ def fill_elem_element(top, kind)
92
+ each_atom_element(kind) do |elem|
93
+ person = top.send("#{kind}s".to_sym).new
94
+
95
+ ["name", "uri", "email"].each do |name|
96
+ person.send("#{name}=".to_sym, elem.get_atom_text(name))
97
+ end
98
+ end
99
+ end
100
+
101
+ def fill_attr_element(top, array, kind)
102
+ each_atom_element(kind) do |elem|
103
+ thing = array.new
104
+
105
+ thing.class.attrs.each do |name,req|
106
+ value = elem.ns_attr name.to_s
107
+ if value and name == :href
108
+ thing[name.to_s] = (URI.parse(top.base) + value).to_s
109
+ elsif value
110
+ thing[name.to_s] = value
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # 'base' is the URI that you fetched this document from.
117
+ def to_atom_entry base = ""
118
+ unless self.name == "entry" and self.namespace == Atom::NS
119
+ raise TypeError, "this isn't an atom:entry! (name: #{self.name}, ns: #{self.namespace})"
120
+ end
121
+
122
+ entry = Atom::Entry.new
123
+
124
+ entry.base = if attributes["xml:base"]
125
+ (URI.parse(base) + attributes["xml:base"]).to_s
126
+ else
127
+ # go with the URL we were passed in
128
+ base
129
+ end
130
+
131
+ # Text constructs
132
+ entry.class.elements.find_all { |n,k,r| k.ancestors.member? Atom::Text }.each do |n,k,r|
133
+ fill_text_construct(entry, n)
134
+ end
135
+
136
+ ["id", "published", "updated"].each do |name|
137
+ entry.send("#{name}=".to_sym, get_atom_text(name))
138
+ end
139
+
140
+ ["author", "contributor"].each do |type|
141
+ fill_elem_element(entry, type)
142
+ end
143
+
144
+ {"link" => entry.links, "category" => entry.categories}.each do |k,v|
145
+ fill_attr_element(entry, v, k)
146
+ end
147
+
148
+ # extension elements
149
+ get_extensions.each do |elem|
150
+ entry.extensions << elem.dup # otherwise they get removed from the doc
151
+ end
152
+
153
+ entry
154
+ end
155
+
156
+ # 'base' is the URI that you fetched this document from.
157
+ def to_atom_feed base = ""
158
+ unless self.name == "feed" and self.namespace == Atom::NS
159
+ raise TypeError, "this isn't an atom:feed! (name: #{self.name}, ns: #{self.namespace})"
160
+ end
161
+
162
+ feed = Atom::Feed.new
163
+
164
+ feed.base = if attributes["xml:base"]
165
+ (URI.parse(base) + attributes["xml:base"]).to_s
166
+ else
167
+ # go with the URL we were passed in
168
+ base
169
+ end
170
+
171
+ # Text constructs
172
+ feed.class.elements.find_all { |n,k,r| k.ancestors.member? Atom::Text }.each do |n,k,r|
173
+ fill_text_construct(feed, n)
174
+ end
175
+
176
+ ["id", "updated", "generator", "icon", "logo"].each do |name|
177
+ feed.send("#{name}=".to_sym, get_atom_text(name))
178
+ end
179
+
180
+ ["author", "contributor"].each do |type|
181
+ fill_elem_element(feed, type)
182
+ end
183
+
184
+ {"link" => feed.links, "category" => feed.categories}.each do |k,v|
185
+ fill_attr_element(feed, v, k)
186
+ end
187
+
188
+ each_atom_element("entry") do |elem|
189
+ feed << elem.to_atom_entry(feed.base)
190
+ end
191
+
192
+ get_extensions.each do |elem|
193
+ # have to duplicate them, or they'll get removed from the doc
194
+ feed.extensions << elem.dup
195
+ end
196
+
197
+ feed
198
+ end
199
+ end
200
+ end