atom-tools 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/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