thehack-atom-tools 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +18 -0
- data/README +65 -0
- data/Rakefile +87 -0
- 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 +125 -0
- data/lib/atom/element.rb +640 -0
- data/lib/atom/entry.rb +134 -0
- data/lib/atom/feed.rb +223 -0
- data/lib/atom/http.rb +417 -0
- data/lib/atom/service.rb +106 -0
- data/lib/atom/text.rb +231 -0
- data/lib/atom/tools.rb +163 -0
- data/setup.rb +1585 -0
- data/test/conformance/order.rb +118 -0
- data/test/conformance/title.rb +108 -0
- data/test/conformance/updated.rb +34 -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 +161 -0
- data/test/test_feed.rb +134 -0
- data/test/test_general.rb +72 -0
- data/test/test_http.rb +323 -0
- data/test/test_protocol.rb +168 -0
- data/test/test_xml.rb +445 -0
- metadata +83 -0
data/bin/atom-purge
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
=begin
|
|
4
|
+
Usage: atom-purge [options] collection
|
|
5
|
+
delete all the entries in an Atom Collection
|
|
6
|
+
|
|
7
|
+
'collection' can be a path on the local filesystem, the
|
|
8
|
+
URL of an Atom Collection or '-' for stdin. the feed is parsed
|
|
9
|
+
and every Member URI found in it is DELETEd.
|
|
10
|
+
=end
|
|
11
|
+
|
|
12
|
+
require 'atom/tools'
|
|
13
|
+
include Atom::Tools
|
|
14
|
+
|
|
15
|
+
def parse_options
|
|
16
|
+
options = {}
|
|
17
|
+
|
|
18
|
+
opts = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = <<END
|
|
20
|
+
Usage: #{$0} [options] collection
|
|
21
|
+
delete all the entries in an Atom Collection
|
|
22
|
+
|
|
23
|
+
'collection' can be a path on the local filesystem, the
|
|
24
|
+
URL of an Atom Collection or '-' for stdin. the feed is parsed
|
|
25
|
+
and every Member URI found in it is DELETEd.
|
|
26
|
+
|
|
27
|
+
END
|
|
28
|
+
|
|
29
|
+
opts.on('-c', '--no-complete', "don't follow previous and next links in the source feed") do
|
|
30
|
+
options[:complete] = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on('-v', '--verbose') { options[:verbose] = true }
|
|
34
|
+
|
|
35
|
+
opts.on('-i', '--interactive', "ask before each DELETE") { options[:interactive] = true }
|
|
36
|
+
|
|
37
|
+
atom_options opts, options
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
opts.parse!(ARGV)
|
|
41
|
+
|
|
42
|
+
if ARGV.length != 1
|
|
43
|
+
puts opts
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
options
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if __FILE__ == $0
|
|
51
|
+
require 'optparse'
|
|
52
|
+
|
|
53
|
+
options = parse_options
|
|
54
|
+
|
|
55
|
+
source = ARGV[0]
|
|
56
|
+
dest = ARGV[1]
|
|
57
|
+
|
|
58
|
+
entries = parse_input source, options
|
|
59
|
+
|
|
60
|
+
http = Atom::HTTP.new
|
|
61
|
+
setup_http http, options
|
|
62
|
+
|
|
63
|
+
tty = File.open('/dev/tty', 'w+') if options[:interactive]
|
|
64
|
+
|
|
65
|
+
uris = entries.each do |e|
|
|
66
|
+
next unless (uri = e.edit_url)
|
|
67
|
+
|
|
68
|
+
puts "deleting #{uri}" if options[:verbose]
|
|
69
|
+
|
|
70
|
+
if options[:interactive]
|
|
71
|
+
tty.puts "delete #{uri}"
|
|
72
|
+
tty.puts "title: #{e.title}"
|
|
73
|
+
tty.puts e.content.to_s
|
|
74
|
+
tty.puts
|
|
75
|
+
tty.print "? "
|
|
76
|
+
|
|
77
|
+
next unless ['y', 'yes'].member? tty.gets.chomp
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
http.delete uri
|
|
81
|
+
end
|
|
82
|
+
end
|
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
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require "atom/http"
|
|
2
|
+
require "atom/feed"
|
|
3
|
+
|
|
4
|
+
module Atom
|
|
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
|
|
45
|
+
|
|
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
|
+
@http = http
|
|
74
|
+
|
|
75
|
+
if href
|
|
76
|
+
self.href = href
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.parse xml, base = ''
|
|
81
|
+
e = super
|
|
82
|
+
|
|
83
|
+
# URL absolutization
|
|
84
|
+
if !e.base.empty? and e.href
|
|
85
|
+
e.href = (e.base.to_uri + e.href).to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
e
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# POST an entry to the collection, with an optional slug
|
|
92
|
+
def post!(entry, slug = nil)
|
|
93
|
+
raise "Cowardly refusing to POST a non-Atom::Entry" unless entry.is_a? Atom::Entry
|
|
94
|
+
headers = {"Content-Type" => "application/atom+xml" }
|
|
95
|
+
headers["Slug"] = slug if slug
|
|
96
|
+
|
|
97
|
+
@http.post(@href, entry.to_s, headers)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# PUT an updated version of an entry to the collection
|
|
101
|
+
def put!(entry, url = entry.edit_url)
|
|
102
|
+
@http.put_atom_entry(entry, url)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# DELETE an entry from the collection
|
|
106
|
+
def delete!(entry, url = entry.edit_url)
|
|
107
|
+
@http.delete(url)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# POST a media item to the collection
|
|
111
|
+
def post_media!(data, content_type, slug = nil)
|
|
112
|
+
headers = {"Content-Type" => content_type}
|
|
113
|
+
headers["Slug"] = slug if slug
|
|
114
|
+
|
|
115
|
+
@http.post(@href, data, headers)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# PUT a media item to the collection
|
|
119
|
+
def put_media!(data, content_type, slug = nil)
|
|
120
|
+
headers = {"Content-Type" => content_type}
|
|
121
|
+
|
|
122
|
+
@http.put(url, data, headers)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/atom/element.rb
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
require "rexml/element"
|
|
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
|
+
|
|
26
|
+
module Atom # :nodoc:
|
|
27
|
+
NS = "http://www.w3.org/2005/Atom"
|
|
28
|
+
PP_NS = "http://www.w3.org/2007/app"
|
|
29
|
+
|
|
30
|
+
class ParseError < StandardError; end
|
|
31
|
+
|
|
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
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ignore the man behind the curtain.
|
|
39
|
+
def self.Multiple klass
|
|
40
|
+
Class.new(Array) do
|
|
41
|
+
@class = klass
|
|
42
|
+
|
|
43
|
+
def new *args
|
|
44
|
+
item = self.class.holds.new *args
|
|
45
|
+
self << item
|
|
46
|
+
|
|
47
|
+
item
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def << item
|
|
51
|
+
raise ArgumentError, "this can only hold items of class #{self.class.holds}" unless item.is_a? self.class.holds
|
|
52
|
+
|
|
53
|
+
super(item)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.holds; @class end
|
|
57
|
+
def self.single?; true end
|
|
58
|
+
def taguri; end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
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 ||= []
|
|
68
|
+
|
|
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
|
|
80
|
+
|
|
81
|
+
@on_parse << process
|
|
82
|
+
end
|
|
83
|
+
|
|
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
|
|
101
|
+
end
|
|
102
|
+
|
|
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
|
|
122
|
+
end
|
|
123
|
+
|
|
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
|
|
134
|
+
end
|
|
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)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
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)
|
|
168
|
+
attr_reader name
|
|
169
|
+
|
|
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
|
|
179
|
+
|
|
180
|
+
define_method "#{name}!".to_sym do
|
|
181
|
+
set(name, Time.now)
|
|
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
|
|
191
|
+
end
|
|
192
|
+
|
|
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
|
|
215
|
+
|
|
216
|
+
# an element that is parsed by Element descendant 'klass'
|
|
217
|
+
def atom_element(name, klass)
|
|
218
|
+
self.element(['atom', Atom::NS], name, klass)
|
|
219
|
+
end
|
|
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
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
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
|
|
274
|
+
end
|
|
275
|
+
|
|
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)
|
|
279
|
+
end
|
|
280
|
+
|
|
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
|
|
288
|
+
|
|
289
|
+
self.on_build do |e,x|
|
|
290
|
+
if v = e.get(name)
|
|
291
|
+
e.set_atom_attrb(x, name, v.to_s)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# an XML attribute in the Atom namespace
|
|
297
|
+
def atom_attrb(name)
|
|
298
|
+
self.attrb(['atom', Atom::NS], name)
|
|
299
|
+
end
|
|
300
|
+
|
|
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)
|
|
305
|
+
|
|
306
|
+
existing and existing.href
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def_set name do |value|
|
|
310
|
+
existing = find_link(criteria)
|
|
311
|
+
|
|
312
|
+
if existing
|
|
313
|
+
existing.href = value
|
|
314
|
+
else
|
|
315
|
+
links.new criteria.merge(:href => value)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
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
|
|
383
|
+
end
|
|
384
|
+
|
|
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
|
|
392
|
+
end
|
|
393
|
+
|
|
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}"
|
|
424
|
+
end
|
|
425
|
+
|
|
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
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# converts to a REXML::Element
|
|
452
|
+
def to_xml
|
|
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
|
+
|
|
480
|
+
def to_s
|
|
481
|
+
to_xml.to_s
|
|
482
|
+
end
|
|
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
|
|
492
|
+
end
|
|
493
|
+
|
|
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
|
|
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
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def self.initters &block
|
|
518
|
+
@on_init ||= []
|
|
519
|
+
@on_init.each &block
|
|
520
|
+
end
|
|
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
|
|
561
|
+
def get name
|
|
562
|
+
send "#{name}".to_sym
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# calls a setter
|
|
566
|
+
def set name, value
|
|
567
|
+
send "#{name}=", value
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# A link has the following attributes:
|
|
572
|
+
#
|
|
573
|
+
# href (required):: the link's IRI
|
|
574
|
+
# rel:: the relationship of the linked item to the current item
|
|
575
|
+
# type:: a hint about the media type of the linked item
|
|
576
|
+
# hreflang:: the language of the linked item (RFC3066)
|
|
577
|
+
# title:: human-readable information about the link
|
|
578
|
+
# length:: a hint about the length (in octets) of the linked item
|
|
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
|
|
588
|
+
|
|
589
|
+
include AttrEl
|
|
590
|
+
|
|
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.empty? and e.href
|
|
600
|
+
# e.href = (e.base.to_uri + e.href).to_s
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
e
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# A category has the following attributes:
|
|
608
|
+
#
|
|
609
|
+
# term (required):: a string that identifies the category
|
|
610
|
+
# scheme:: an IRI that identifies a categorization scheme
|
|
611
|
+
# label:: a human-readable label
|
|
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
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# A person construct has the following child elements:
|
|
623
|
+
#
|
|
624
|
+
# name (required):: a human-readable name
|
|
625
|
+
# uri:: an IRI associated with the person
|
|
626
|
+
# email:: an email address associated with the person
|
|
627
|
+
class Person < Atom::Element
|
|
628
|
+
atom_string :name
|
|
629
|
+
atom_string :uri
|
|
630
|
+
atom_string :email
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
class Author < Atom::Person
|
|
634
|
+
is_atom_element :author
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
class Contributor < Atom::Person
|
|
638
|
+
is_atom_element :contributor
|
|
639
|
+
end
|
|
640
|
+
end
|