atom-tools 1.0.0 → 2.0.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 +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
|