atom-tools 0.9.0

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