atom-tools 0.9.0 → 0.9.1
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/README +3 -3
- data/Rakefile +1 -1
- data/bin/atom-client.rb +13 -10
- data/lib/atom/collection.rb +2 -2
- data/lib/atom/element.rb +5 -1
- data/lib/atom/entry.rb +9 -2
- data/lib/atom/feed.rb +11 -6
- data/lib/atom/http.rb +157 -38
- data/lib/atom/service.rb +170 -0
- data/lib/atom/text.rb +15 -2
- data/lib/atom/xml.rb +1 -1
- data/test/conformance/updated.rb +2 -1
- data/test/test_constructs.rb +45 -2
- data/test/test_feed.rb +27 -0
- data/test/test_http.rb +116 -20
- data/test/test_protocol.rb +77 -13
- data/test/test_xml.rb +15 -1
- metadata +3 -38
- data/bin/atom-server.rb~ +0 -71
- data/doc/classes/Atom/App.html +0 -217
- data/doc/classes/Atom/Author.html +0 -130
- data/doc/classes/Atom/Category.html +0 -128
- data/doc/classes/Atom/Collection.html +0 -322
- data/doc/classes/Atom/Content.html +0 -129
- data/doc/classes/Atom/Contributor.html +0 -119
- data/doc/classes/Atom/Element.html +0 -325
- data/doc/classes/Atom/Entry.html +0 -365
- data/doc/classes/Atom/Feed.html +0 -585
- data/doc/classes/Atom/HTTP.html +0 -374
- data/doc/classes/Atom/Link.html +0 -137
- data/doc/classes/Atom/Text.html +0 -229
- data/doc/classes/XHTML.html +0 -118
- data/doc/created.rid +0 -1
- data/doc/files/README.html +0 -213
- data/doc/files/lib/atom/app_rb.html +0 -110
- data/doc/files/lib/atom/collection_rb.html +0 -110
- data/doc/files/lib/atom/element_rb.html +0 -109
- data/doc/files/lib/atom/entry_rb.html +0 -111
- data/doc/files/lib/atom/feed_rb.html +0 -112
- data/doc/files/lib/atom/http_rb.html +0 -109
- data/doc/files/lib/atom/text_rb.html +0 -108
- data/doc/files/lib/atom/xml_rb.html +0 -110
- data/doc/files/lib/atom/yaml_rb.html +0 -109
- data/doc/fr_class_index.html +0 -39
- data/doc/fr_file_index.html +0 -36
- data/doc/fr_method_index.html +0 -62
- data/doc/index.html +0 -24
- data/doc/rdoc-style.css +0 -208
- data/lib/atom/app.rb +0 -87
data/lib/atom/service.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
require "atom/http"
|
4
|
+
require "atom/element"
|
5
|
+
require "atom/collection"
|
6
|
+
|
7
|
+
module Atom
|
8
|
+
PP_NS = "http://purl.org/atom/app#"
|
9
|
+
|
10
|
+
class WrongNamespace < RuntimeError #:nodoc:
|
11
|
+
end
|
12
|
+
class WrongMimetype < RuntimeError # :nodoc:
|
13
|
+
end
|
14
|
+
class WrongResponse < RuntimeError # :nodoc:
|
15
|
+
end
|
16
|
+
|
17
|
+
# an Atom::Workspace has a #title (Atom::Text) and #collections, an Array of Atom::Collection s
|
18
|
+
class Workspace < Atom::Element
|
19
|
+
element :collections, Atom::Multiple(Atom::Collection)
|
20
|
+
element :title, Atom::Text
|
21
|
+
|
22
|
+
def self.parse(xml, base = "", http = Atom::HTTP.new) # :nodoc:
|
23
|
+
ws = Atom::Workspace.new("workspace")
|
24
|
+
|
25
|
+
rxml = if xml.is_a? REXML::Document
|
26
|
+
xml.root
|
27
|
+
elsif xml.is_a? REXML::Element
|
28
|
+
xml
|
29
|
+
else
|
30
|
+
REXML::Document.new(xml)
|
31
|
+
end
|
32
|
+
|
33
|
+
xml.fill_text_construct(ws, "title")
|
34
|
+
|
35
|
+
REXML::XPath.match( rxml,
|
36
|
+
"./app:collection",
|
37
|
+
{"app" => Atom::PP_NS} ).each do |col_el|
|
38
|
+
# absolutize relative URLs
|
39
|
+
url = base.to_uri + col_el.attributes["href"].to_uri
|
40
|
+
|
41
|
+
coll = Atom::Collection.new(url, http)
|
42
|
+
|
43
|
+
# XXX this is a Text Construct, and should be parsed as such
|
44
|
+
col_el.fill_text_construct(coll, "title")
|
45
|
+
|
46
|
+
accepts = REXML::XPath.first( col_el,
|
47
|
+
"./app:accept",
|
48
|
+
{"app" => Atom::PP_NS} )
|
49
|
+
coll.accepts = (accepts ? accepts.text : "entry")
|
50
|
+
|
51
|
+
ws.collections << coll
|
52
|
+
end
|
53
|
+
|
54
|
+
ws
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_element # :nodoc:
|
58
|
+
root = REXML::Element.new "workspace"
|
59
|
+
|
60
|
+
# damn you, REXML. Damn you and you bizarre handling of namespaces
|
61
|
+
title = self.title.to_element
|
62
|
+
title.name = "atom:title"
|
63
|
+
root << title
|
64
|
+
|
65
|
+
self.collections.each do |coll|
|
66
|
+
el = REXML::Element.new "collection"
|
67
|
+
|
68
|
+
el.attributes["href"] = coll.uri
|
69
|
+
|
70
|
+
title = coll.title.to_element
|
71
|
+
title.name = "atom:title"
|
72
|
+
el << title
|
73
|
+
|
74
|
+
unless coll.accepts.nil?
|
75
|
+
accepts = REXML::Element.new "accepts"
|
76
|
+
accepts.text = coll.accepts
|
77
|
+
el << accepts
|
78
|
+
end
|
79
|
+
|
80
|
+
root << el
|
81
|
+
end
|
82
|
+
|
83
|
+
root
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Atom::Service represents an Atom Publishing Protocol service
|
88
|
+
# document. Its only child is #workspaces, which is an Array of
|
89
|
+
# Atom::Workspace s
|
90
|
+
class Service < Atom::Element
|
91
|
+
element :workspaces, Atom::Multiple(Atom::Workspace)
|
92
|
+
|
93
|
+
# retrieves and parses an Atom service document.
|
94
|
+
def initialize(service_url = "", http = Atom::HTTP.new)
|
95
|
+
super("service")
|
96
|
+
|
97
|
+
@http = http
|
98
|
+
|
99
|
+
return if service_url.empty?
|
100
|
+
|
101
|
+
base = URI.parse(service_url)
|
102
|
+
|
103
|
+
rxml = nil
|
104
|
+
|
105
|
+
res = @http.get(base)
|
106
|
+
|
107
|
+
unless res.code == "200" # XXX needs to handle redirects, &c.
|
108
|
+
raise WrongResponse, "service document URL responded with unexpected code #{res.code}"
|
109
|
+
end
|
110
|
+
|
111
|
+
unless res.content_type == "application/atomserv+xml"
|
112
|
+
raise WrongMimetype, "this isn't an atom service document!"
|
113
|
+
end
|
114
|
+
|
115
|
+
parse(res.body, base)
|
116
|
+
end
|
117
|
+
|
118
|
+
# parse a service document, adding its workspaces to this object
|
119
|
+
def parse xml, base = ""
|
120
|
+
rxml = if xml.is_a? REXML::Document
|
121
|
+
xml.root
|
122
|
+
elsif xml.is_a? REXML::Element
|
123
|
+
xml
|
124
|
+
else
|
125
|
+
REXML::Document.new(xml)
|
126
|
+
end
|
127
|
+
|
128
|
+
unless rxml.root.namespace == PP_NS
|
129
|
+
raise WrongNamespace, "this isn't an atom service document!"
|
130
|
+
end
|
131
|
+
|
132
|
+
REXML::XPath.match( rxml, "/app:service/app:workspace", {"app" => Atom::PP_NS} ).each do |ws_el|
|
133
|
+
self.workspaces << Atom::Workspace.parse(ws_el, base, @http)
|
134
|
+
end
|
135
|
+
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
# serialize to a (namespaced) REXML::Document
|
140
|
+
def to_xml
|
141
|
+
doc = REXML::Document.new
|
142
|
+
|
143
|
+
root = REXML::Element.new "service"
|
144
|
+
root.add_namespace Atom::PP_NS
|
145
|
+
root.add_namespace "atom", Atom::NS
|
146
|
+
|
147
|
+
self.workspaces.each do |ws|
|
148
|
+
root << ws.to_element
|
149
|
+
end
|
150
|
+
|
151
|
+
doc << root
|
152
|
+
doc
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Entry
|
157
|
+
# the @href of an entry's link[@rel="edit"]
|
158
|
+
def edit_url
|
159
|
+
begin
|
160
|
+
edit_link = self.links.find do |link|
|
161
|
+
link["rel"] == "edit"
|
162
|
+
end
|
163
|
+
|
164
|
+
edit_link["href"]
|
165
|
+
rescue
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/lib/atom/text.rb
CHANGED
@@ -37,14 +37,27 @@ module Atom
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
#
|
40
|
+
# attempts to parse the content of this element as XML and return it
|
41
|
+
# as an array of REXML::Elements.
|
42
|
+
#
|
43
|
+
# If this self["type"] is "html" and Hpricot is installed, it will
|
44
|
+
# be converted to XHTML first.
|
41
45
|
def xml
|
42
46
|
if self["type"] == "xhtml"
|
43
47
|
@content.children
|
44
48
|
elsif self["type"] == "text"
|
45
49
|
[self.to_s]
|
50
|
+
elsif self["type"] == "html"
|
51
|
+
begin
|
52
|
+
require "hpricot"
|
53
|
+
rescue
|
54
|
+
raise "Turning HTML content into XML requires Hpricot."
|
55
|
+
end
|
56
|
+
|
57
|
+
fixed = Hpricot(self.to_s, :xhtml_strict => true)
|
58
|
+
REXML::Document.new("<div>#{fixed}</div>").root.children
|
46
59
|
else
|
47
|
-
# XXX
|
60
|
+
# XXX check that @type is an XML mimetype and parse it
|
48
61
|
raise "I haven't implemented this yet"
|
49
62
|
end
|
50
63
|
end
|
data/lib/atom/xml.rb
CHANGED
@@ -105,7 +105,7 @@ module REXML # :nodoc: all
|
|
105
105
|
thing.class.attrs.each do |name,req|
|
106
106
|
value = elem.ns_attr name.to_s
|
107
107
|
if value and name == :href
|
108
|
-
thing[name.to_s] = (
|
108
|
+
thing[name.to_s] = (top.base.to_uri + value).to_s
|
109
109
|
elsif value
|
110
110
|
thing[name.to_s] = value
|
111
111
|
end
|
data/test/conformance/updated.rb
CHANGED
@@ -16,7 +16,8 @@ class TestUpdatedConformance < Test::Unit::TestCase
|
|
16
16
|
|
17
17
|
# this is an insignificant change,
|
18
18
|
# (ie. atom:updated_1 == atom:updated_2),
|
19
|
-
#
|
19
|
+
#
|
20
|
+
# the update is applied, your application can handle that however it wants.
|
20
21
|
feed.update!
|
21
22
|
assert_equal "12 of 13 miner<b>s</b> survive mine collapse", feed.entries.first.content.to_s.strip
|
22
23
|
|
data/test/test_constructs.rb
CHANGED
@@ -2,11 +2,54 @@ require "test/unit"
|
|
2
2
|
require "atom/entry"
|
3
3
|
|
4
4
|
class ConstructTest < Test::Unit::TestCase
|
5
|
+
def test_text_construct_html_to_xml
|
6
|
+
begin
|
7
|
+
require "hpricot"
|
8
|
+
rescue
|
9
|
+
# hpricot isn't installed, just skip this test
|
10
|
+
puts "skipping hpricot tests"
|
11
|
+
return
|
12
|
+
end
|
13
|
+
|
14
|
+
entry = Atom::Entry.new
|
15
|
+
|
16
|
+
html = <<END
|
17
|
+
<p>Paragraph 1 contains <a href=http://example.org/>a link
|
18
|
+
<p>This really is a horrendous mess.
|
19
|
+
END
|
20
|
+
|
21
|
+
entry.content = html
|
22
|
+
entry.content["type"] = "html"
|
23
|
+
|
24
|
+
xhtml = entry.content.xml
|
25
|
+
|
26
|
+
# Hpricot is imperfect; for now I'll just test that it's parseable
|
27
|
+
assert_instance_of Array, xhtml
|
28
|
+
assert_instance_of REXML::Element, xhtml.first
|
29
|
+
|
30
|
+
=begin
|
31
|
+
assert_equal 2, xhtml.length
|
32
|
+
|
33
|
+
first = xhtml.first
|
34
|
+
assert_equal "p", first.name
|
35
|
+
assert_equal 2, first.children.length
|
36
|
+
|
37
|
+
a = first.children.last
|
38
|
+
assert_equal "a", a.name
|
39
|
+
assert_equal "http://example.org/", a.attributes["href"]
|
40
|
+
assert_equal "a link", a.text
|
41
|
+
|
42
|
+
last = xhtml.last
|
43
|
+
assert_equal "p", last.name
|
44
|
+
assert_equal "This really is a horrendous mess.", last.text
|
45
|
+
=end
|
46
|
+
end
|
47
|
+
|
5
48
|
def test_text_construct_text
|
6
49
|
entry = Atom::Entry.new
|
7
50
|
|
8
|
-
assert_nil
|
9
|
-
assert_equal
|
51
|
+
assert_nil entry.title
|
52
|
+
assert_equal "", entry.title.to_s
|
10
53
|
|
11
54
|
entry.title = "<3"
|
12
55
|
|
data/test/test_feed.rb
CHANGED
@@ -81,6 +81,32 @@ END
|
|
81
81
|
assert_equal 1, feed.entries.length
|
82
82
|
end
|
83
83
|
|
84
|
+
def test_media_types
|
85
|
+
c = proc do |c_t|
|
86
|
+
@s.mount_proc("/") do |req,res|
|
87
|
+
res.content_type = c_t
|
88
|
+
res.body = @test_feed
|
89
|
+
|
90
|
+
@s.stop
|
91
|
+
end
|
92
|
+
one_shot
|
93
|
+
end
|
94
|
+
|
95
|
+
feed = Atom::Feed.new "http://localhost:#{@port}/"
|
96
|
+
|
97
|
+
# even if it looks like a feed, the server's word is law
|
98
|
+
c.call("text/plain")
|
99
|
+
assert_raise(Atom::HTTPException) { feed.update! }
|
100
|
+
|
101
|
+
# text/xml isn't the preferred mimetype, but we'll accept it
|
102
|
+
c.call("text/xml")
|
103
|
+
assert_nothing_raised { feed.update! }
|
104
|
+
|
105
|
+
# same goes for application/xml
|
106
|
+
# XXX c.call("application/xml")
|
107
|
+
# assert_nothing_raised { feed.update! }
|
108
|
+
end
|
109
|
+
|
84
110
|
def test_conditional_get
|
85
111
|
@s.mount_proc("/") do |req,res|
|
86
112
|
assert_nil req["If-None-Match"]
|
@@ -124,5 +150,6 @@ END
|
|
124
150
|
assert_equal 1, feed.entries.length
|
125
151
|
end
|
126
152
|
|
153
|
+
# prepares the server for a single request
|
127
154
|
def one_shot; Thread.new { @s.start }; end
|
128
155
|
end
|
data/test/test_http.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
|
1
3
|
require "atom/http"
|
2
4
|
require "webrick"
|
3
5
|
|
4
6
|
class AtomProtocolTest < Test::Unit::TestCase
|
7
|
+
REALM = "test authentication"
|
8
|
+
USER = "test_user"
|
9
|
+
PASS = "aoeuaoeu"
|
10
|
+
SECRET_DATA = "I kissed a boy once"
|
11
|
+
|
5
12
|
def setup
|
6
13
|
@http = Atom::HTTP.new
|
7
14
|
@port = rand(1024) + 1024
|
@@ -11,13 +18,21 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
11
18
|
end
|
12
19
|
|
13
20
|
def test_parse_wwwauth
|
14
|
-
header = '
|
21
|
+
header = 'realm="SokEvo"'
|
15
22
|
|
16
|
-
|
17
|
-
|
23
|
+
params = @http.send :parse_quoted_wwwauth, header
|
24
|
+
assert_equal "SokEvo", params[:realm]
|
25
|
+
|
26
|
+
header = 'opaque="07UrfUiCYac5BbWJ", algorithm=MD5-sess, qop="auth", stale=TRUE, nonce="MDAx0Mzk", realm="test authentication"'
|
18
27
|
|
19
|
-
|
20
|
-
|
28
|
+
params = @http.send :parse_wwwauth_digest, header
|
29
|
+
|
30
|
+
assert_equal "test authentication", params[:realm]
|
31
|
+
assert_equal "MDAx0Mzk", params[:nonce]
|
32
|
+
assert_equal true, params[:stale]
|
33
|
+
assert_equal "auth", params[:qop]
|
34
|
+
assert_equal "MD5-sess", params[:algorithm]
|
35
|
+
assert_equal "07UrfUiCYac5BbWJ", params[:opaque]
|
21
36
|
end
|
22
37
|
|
23
38
|
def test_GET
|
@@ -32,11 +47,11 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
32
47
|
|
33
48
|
one_shot
|
34
49
|
|
35
|
-
|
50
|
+
get_root
|
36
51
|
|
37
|
-
assert_equal("200", res.code)
|
38
|
-
assert_equal("text/plain", res.content_type)
|
39
|
-
assert_equal("just junk", res.body)
|
52
|
+
assert_equal("200", @res.code)
|
53
|
+
assert_equal("text/plain", @res.content_type)
|
54
|
+
assert_equal("just junk", @res.body)
|
40
55
|
end
|
41
56
|
|
42
57
|
def test_GET_headers
|
@@ -48,39 +63,120 @@ class AtomProtocolTest < Test::Unit::TestCase
|
|
48
63
|
|
49
64
|
one_shot
|
50
65
|
|
51
|
-
|
66
|
+
get_root("User-Agent" => "tester agent")
|
52
67
|
|
53
|
-
assert_equal("200", res.code)
|
68
|
+
assert_equal("200", @res.code)
|
54
69
|
end
|
55
70
|
|
56
71
|
def test_basic_auth
|
57
72
|
@s.mount_proc("/") do |req,res|
|
58
|
-
WEBrick::HTTPAuth.basic_auth(req, res,
|
59
|
-
u ==
|
73
|
+
WEBrick::HTTPAuth.basic_auth(req, res, REALM) do |u,p|
|
74
|
+
u == USER and p == PASS
|
60
75
|
end
|
61
76
|
|
62
|
-
res.body =
|
77
|
+
res.body = SECRET_DATA
|
63
78
|
@s.stop
|
64
79
|
end
|
65
80
|
|
66
81
|
one_shot
|
67
|
-
|
82
|
+
|
83
|
+
# with no credentials
|
84
|
+
assert_raises(Atom::Unauthorized) { get_root }
|
85
|
+
|
86
|
+
@http.user = USER
|
87
|
+
@http.pass = "incorrect_password"
|
88
|
+
|
89
|
+
# with incorrect credentials
|
68
90
|
assert_raises(Atom::Unauthorized) { get_root }
|
69
91
|
|
70
92
|
@http.when_auth do |abs_url,realm|
|
71
93
|
assert_equal "http://localhost:#{@port}/", abs_url
|
72
|
-
assert_equal
|
94
|
+
assert_equal REALM, realm
|
73
95
|
|
74
|
-
[
|
96
|
+
[USER, PASS]
|
75
97
|
end
|
76
98
|
|
77
99
|
one_shot
|
78
100
|
|
101
|
+
get_root
|
102
|
+
assert_equal "200", @res.code
|
103
|
+
assert_equal SECRET_DATA, @res.body
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_digest_auth
|
107
|
+
# a dummy userdb
|
108
|
+
userdb = {}
|
109
|
+
userdb[USER] = PASS
|
110
|
+
|
111
|
+
def userdb.get_passwd(realm, user, reload)
|
112
|
+
Digest::MD5::hexdigest([user, realm, self[user]].join(":"))
|
113
|
+
end
|
114
|
+
|
115
|
+
authenticator = WEBrick::HTTPAuth::DigestAuth.new(
|
116
|
+
:UserDB => userdb,
|
117
|
+
:Realm => REALM,
|
118
|
+
:Algorithm => "MD5"
|
119
|
+
)
|
120
|
+
|
121
|
+
@s.mount_proc("/") do |req,res|
|
122
|
+
authenticator.authenticate(req, res)
|
123
|
+
res.body = SECRET_DATA
|
124
|
+
end
|
125
|
+
|
126
|
+
one_shot
|
127
|
+
|
128
|
+
# no credentials
|
129
|
+
assert_raises(Atom::Unauthorized) { get_root }
|
130
|
+
|
131
|
+
@http.user = USER
|
132
|
+
@http.pass = PASS
|
133
|
+
|
134
|
+
# correct credentials
|
79
135
|
res = get_root
|
80
|
-
assert_equal
|
81
|
-
|
136
|
+
assert_equal SECRET_DATA, res.body
|
137
|
+
|
138
|
+
@s.stop
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_wsse_auth
|
142
|
+
@s.mount_proc("/") do |req,res|
|
143
|
+
assert_equal 'WSSE profile="UsernameToken"', req["Authorization"]
|
144
|
+
|
145
|
+
xwsse = req["X-WSSE"]
|
146
|
+
|
147
|
+
p = @http.send :parse_quoted_wwwauth, xwsse
|
148
|
+
|
149
|
+
assert_equal USER, p[:Username]
|
150
|
+
assert_match /^UsernameToken /, xwsse
|
151
|
+
|
152
|
+
# un-base64 in preparation for SHA1-ing
|
153
|
+
nonce = p[:Nonce].unpack("m").first
|
154
|
+
|
155
|
+
# Base64( SHA1( Nonce + CreationTimestamp + Password ) )
|
156
|
+
pd_string = nonce + p[:Created] + PASS
|
157
|
+
password_digest = [Digest::SHA1.digest(pd_string)].pack("m").chomp
|
158
|
+
|
159
|
+
assert_equal password_digest, p[:PasswordDigest]
|
160
|
+
|
161
|
+
res.body = SECRET_DATA
|
162
|
+
@s.stop
|
163
|
+
end
|
164
|
+
|
165
|
+
one_shot
|
166
|
+
|
167
|
+
@http.always_auth = :wsse
|
168
|
+
@http.user = USER
|
169
|
+
@http.pass = PASS
|
170
|
+
|
171
|
+
get_root
|
172
|
+
|
173
|
+
assert_equal "200", @res.code
|
174
|
+
assert_equal SECRET_DATA, @res.body
|
175
|
+
end
|
176
|
+
|
177
|
+
def get_root(*args)
|
178
|
+
@res = @http.get("http://localhost:#{@port}/", *args)
|
82
179
|
end
|
83
180
|
|
84
|
-
def get_root(*args); @http.get("http://localhost:#{@port}/", *args); end
|
85
181
|
def one_shot; Thread.new { @s.start }; end
|
86
182
|
end
|