atom-tools 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +3 -3
- data/README +4 -44
- data/Rakefile +9 -2
- data/bin/atom-cp +159 -0
- data/bin/atom-grep +78 -0
- data/bin/atom-post +72 -0
- data/bin/atom-purge +82 -0
- data/lib/atom/cache.rb +178 -0
- data/lib/atom/collection.rb +77 -17
- data/lib/atom/element.rb +520 -166
- data/lib/atom/entry.rb +82 -142
- data/lib/atom/feed.rb +48 -66
- data/lib/atom/http.rb +115 -35
- data/lib/atom/service.rb +56 -113
- data/lib/atom/text.rb +79 -63
- data/lib/atom/tools.rb +163 -0
- data/test/conformance/order.rb +11 -10
- data/test/conformance/title.rb +9 -9
- data/test/test_constructs.rb +23 -10
- data/test/test_feed.rb +0 -44
- data/test/test_general.rb +0 -40
- data/test/test_http.rb +18 -0
- data/test/test_protocol.rb +60 -22
- data/test/test_xml.rb +73 -41
- metadata +47 -37
- data/bin/atom-client.rb +0 -275
- data/lib/atom/xml.rb +0 -213
- data/lib/atom/yaml.rb +0 -116
data/lib/atom/cache.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# portions of this ported from httplib2 <http://code.google.com/p/httplib2/>
|
2
|
+
# copyright 2006, Joe Gregorio
|
3
|
+
#
|
4
|
+
# used under the terms of the MIT license
|
5
|
+
|
6
|
+
require "md5"
|
7
|
+
|
8
|
+
def normalize_header_names _headers
|
9
|
+
headers = {}
|
10
|
+
_headers.each { |k,v| headers[k.downcase] = v }
|
11
|
+
headers
|
12
|
+
end
|
13
|
+
|
14
|
+
def _parse_cache_control headers
|
15
|
+
retval = {}
|
16
|
+
headers = normalize_header_names(headers) if headers.is_a? Hash
|
17
|
+
|
18
|
+
if headers['cache-control']
|
19
|
+
parts = headers['cache-control'].split(',')
|
20
|
+
parts.each do |part|
|
21
|
+
if part.match(/=/)
|
22
|
+
k, v = part.split('=').map { |p| p.strip }
|
23
|
+
retval[k] = v
|
24
|
+
else
|
25
|
+
retval[part.strip] = 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
retval
|
31
|
+
end
|
32
|
+
|
33
|
+
def _updateCache request_headers, response, cache, cachekey
|
34
|
+
cc = _parse_cache_control request_headers
|
35
|
+
cc_response = _parse_cache_control response
|
36
|
+
if cc['no-store'] or cc_response['no-store']
|
37
|
+
cache.delete cachekey
|
38
|
+
else
|
39
|
+
result = "HTTP/#{response.http_version} #{response.code} #{response.message}\r\n"
|
40
|
+
|
41
|
+
response.each_capitalized_name do |field|
|
42
|
+
next if ['status', 'content-encoding', 'transfer-encoding'].member? field.downcase
|
43
|
+
response.get_fields(field).each do |value|
|
44
|
+
result += "#{field}: #{value}\r\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
cache[cachekey] = result + "\r\n" + response.body
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
=begin
|
53
|
+
Determine freshness from the Date, Expires and Cache-Control headers.
|
54
|
+
|
55
|
+
We don't handle the following:
|
56
|
+
|
57
|
+
1. Cache-Control: max-stale
|
58
|
+
2. Age: headers are not used in the calculations.
|
59
|
+
|
60
|
+
Not that this algorithm is simpler than you might think
|
61
|
+
because we are operating as a private (non-shared) cache.
|
62
|
+
This lets us ignore 's-maxage'. We can also ignore
|
63
|
+
'proxy-invalidate' since we aren't a proxy.
|
64
|
+
We will never return a stale document as
|
65
|
+
fresh as a design decision, and thus the non-implementation
|
66
|
+
of 'max-stale'. This also lets us safely ignore 'must-revalidate'
|
67
|
+
since we operate as if every server has sent 'must-revalidate'.
|
68
|
+
Since we are private we get to ignore both 'public' and
|
69
|
+
'private' parameters. We also ignore 'no-transform' since
|
70
|
+
we don't do any transformations.
|
71
|
+
The 'no-store' parameter is handled at a higher level.
|
72
|
+
So the only Cache-Control parameters we look at are:
|
73
|
+
|
74
|
+
no-cache
|
75
|
+
only-if-cached
|
76
|
+
max-age
|
77
|
+
min-fresh
|
78
|
+
=end
|
79
|
+
def _entry_disposition(response_headers, request_headers)
|
80
|
+
request_headers = normalize_header_names(request_headers)
|
81
|
+
|
82
|
+
cc = _parse_cache_control(request_headers)
|
83
|
+
cc_response = _parse_cache_control(response_headers)
|
84
|
+
|
85
|
+
if request_headers['pragma'] and request_headers['pragma'].downcase.match(/no-cache/)
|
86
|
+
unless request_headers.key? 'cache-control'
|
87
|
+
request_headers['cache-control'] = 'no-cache'
|
88
|
+
end
|
89
|
+
:TRANSPARENT
|
90
|
+
elsif cc.key? 'no-cache'
|
91
|
+
:TRANSPARENT
|
92
|
+
elsif cc_response.key? 'no-cache'
|
93
|
+
:STALE
|
94
|
+
elsif cc.key? 'only-if-cached'
|
95
|
+
:FRESH
|
96
|
+
elsif response_headers.key? 'date'
|
97
|
+
date = Time.rfc2822(response_headers['date'])
|
98
|
+
diff = Time.now - date
|
99
|
+
current_age = (diff > 0) ? diff : 0
|
100
|
+
if cc_response.key? 'max-age'
|
101
|
+
freshness_lifetime = cc_response['max-age'].to_i
|
102
|
+
elsif response_headers.key? 'expires'
|
103
|
+
expires = Time.rfc2822(response_headers['expires'])
|
104
|
+
diff = expires - date
|
105
|
+
freshness_lifetime = (diff > 0) ? diff : 0
|
106
|
+
else
|
107
|
+
freshness_lifetime = 0
|
108
|
+
end
|
109
|
+
|
110
|
+
if cc.key? 'max-age'
|
111
|
+
freshness_lifetime = cc['max-age'].to_i
|
112
|
+
end
|
113
|
+
|
114
|
+
if cc.key? 'min-fresh'
|
115
|
+
min_fresh = cc['min-fresh'].to_i
|
116
|
+
current_age += min_fresh
|
117
|
+
end
|
118
|
+
|
119
|
+
if freshness_lifetime > current_age
|
120
|
+
:FRESH
|
121
|
+
else
|
122
|
+
:STALE
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module Atom
|
128
|
+
# this cache never actually saves anything
|
129
|
+
class NilCache
|
130
|
+
def [] key
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def []= key, value
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def delete key
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# uses a local directory to store cache files
|
144
|
+
class FileCache
|
145
|
+
def initialize dir
|
146
|
+
@dir = dir
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_file(key)
|
150
|
+
@dir + "/" + self.safe(key)
|
151
|
+
end
|
152
|
+
|
153
|
+
# turns a URL into a safe filename
|
154
|
+
def safe filename
|
155
|
+
filemd5 = MD5.hexdigest(filename)
|
156
|
+
filename = filename.sub(/^\w+:\/\//, '')
|
157
|
+
filename = filename.gsub(/[?\/:|]+/, ',')
|
158
|
+
|
159
|
+
filename + "," + filemd5
|
160
|
+
end
|
161
|
+
|
162
|
+
def [] key
|
163
|
+
File.read(self.to_file(key))
|
164
|
+
rescue Errno::ENOENT
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
def []= key, value
|
169
|
+
File.open(self.to_file(key), 'w') do |f|
|
170
|
+
f.write(value)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def delete key
|
175
|
+
File.delete(self.to_file(key))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/atom/collection.rb
CHANGED
@@ -1,20 +1,80 @@
|
|
1
1
|
require "atom/http"
|
2
2
|
require "atom/feed"
|
3
3
|
|
4
|
-
# so we can do some mimetype guessing
|
5
|
-
require "webrick/httputils"
|
6
|
-
|
7
4
|
module Atom
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
5
|
+
class Categories < Atom::Element
|
6
|
+
is_element PP_NS, 'categories'
|
7
|
+
|
8
|
+
atom_elements :category, :list, Atom::Category
|
9
|
+
|
10
|
+
attrb ['app', PP_NS], :scheme
|
11
|
+
attrb ['app', PP_NS], :href
|
12
|
+
|
13
|
+
def scheme= s
|
14
|
+
list.each do |cat|
|
15
|
+
unless cat.scheme
|
16
|
+
cat.scheme = s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# 'fixed' attribute parsing/building
|
22
|
+
attr_accessor :fixed
|
23
|
+
|
24
|
+
on_parse_attr [PP_NS, :fixed] do |e,x|
|
25
|
+
e.set(:fixed, x == 'yes')
|
26
|
+
end
|
27
|
+
|
28
|
+
on_build do |e,x|
|
29
|
+
if e.get(:fixed)
|
30
|
+
e.attributes['fixed'] = 'yes'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Collection < Atom::Element
|
36
|
+
is_element PP_NS, 'collection'
|
37
|
+
|
38
|
+
strings ['app', PP_NS], :accept, :accepts
|
39
|
+
attrb ['app', PP_NS], :href
|
40
|
+
|
41
|
+
def_set :href do |href|
|
42
|
+
@href = href
|
43
|
+
@feed = Atom::Feed.new @href, @http
|
44
|
+
end
|
15
45
|
|
16
|
-
|
17
|
-
|
46
|
+
atom_element :title, Atom::Title
|
47
|
+
|
48
|
+
elements ['app', PP_NS], :categories, :categories, Atom::Categories
|
49
|
+
|
50
|
+
def title
|
51
|
+
@title or @feed.title
|
52
|
+
end
|
53
|
+
|
54
|
+
def accepts
|
55
|
+
if @accepts.empty?
|
56
|
+
['application/atom+xml;type=entry']
|
57
|
+
else
|
58
|
+
@accepts
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def accepts= array
|
63
|
+
@accepts = array
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :http
|
67
|
+
|
68
|
+
attr_reader :feed
|
69
|
+
|
70
|
+
def initialize(href = nil, http = Atom::HTTP.new)
|
71
|
+
super()
|
72
|
+
|
73
|
+
if href
|
74
|
+
self.href = href
|
75
|
+
end
|
76
|
+
|
77
|
+
@http = http
|
18
78
|
end
|
19
79
|
|
20
80
|
# POST an entry to the collection, with an optional slug
|
@@ -22,10 +82,10 @@ module Atom
|
|
22
82
|
raise "Cowardly refusing to POST a non-Atom::Entry" unless entry.is_a? Atom::Entry
|
23
83
|
headers = {"Content-Type" => "application/atom+xml" }
|
24
84
|
headers["Slug"] = slug if slug
|
25
|
-
|
26
|
-
@http.post(@
|
85
|
+
|
86
|
+
@http.post(@href, entry.to_s, headers)
|
27
87
|
end
|
28
|
-
|
88
|
+
|
29
89
|
# PUT an updated version of an entry to the collection
|
30
90
|
def put!(entry, url = entry.edit_url)
|
31
91
|
@http.put_atom_entry(entry, url)
|
@@ -40,8 +100,8 @@ module Atom
|
|
40
100
|
def post_media!(data, content_type, slug = nil)
|
41
101
|
headers = {"Content-Type" => content_type}
|
42
102
|
headers["Slug"] = slug if slug
|
43
|
-
|
44
|
-
@http.post(@
|
103
|
+
|
104
|
+
@http.post(@href, data, headers)
|
45
105
|
end
|
46
106
|
|
47
107
|
# PUT a media item to the collection
|
data/lib/atom/element.rb
CHANGED
@@ -1,44 +1,56 @@
|
|
1
1
|
require "time"
|
2
2
|
require "rexml/element"
|
3
3
|
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module URI # :nodoc: all
|
7
|
+
class Generic; def to_uri; self; end; end
|
8
|
+
end
|
9
|
+
|
10
|
+
class String # :nodoc:
|
11
|
+
def to_uri; URI.parse(self); end
|
12
|
+
end
|
13
|
+
|
14
|
+
# cribbed from metaid.rb
|
15
|
+
class Object
|
16
|
+
# The hidden singleton lurks behind everyone
|
17
|
+
def metaclass; class << self; self; end; end
|
18
|
+
def meta_eval &blk; metaclass.instance_eval &blk; end
|
19
|
+
|
20
|
+
# Adds methods to a metaclass
|
21
|
+
def meta_def name, &blk
|
22
|
+
meta_eval { define_method name, &blk }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
4
26
|
module Atom # :nodoc:
|
5
|
-
|
6
|
-
|
7
|
-
return if date.nil?
|
27
|
+
NS = "http://www.w3.org/2005/Atom"
|
28
|
+
PP_NS = "http://www.w3.org/2007/app"
|
8
29
|
|
9
|
-
|
10
|
-
date
|
11
|
-
else
|
12
|
-
Time.parse date.to_s
|
13
|
-
end
|
14
|
-
|
15
|
-
def date.to_s
|
16
|
-
iso8601
|
17
|
-
end
|
30
|
+
class ParseError < StandardError; end
|
18
31
|
|
19
|
-
|
20
|
-
|
32
|
+
module AttrEl
|
33
|
+
# for backwards compatibility
|
34
|
+
def [] k; self.send(k.to_sym); end
|
35
|
+
def []= k, v; self.send("#{k}=".to_sym, v); end
|
21
36
|
end
|
22
|
-
|
37
|
+
|
23
38
|
# ignore the man behind the curtain.
|
24
39
|
def self.Multiple klass
|
25
40
|
Class.new(Array) do
|
26
41
|
@class = klass
|
27
42
|
|
28
|
-
def new
|
29
|
-
item = self.class.holds.new
|
43
|
+
def new *args
|
44
|
+
item = self.class.holds.new *args
|
30
45
|
self << item
|
31
|
-
|
46
|
+
|
32
47
|
item
|
33
48
|
end
|
34
49
|
|
35
50
|
def << item
|
36
51
|
raise ArgumentError, "this can only hold items of class #{self.class.holds}" unless item.is_a? self.class.holds
|
37
|
-
super(item)
|
38
|
-
end
|
39
52
|
|
40
|
-
|
41
|
-
collect do |item| item.to_element end
|
53
|
+
super(item)
|
42
54
|
end
|
43
55
|
|
44
56
|
def self.holds; @class end
|
@@ -47,187 +59,514 @@ module Atom # :nodoc:
|
|
47
59
|
end
|
48
60
|
end
|
49
61
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
module Parsers
|
63
|
+
# adds a parser that calls the given block for a single element that
|
64
|
+
# matches the given name and namespace (if it exists)
|
65
|
+
def on_parse name_pair, &block
|
66
|
+
uri, name = name_pair
|
67
|
+
@on_parse ||= []
|
56
68
|
|
57
|
-
|
58
|
-
|
69
|
+
process = lambda do |e,x|
|
70
|
+
el = e.get_elem(x, uri, name)
|
71
|
+
|
72
|
+
if el
|
73
|
+
block.call e, el
|
74
|
+
|
75
|
+
e.extensions.delete_if do |c|
|
76
|
+
c.namespace == uri and c.name == name.to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
59
80
|
|
60
|
-
|
61
|
-
def self.attrs # :nodoc:
|
62
|
-
@attrs || []
|
81
|
+
@on_parse << process
|
63
82
|
end
|
64
83
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
84
|
+
# adds a parser that calls the given block for the attribute that
|
85
|
+
# matches the given name (if it exists)
|
86
|
+
def on_parse_attr name_pair, &block
|
87
|
+
uri, name = name_pair
|
88
|
+
@on_parse ||= []
|
89
|
+
|
90
|
+
process = lambda do |e,x|
|
91
|
+
x = e.get_atom_attrb(x, name)
|
92
|
+
|
93
|
+
if x
|
94
|
+
block.call e, x
|
95
|
+
|
96
|
+
e.extensions.attributes.delete name.to_s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
@on_parse << process
|
68
101
|
end
|
69
102
|
|
70
|
-
#
|
71
|
-
|
72
|
-
|
103
|
+
# adds a parser that calls the given block for all elements
|
104
|
+
# that match the given name and namespace
|
105
|
+
def on_parse_many name_pair, &block
|
106
|
+
uri, name = name_pair
|
107
|
+
@on_parse ||= []
|
108
|
+
|
109
|
+
process = lambda do |e,x|
|
110
|
+
els = e.get_elems(x, uri, name)
|
111
|
+
|
112
|
+
unless els.empty?
|
113
|
+
block.call e, els
|
114
|
+
|
115
|
+
els.each do |el|
|
116
|
+
e.extensions.delete_if { |c| c.namespace == uri and c.name == name.to_s }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
@on_parse << process
|
73
122
|
end
|
74
123
|
|
75
|
-
#
|
76
|
-
def
|
77
|
-
|
78
|
-
|
124
|
+
# adds a parser that calls the given block for this element
|
125
|
+
def on_parse_root &block
|
126
|
+
@on_parse ||= []
|
127
|
+
|
128
|
+
process = lambda do |e,x|
|
129
|
+
block.call e, x
|
130
|
+
|
131
|
+
x.elements.each do |el|
|
132
|
+
e.extensions.clear
|
133
|
+
end
|
79
134
|
end
|
80
|
-
|
81
|
-
|
135
|
+
|
136
|
+
@on_parse << process
|
137
|
+
end
|
138
|
+
|
139
|
+
# parses the text content of an element named 'name' into an attribute
|
140
|
+
# on this Element named 'name'
|
141
|
+
def parse_plain uri, name
|
142
|
+
self.on_parse [uri, name] do |e,x|
|
143
|
+
e.set(name, x.text)
|
82
144
|
end
|
83
145
|
end
|
146
|
+
end
|
84
147
|
|
85
|
-
|
86
|
-
def
|
148
|
+
module Converters
|
149
|
+
def build_plain ns, name
|
150
|
+
self.on_build do |e,x|
|
151
|
+
if v = e.get(name)
|
152
|
+
el = e.append_elem(x, ns, name)
|
153
|
+
el.text = v.to_s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# an element in the Atom namespace containing text
|
159
|
+
def atom_string(name)
|
160
|
+
attr_accessor name
|
161
|
+
|
162
|
+
self.parse_plain(Atom::NS, name)
|
163
|
+
self.build_plain(['atom', Atom::NS], name)
|
164
|
+
end
|
165
|
+
|
166
|
+
# an element in namespace 'ns' containing a RFC3339 timestamp
|
167
|
+
def time(ns, name)
|
87
168
|
attr_reader name
|
88
169
|
|
89
|
-
|
90
|
-
|
170
|
+
self.def_set name do |time|
|
171
|
+
unless time.respond_to? :iso8601
|
172
|
+
time = Time.parse(time.to_s)
|
173
|
+
end
|
174
|
+
|
175
|
+
def time.to_s; iso8601; end
|
176
|
+
|
177
|
+
instance_variable_set("@#{name}", time)
|
178
|
+
end
|
91
179
|
|
92
|
-
|
93
|
-
|
180
|
+
define_method "#{name}!".to_sym do
|
181
|
+
set(name, Time.now)
|
94
182
|
end
|
183
|
+
|
184
|
+
self.parse_plain(ns[1], name)
|
185
|
+
self.build_plain(ns, name)
|
186
|
+
end
|
187
|
+
|
188
|
+
# an element in the Atom namespace containing a timestamp
|
189
|
+
def atom_time(name)
|
190
|
+
self.time ['atom', Atom::NS], name
|
95
191
|
end
|
96
192
|
|
97
|
-
#
|
98
|
-
def
|
99
|
-
|
193
|
+
# an element that is parsed by Element descendant 'klass'
|
194
|
+
def element(ns, name, klass)
|
195
|
+
el_name = name
|
196
|
+
name = name.to_s.gsub(/-/, '_')
|
197
|
+
|
198
|
+
attr_reader name
|
199
|
+
|
200
|
+
self.on_parse [ns[1], el_name] do |e,x|
|
201
|
+
e.instance_variable_set("@#{name}", klass.parse(x, e.base))
|
202
|
+
end
|
203
|
+
|
204
|
+
self.on_build do |e,x|
|
205
|
+
if v = e.get(name)
|
206
|
+
el = e.append_elem(x, ns, el_name)
|
207
|
+
v.build(el)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def_set name do |value|
|
212
|
+
instance_variable_set("@#{name}", klass.new(value))
|
213
|
+
end
|
214
|
+
end
|
100
215
|
|
101
|
-
|
216
|
+
# an element that is parsed by Element descendant 'klass'
|
217
|
+
def atom_element(name, klass)
|
218
|
+
self.element(['atom', Atom::NS], name, klass)
|
102
219
|
end
|
103
|
-
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
220
|
+
|
221
|
+
# an element that can appear multiple times that contains text
|
222
|
+
#
|
223
|
+
# 'one_name' is the name of the element, 'many_name' is the name of
|
224
|
+
# the attribute that will be created on this Element
|
225
|
+
def strings(ns, one_name, many_name)
|
226
|
+
attr_reader many_name
|
227
|
+
|
228
|
+
self.on_init do
|
229
|
+
instance_variable_set("@#{many_name}", [])
|
230
|
+
end
|
231
|
+
|
232
|
+
self.on_parse_many [ns[1], one_name] do |e,xs|
|
233
|
+
var = e.instance_variable_get("@#{many_name}")
|
234
|
+
|
235
|
+
xs.each do |el|
|
236
|
+
var << el.text
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
self.on_build do |e,x|
|
241
|
+
e.instance_variable_get("@#{many_name}").each do |v|
|
242
|
+
e.append_elem(x, ns, one_name).text = v
|
113
243
|
end
|
114
|
-
|
115
|
-
set(name, i)
|
116
244
|
end
|
117
245
|
end
|
118
246
|
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
247
|
+
# an element that can appear multiple times that is parsed by Element
|
248
|
+
# descendant 'klass'
|
249
|
+
#
|
250
|
+
# 'one_name' is the name of the element, 'many_name' is the name of
|
251
|
+
# the attribute that will be created on this Element
|
252
|
+
def elements(ns, one_name, many_name, klass)
|
253
|
+
attr_reader many_name
|
254
|
+
|
255
|
+
self.on_init do
|
256
|
+
var = Atom::Multiple(klass).new
|
257
|
+
instance_variable_set("@#{many_name}", var)
|
258
|
+
end
|
259
|
+
|
260
|
+
self.on_parse_many [ns[1], one_name] do |e,xs|
|
261
|
+
var = e.get(many_name)
|
262
|
+
|
263
|
+
xs.each do |el|
|
264
|
+
var << klass.parse(el, e.base)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
self.on_build do |e,x|
|
269
|
+
e.get(many_name).each do |v|
|
270
|
+
el = e.append_elem(x, ns, one_name)
|
271
|
+
v.build(el)
|
272
|
+
end
|
273
|
+
end
|
124
274
|
end
|
125
|
-
|
126
|
-
# set the value of an attribute
|
127
|
-
def []= key, value
|
128
|
-
test_key key
|
129
275
|
|
130
|
-
|
276
|
+
# like #elements but in the Atom namespace
|
277
|
+
def atom_elements(one_name, many_name, klass)
|
278
|
+
self.elements(['atom', Atom::NS], one_name, many_name, klass)
|
131
279
|
end
|
132
280
|
|
133
|
-
#
|
134
|
-
def
|
135
|
-
|
136
|
-
|
281
|
+
# an XML attribute in the namespace 'ns'
|
282
|
+
def attrb(ns, name)
|
283
|
+
attr_accessor name
|
284
|
+
|
285
|
+
self.on_parse_attr [ns[1], name] do |e,x|
|
286
|
+
e.set(name, x)
|
287
|
+
end
|
137
288
|
|
138
|
-
self.
|
139
|
-
if
|
140
|
-
|
141
|
-
set(name, kind.new)
|
289
|
+
self.on_build do |e,x|
|
290
|
+
if v = e.get(name)
|
291
|
+
e.set_atom_attrb(x, name, v.to_s)
|
142
292
|
end
|
143
293
|
end
|
144
294
|
end
|
145
295
|
|
146
|
-
#
|
147
|
-
def
|
148
|
-
|
296
|
+
# an XML attribute in the Atom namespace
|
297
|
+
def atom_attrb(name)
|
298
|
+
self.attrb(['atom', Atom::NS], name)
|
149
299
|
end
|
150
|
-
|
151
|
-
# convert to a REXML::Element (with no namespace)
|
152
|
-
def to_element
|
153
|
-
elem = REXML::Element.new(local_name)
|
154
300
|
|
155
|
-
|
156
|
-
|
157
|
-
|
301
|
+
# a type of Atom Link. specifics defined by Hash 'criteria'
|
302
|
+
def atom_link name, criteria
|
303
|
+
def_get name do
|
304
|
+
existing = find_link(criteria)
|
158
305
|
|
159
|
-
|
160
|
-
|
161
|
-
e = [ e ] unless e.is_a? Array
|
306
|
+
existing and existing.href
|
307
|
+
end
|
162
308
|
|
163
|
-
|
164
|
-
|
165
|
-
|
309
|
+
def_set name do |value|
|
310
|
+
existing = find_link(criteria)
|
311
|
+
|
312
|
+
if existing
|
313
|
+
existing.href = value
|
166
314
|
else
|
167
|
-
|
315
|
+
links.new criteria.merge(:href => value)
|
168
316
|
end
|
169
317
|
end
|
318
|
+
end
|
319
|
+
end
|
170
320
|
|
171
|
-
|
172
|
-
|
173
|
-
|
321
|
+
# The Class' methods provide a DSL for describing Atom's structure
|
322
|
+
# (and more generally for describing simple namespaced XML)
|
323
|
+
class Element
|
324
|
+
# this element's xml:base
|
325
|
+
attr_accessor :base
|
326
|
+
|
327
|
+
# xml elements and attributes that have been parsed, but are unknown
|
328
|
+
attr_reader :extensions
|
329
|
+
|
330
|
+
# attaches a name and a namespace to an element
|
331
|
+
# this needs to be called on any new element
|
332
|
+
def self.is_element ns, name
|
333
|
+
meta_def :self_namespace do; ns; end
|
334
|
+
meta_def :self_name do; name.to_s; end
|
335
|
+
end
|
336
|
+
|
337
|
+
# wrapper for #is_element
|
338
|
+
def self.is_atom_element name
|
339
|
+
self.is_element Atom::NS, name
|
340
|
+
end
|
341
|
+
|
342
|
+
# gets a single namespaced child element
|
343
|
+
def get_elem xml, ns, name
|
344
|
+
REXML::XPath.first xml, "./ns:#{name}", { 'ns' => ns }
|
345
|
+
end
|
346
|
+
|
347
|
+
# gets multiple namespaced child elements
|
348
|
+
def get_elems xml, ns, name
|
349
|
+
REXML::XPath.match xml, "./ns:#{name}", { 'ns' => ns }
|
350
|
+
end
|
351
|
+
|
352
|
+
# gets a child element in the Atom namespace
|
353
|
+
def get_atom_elem xml, name
|
354
|
+
get_elem xml, Atom::NS, name
|
355
|
+
end
|
356
|
+
|
357
|
+
# gets multiple child elements in the Atom namespace
|
358
|
+
def get_atom_elems xml, name
|
359
|
+
get_elems Atom::NS, name
|
360
|
+
end
|
361
|
+
|
362
|
+
# gets an attribute on +xml+
|
363
|
+
def get_atom_attrb xml, name
|
364
|
+
xml.attributes[name.to_s]
|
365
|
+
end
|
366
|
+
|
367
|
+
# sets an attribute on +xml+
|
368
|
+
def set_atom_attrb xml, name, value
|
369
|
+
xml.attributes[name.to_s] = value
|
370
|
+
end
|
371
|
+
|
372
|
+
extend Parsers
|
373
|
+
extend Converters
|
374
|
+
|
375
|
+
def self.on_build &block
|
376
|
+
@on_build ||= []
|
377
|
+
@on_build << block
|
378
|
+
end
|
379
|
+
|
380
|
+
def self.do_parsing e, root
|
381
|
+
if ancestors[1].respond_to? :do_parsing
|
382
|
+
ancestors[1].do_parsing e, root
|
174
383
|
end
|
175
384
|
|
176
|
-
|
177
|
-
|
385
|
+
@on_parse ||= []
|
386
|
+
@on_parse.each { |p| p.call e, root }
|
387
|
+
end
|
388
|
+
|
389
|
+
def self.builders &block
|
390
|
+
if ancestors[1].respond_to? :builders
|
391
|
+
ancestors[1].builders &block
|
178
392
|
end
|
179
393
|
|
180
|
-
|
181
|
-
|
394
|
+
@on_build ||= []
|
395
|
+
@on_build.each &block
|
396
|
+
end
|
397
|
+
|
398
|
+
# turns a String, an IO-like, a REXML::Element, etc. into an Atom::Element
|
399
|
+
#
|
400
|
+
# the 'base' base URL parameter should be supplied if you know where this
|
401
|
+
# XML was fetched from
|
402
|
+
#
|
403
|
+
# if you want to parse into an existing Atom::Element, it can be passed in
|
404
|
+
# as 'element'
|
405
|
+
def self.parse xml, base = '', element = nil
|
406
|
+
if xml.respond_to? :elements
|
407
|
+
root = xml.dup
|
408
|
+
else
|
409
|
+
xml = xml.read if xml.respond_to? :read
|
410
|
+
|
411
|
+
begin
|
412
|
+
root = REXML::Document.new(xml.to_s).root
|
413
|
+
rescue REXML::ParseException => e
|
414
|
+
raise Atom::ParseError, e.message
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
unless root.local_name == self.self_name
|
419
|
+
raise Atom::ParseError, "expected element named #{self.self_name}, not #{root.local_name}"
|
420
|
+
end
|
421
|
+
|
422
|
+
unless root.namespace == self.self_namespace
|
423
|
+
raise Atom::ParseError, "expected element in namespace #{self.self_namespace}, not #{root.namespace}"
|
182
424
|
end
|
183
425
|
|
184
|
-
|
426
|
+
if root.attributes['xml:base']
|
427
|
+
base = (base.to_uri + root.attributes['xml:base'])
|
428
|
+
end
|
429
|
+
|
430
|
+
e = element ? element : self.new
|
431
|
+
e.base = base
|
432
|
+
|
433
|
+
# extension elements
|
434
|
+
root.elements.each do |c|
|
435
|
+
e.extensions << c
|
436
|
+
end
|
437
|
+
|
438
|
+
# extension attributes
|
439
|
+
root.attributes.each do |k,v|
|
440
|
+
e.extensions.attributes[k] = v
|
441
|
+
end
|
442
|
+
|
443
|
+
# as things are parsed, they're removed from e.extensions. whatever's
|
444
|
+
# left over is stored so it can be round-tripped
|
445
|
+
|
446
|
+
self.do_parsing e, root
|
447
|
+
|
448
|
+
e
|
185
449
|
end
|
186
|
-
|
187
|
-
#
|
450
|
+
|
451
|
+
# converts to a REXML::Element
|
188
452
|
def to_xml
|
189
|
-
|
190
|
-
root
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
453
|
+
root = REXML::Element.new self.class.self_name
|
454
|
+
root.add_namespace self.class.self_namespace
|
455
|
+
|
456
|
+
build root
|
457
|
+
|
458
|
+
root
|
459
|
+
end
|
460
|
+
|
461
|
+
# fill a REXML::Element with the data from this Atom::Element
|
462
|
+
def build root
|
463
|
+
if self.base and not self.base.empty?
|
464
|
+
root.attributes['xml:base'] = self.base
|
465
|
+
end
|
466
|
+
|
467
|
+
self.class.builders do |builder|
|
468
|
+
builder.call self, root
|
469
|
+
end
|
470
|
+
|
471
|
+
@extensions.each do |e|
|
472
|
+
root << e.dup
|
473
|
+
end
|
474
|
+
|
475
|
+
@extensions.attributes.each do |k,v|
|
476
|
+
root.attributes[k] = v
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
197
480
|
def to_s
|
198
481
|
to_xml.to_s
|
199
482
|
end
|
200
|
-
|
201
|
-
|
202
|
-
|
483
|
+
|
484
|
+
# defines a getter that calls 'block'
|
485
|
+
def self.def_get(name, &block)
|
486
|
+
define_method name.to_sym, &block
|
487
|
+
end
|
488
|
+
|
489
|
+
# defines a setter that calls 'block'
|
490
|
+
def self.def_set(name, &block)
|
491
|
+
define_method "#{name}=".to_sym, &block
|
203
492
|
end
|
204
|
-
|
205
|
-
private
|
206
493
|
|
207
|
-
#
|
208
|
-
def
|
209
|
-
|
210
|
-
|
494
|
+
# be sure to call #super if you override this method!
|
495
|
+
def initialize defaults = {}
|
496
|
+
@extensions = []
|
497
|
+
|
498
|
+
@extensions.instance_variable_set('@attrs', {})
|
499
|
+
def @extensions.attributes
|
500
|
+
@attrs
|
501
|
+
end
|
502
|
+
|
503
|
+
self.class.initters do |init|
|
504
|
+
self.instance_eval &init
|
211
505
|
end
|
506
|
+
|
507
|
+
defaults.each do |k,v|
|
508
|
+
set(k, v)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def self.on_init &block
|
513
|
+
@on_init ||= []
|
514
|
+
@on_init << block
|
212
515
|
end
|
213
516
|
|
214
|
-
|
215
|
-
|
216
|
-
|
517
|
+
def self.initters &block
|
518
|
+
@on_init ||= []
|
519
|
+
@on_init.each &block
|
217
520
|
end
|
218
521
|
|
522
|
+
# appends an element named 'name' in namespace 'ns' to 'root'
|
523
|
+
# ns is either [prefix, namespace] or just a String containing the namespace
|
524
|
+
def append_elem(root, ns, name)
|
525
|
+
if ns.is_a? Array
|
526
|
+
prefix, uri = ns
|
527
|
+
else
|
528
|
+
prefix, uri = nil, ns
|
529
|
+
end
|
530
|
+
|
531
|
+
name = name.to_s
|
532
|
+
|
533
|
+
existing_prefix = root.namespaces.find do |k,v|
|
534
|
+
v == uri
|
535
|
+
end
|
536
|
+
|
537
|
+
root << if existing_prefix
|
538
|
+
prefix = existing_prefix[0]
|
539
|
+
|
540
|
+
if prefix != 'xmlns'
|
541
|
+
name = prefix + ':' + name
|
542
|
+
end
|
543
|
+
|
544
|
+
REXML::Element.new(name)
|
545
|
+
elsif prefix
|
546
|
+
e = REXML::Element.new(prefix + ':' + name)
|
547
|
+
e.add_namespace(prefix, uri)
|
548
|
+
e
|
549
|
+
else
|
550
|
+
e = REXML::Element.new(name)
|
551
|
+
e.add_namespace(uri)
|
552
|
+
e
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def base= uri # :nodoc:
|
557
|
+
@base = uri.to_s
|
558
|
+
end
|
559
|
+
|
560
|
+
# calls a getter
|
219
561
|
def get name
|
220
|
-
|
562
|
+
send "#{name}".to_sym
|
221
563
|
end
|
222
564
|
|
565
|
+
# calls a setter
|
223
566
|
def set name, value
|
224
|
-
|
567
|
+
send "#{name}=", value
|
225
568
|
end
|
226
569
|
end
|
227
|
-
|
228
|
-
# this facilitates YAML output
|
229
|
-
class AttrEl < Atom::Element # :nodoc:
|
230
|
-
end
|
231
570
|
|
232
571
|
# A link has the following attributes:
|
233
572
|
#
|
@@ -237,31 +576,47 @@ module Atom # :nodoc:
|
|
237
576
|
# hreflang:: the language of the linked item (RFC3066)
|
238
577
|
# title:: human-readable information about the link
|
239
578
|
# length:: a hint about the length (in octets) of the linked item
|
240
|
-
class Link < Atom::
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
579
|
+
class Link < Atom::Element
|
580
|
+
is_atom_element :link
|
581
|
+
|
582
|
+
atom_attrb :href
|
583
|
+
atom_attrb :rel
|
584
|
+
atom_attrb :type
|
585
|
+
atom_attrb :hreflang
|
586
|
+
atom_attrb :title
|
587
|
+
atom_attrb :length
|
247
588
|
|
248
|
-
|
249
|
-
super name
|
589
|
+
include AttrEl
|
250
590
|
|
251
|
-
|
252
|
-
|
591
|
+
def rel
|
592
|
+
@rel or 'alternate'
|
593
|
+
end
|
594
|
+
|
595
|
+
def self.parse xml, base = ''
|
596
|
+
e = super
|
597
|
+
|
598
|
+
# URL absolutization
|
599
|
+
if e.base and e.href
|
600
|
+
e.href = (e.base.to_uri + e.href).to_s
|
601
|
+
end
|
602
|
+
|
603
|
+
e
|
253
604
|
end
|
254
605
|
end
|
255
|
-
|
606
|
+
|
256
607
|
# A category has the following attributes:
|
257
608
|
#
|
258
609
|
# term (required):: a string that identifies the category
|
259
610
|
# scheme:: an IRI that identifies a categorization scheme
|
260
611
|
# label:: a human-readable label
|
261
|
-
class Category < Atom::
|
262
|
-
|
263
|
-
|
264
|
-
|
612
|
+
class Category < Atom::Element
|
613
|
+
is_atom_element :category
|
614
|
+
|
615
|
+
atom_attrb :term
|
616
|
+
atom_attrb :scheme
|
617
|
+
atom_attrb :label
|
618
|
+
|
619
|
+
include AttrEl
|
265
620
|
end
|
266
621
|
|
267
622
|
# A person construct has the following child elements:
|
@@ -269,18 +624,17 @@ module Atom # :nodoc:
|
|
269
624
|
# name (required):: a human-readable name
|
270
625
|
# uri:: an IRI associated with the person
|
271
626
|
# email:: an email address associated with the person
|
272
|
-
class
|
273
|
-
|
274
|
-
|
275
|
-
|
627
|
+
class Person < Atom::Element
|
628
|
+
atom_string :name
|
629
|
+
atom_string :uri
|
630
|
+
atom_string :email
|
276
631
|
end
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
element :email, String
|
632
|
+
|
633
|
+
class Author < Atom::Person
|
634
|
+
is_atom_element :author
|
635
|
+
end
|
636
|
+
|
637
|
+
class Contributor < Atom::Person
|
638
|
+
is_atom_element :contributor
|
285
639
|
end
|
286
640
|
end
|