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.
- data/COPYING +18 -0
- data/README +103 -0
- data/Rakefile +77 -0
- data/bin/atom-client.rb +246 -0
- data/bin/atom-server.rb~ +71 -0
- data/doc/classes/Atom/App.html +217 -0
- data/doc/classes/Atom/Author.html +130 -0
- data/doc/classes/Atom/Category.html +128 -0
- data/doc/classes/Atom/Collection.html +322 -0
- data/doc/classes/Atom/Content.html +129 -0
- data/doc/classes/Atom/Contributor.html +119 -0
- data/doc/classes/Atom/Element.html +325 -0
- data/doc/classes/Atom/Entry.html +365 -0
- data/doc/classes/Atom/Feed.html +585 -0
- data/doc/classes/Atom/HTTP.html +374 -0
- data/doc/classes/Atom/Link.html +137 -0
- data/doc/classes/Atom/Text.html +229 -0
- data/doc/classes/XHTML.html +118 -0
- data/doc/created.rid +1 -0
- data/doc/files/README.html +213 -0
- data/doc/files/lib/atom/app_rb.html +110 -0
- data/doc/files/lib/atom/collection_rb.html +110 -0
- data/doc/files/lib/atom/element_rb.html +109 -0
- data/doc/files/lib/atom/entry_rb.html +111 -0
- data/doc/files/lib/atom/feed_rb.html +112 -0
- data/doc/files/lib/atom/http_rb.html +109 -0
- data/doc/files/lib/atom/text_rb.html +108 -0
- data/doc/files/lib/atom/xml_rb.html +110 -0
- data/doc/files/lib/atom/yaml_rb.html +109 -0
- data/doc/fr_class_index.html +39 -0
- data/doc/fr_file_index.html +36 -0
- data/doc/fr_method_index.html +62 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/atom/app.rb +87 -0
- data/lib/atom/collection.rb +75 -0
- data/lib/atom/element.rb +277 -0
- data/lib/atom/entry.rb +135 -0
- data/lib/atom/feed.rb +229 -0
- data/lib/atom/http.rb +132 -0
- data/lib/atom/text.rb +163 -0
- data/lib/atom/xml.rb +200 -0
- data/lib/atom/yaml.rb +101 -0
- data/setup.rb +1585 -0
- data/test/conformance/order.rb +117 -0
- data/test/conformance/title.rb +108 -0
- data/test/conformance/updated.rb +33 -0
- data/test/conformance/xhtmlcontentdiv.rb +18 -0
- data/test/conformance/xmlnamespace.rb +54 -0
- data/test/runtests.rb +14 -0
- data/test/test_constructs.rb +91 -0
- data/test/test_feed.rb +128 -0
- data/test/test_general.rb +99 -0
- data/test/test_http.rb +86 -0
- data/test/test_protocol.rb +69 -0
- data/test/test_xml.rb +353 -0
- 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
|