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/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
@@ -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
- # a Collection is an Atom::Feed with extra Protocol-specific methods
9
- class Collection < Feed
10
- # comma separated string that contains a list of media types
11
- # accepted by a collection.
12
- #
13
- # XXX I should parse this in some way, but I'm not sure what's useful
14
- attr_accessor :accepts
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
- def initialize(uri, http = Atom::HTTP.new)
17
- super uri, http
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(@uri, entry.to_s, headers)
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(@uri, data, headers)
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
- class Time < ::Time # :nodoc:
6
- def self.new date
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
- date = if date.respond_to?(:iso8601)
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
- date
20
- end
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
- def to_element
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
- # The Class' methods provide a DSL for describing Atom's structure
51
- # (and more generally for describing simple namespaced XML)
52
- class Element < Hash
53
- # a REXML::Element that shares this element's extension attributes
54
- # and child elements
55
- attr_reader :extensions
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
- # this element's xml:base
58
- attr_accessor :base
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
- # this element's attributes
61
- def self.attrs # :nodoc:
62
- @attrs || []
81
+ @on_parse << process
63
82
  end
64
83
 
65
- # this element's child elements
66
- def self.elements # :nodoc:
67
- @elements || []
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
- # required child elements
71
- def self.required # :nodoc:
72
- @elements.find { |name,kind,req| req }
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
- # copy defined elements and attributes so inheritance works
76
- def self.inherited klass # :nodoc:
77
- elements.each do |name, kind, req|
78
- klass.element name, kind, req
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
- attrs.each do |name, req|
81
- klass.attrb name, req
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
- # define a child element
86
- def self.element(name, kind, req = false) # :nodoc:
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
- @elements ||= []
90
- @elements << [name, kind, req]
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
- unless kind.respond_to? :single?
93
- self.define_accessor(name,kind)
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
- # define an attribute
98
- def self.attrb(name, req = false) # :nodoc:
99
- @attrs ||= []
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
- @attrs << [name, req]
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
- # a little bit of magic
105
- def self.define_accessor(name,kind) # :nodoc:
106
- define_method "#{name}=".to_sym do |value|
107
- return unless value
108
-
109
- i = if kind.ancestors.member? Atom::Element
110
- kind.new(value, name.to_s)
111
- else
112
- kind.new(value)
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
- # get the value of an attribute
120
- def [] key
121
- test_key key
122
-
123
- super
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
- super
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
- # internal junk you probably don't care about
134
- def initialize name = nil # :nodoc:
135
- @extensions = REXML::Element.new("extensions")
136
- @local_name = name
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.class.elements.each do |name,kind,req|
139
- if kind.respond_to? :single?
140
- a = kind.new
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
- # eg. "feed" or "entry" or "updated" or "title" or ...
147
- def local_name # :nodoc:
148
- @local_name || self.class.name.split("::").last.downcase
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
- self.class.elements.each do |name,kind,req|
156
- v = get(name)
157
- next if v.nil?
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
- if v.respond_to? :to_element
160
- e = v.to_element
161
- e = [ e ] unless e.is_a? Array
306
+ existing and existing.href
307
+ end
162
308
 
163
- e.each do |bit|
164
- elem << bit
165
- end
309
+ def_set name do |value|
310
+ existing = find_link(criteria)
311
+
312
+ if existing
313
+ existing.href = value
166
314
  else
167
- e = REXML::Element.new(name.to_s, elem).text = get(name)
315
+ links.new criteria.merge(:href => value)
168
316
  end
169
317
  end
318
+ end
319
+ end
170
320
 
171
- self.class.attrs.each do |name,req|
172
- value = self[name.to_s]
173
- elem.attributes[name.to_s] = value.to_s if value
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
- self.extensions.children.each do |element|
177
- elem << element.dup # otherwise they get removed from @extensions
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
- if self.base and not self.base.empty?
181
- elem.attributes["xml:base"] = self.base
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
- elem
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
- # convert to a REXML::Document (properly namespaced)
450
+
451
+ # converts to a REXML::Element
188
452
  def to_xml
189
- doc = REXML::Document.new
190
- root = to_element
191
- root.add_namespace Atom::NS
192
- doc << root
193
- doc
194
- end
195
-
196
- # convert to an XML string
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
- def base= uri # :nodoc:
202
- @base = uri.to_s
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
- # like +valid_key?+ but raises on failure
208
- def test_key key
209
- unless valid_key? key
210
- raise RuntimeError, "this element (#{local_name}) doesn't have that attribute '#{key}'"
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
- # tests that an attribute 'key' has been defined
215
- def valid_key? key
216
- self.class.attrs.find { |name,req| name.to_s == key }
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
- instance_variable_get "@#{name}"
562
+ send "#{name}".to_sym
221
563
  end
222
564
 
565
+ # calls a setter
223
566
  def set name, value
224
- instance_variable_set "@#{name}", value
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::AttrEl
241
- attrb :href, true
242
- attrb :rel
243
- attrb :type
244
- attrb :hreflang
245
- attrb :title
246
- attrb :length
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
- def initialize name = nil # :nodoc:
249
- super name
589
+ include AttrEl
250
590
 
251
- # just setting a default
252
- self["rel"] = "alternate"
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::AttrEl
262
- attrb :term, true
263
- attrb :scheme
264
- attrb :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
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 Author < Atom::Element
273
- element :name, String, true
274
- element :uri, String
275
- element :email, String
627
+ class Person < Atom::Element
628
+ atom_string :name
629
+ atom_string :uri
630
+ atom_string :email
276
631
  end
277
-
278
- # same as Atom::Author
279
- class Contributor < Atom::Element
280
- # Author and Contributor should probably inherit from Person, but
281
- # oh well.
282
- element :name, String, true
283
- element :uri, String
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