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