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/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
@@ -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