atom-tools 0.9.0 → 2.0.5

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.
Files changed (93) hide show
  1. data/COPYING +3 -3
  2. data/README +9 -49
  3. data/Rakefile +65 -62
  4. data/bin/atom-cp +159 -0
  5. data/bin/atom-grep +78 -0
  6. data/bin/atom-post +73 -0
  7. data/bin/atom-purge +82 -0
  8. data/doc/classes/Atom/AttrEl.html +163 -0
  9. data/doc/classes/Atom/Author.html +2 -19
  10. data/doc/classes/Atom/AutodiscoveryFailure.html +111 -0
  11. data/doc/classes/Atom/Categories.html +168 -0
  12. data/doc/classes/Atom/Category.html +10 -1
  13. data/doc/classes/Atom/Collection.html +194 -91
  14. data/doc/classes/Atom/Content.html +42 -1
  15. data/doc/classes/Atom/Contributor.html +2 -8
  16. data/doc/classes/Atom/Control.html +126 -0
  17. data/doc/classes/Atom/Converters.html +571 -0
  18. data/doc/classes/Atom/DigestAuth.html +285 -0
  19. data/doc/classes/Atom/Element.html +673 -106
  20. data/doc/classes/Atom/FileCache.html +287 -0
  21. data/doc/classes/Atom/HTTP.html +211 -95
  22. data/doc/classes/Atom/HTTPResponse.html +190 -0
  23. data/doc/classes/Atom/HasCategories.html +187 -0
  24. data/doc/classes/Atom/HasLinks.html +168 -0
  25. data/doc/classes/Atom/Link.html +80 -3
  26. data/doc/classes/Atom/NilCache.html +202 -0
  27. data/doc/classes/Atom/ParseError.html +111 -0
  28. data/doc/classes/Atom/Parsers.html +311 -0
  29. data/doc/classes/Atom/Person.html +130 -0
  30. data/doc/classes/Atom/Rights.html +113 -0
  31. data/doc/classes/Atom/Service.html +298 -0
  32. data/doc/{files/lib/atom/yaml_rb.html → classes/Atom/Source.html} +26 -24
  33. data/doc/classes/Atom/Subtitle.html +113 -0
  34. data/doc/classes/Atom/Summary.html +113 -0
  35. data/doc/classes/Atom/Text.html +196 -53
  36. data/doc/classes/Atom/Title.html +113 -0
  37. data/doc/classes/Atom/Tools.html +525 -0
  38. data/doc/classes/Atom/Workspace.html +121 -0
  39. data/doc/classes/Object.html +204 -0
  40. data/doc/created.rid +1 -1
  41. data/doc/files/README.html +9 -49
  42. data/doc/files/lib/atom/cache_rb.html +302 -0
  43. data/doc/files/lib/atom/collection_rb.html +1 -2
  44. data/doc/files/lib/atom/element_rb.html +2 -1
  45. data/doc/files/lib/atom/entry_rb.html +2 -2
  46. data/doc/files/lib/atom/feed_rb.html +1 -2
  47. data/doc/files/lib/atom/http_rb.html +6 -1
  48. data/doc/files/lib/atom/{xml_rb.html → service_rb.html} +8 -6
  49. data/doc/files/lib/atom/text_rb.html +2 -1
  50. data/doc/files/lib/atom/{app_rb.html → tools_rb.html} +4 -6
  51. data/doc/fr_class_index.html +23 -3
  52. data/doc/fr_file_index.html +3 -3
  53. data/doc/fr_method_index.html +111 -36
  54. data/lib/atom/cache.rb +178 -0
  55. data/lib/atom/collection.rb +90 -40
  56. data/lib/atom/element.rb +569 -161
  57. data/lib/atom/entry.rb +63 -89
  58. data/lib/atom/feed.rb +65 -67
  59. data/lib/atom/http.rb +421 -49
  60. data/lib/atom/service.rb +106 -0
  61. data/lib/atom/text.rb +138 -61
  62. data/lib/atom/tools.rb +163 -0
  63. data/spec/entry_spec.rb +364 -0
  64. data/spec/ext_spec.rb +42 -0
  65. data/spec/feed_spec.rb +39 -0
  66. data/spec/fixtures/contacts-feed.xml +34 -0
  67. data/spec/fixtures/entry-w-ext.xml +15 -0
  68. data/spec/fixtures/entry-w-xml.xml +24 -0
  69. data/spec/fixtures/entry.xml +42 -0
  70. data/spec/fixtures/feed-w-ext.xml +33 -0
  71. data/spec/fixtures/service-w-xhtml-ns.xml +21 -0
  72. data/spec/fixtures/service.xml +36 -0
  73. data/spec/service_spec.rb +108 -0
  74. data/spec/spec_helper.rb +9 -0
  75. data/test/conformance/order.rb +11 -10
  76. data/test/conformance/title.rb +9 -9
  77. data/test/conformance/updated.rb +2 -1
  78. data/test/runtests.rb +0 -0
  79. data/test/test_constructs.rb +78 -8
  80. data/test/test_feed.rb +35 -29
  81. data/test/test_general.rb +3 -30
  82. data/test/test_http.rb +248 -34
  83. data/test/test_protocol.rb +143 -24
  84. data/test/test_xml.rb +145 -53
  85. metadata +116 -62
  86. data/bin/atom-client.rb +0 -246
  87. data/bin/atom-server.rb~ +0 -71
  88. data/doc/classes/Atom/App.html +0 -217
  89. data/doc/classes/Atom/Entry.html +0 -365
  90. data/doc/classes/Atom/Feed.html +0 -585
  91. data/lib/atom/app.rb +0 -87
  92. data/lib/atom/xml.rb +0 -200
  93. data/lib/atom/yaml.rb +0 -101
data/lib/atom/element.rb CHANGED
@@ -1,39 +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
- def to_element
36
- collect do |item| item.to_element end
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)
37
54
  end
38
55
 
39
56
  def self.holds; @class end
@@ -42,183 +59,520 @@ module Atom # :nodoc:
42
59
  end
43
60
  end
44
61
 
45
- class Element < Hash
46
- # a REXML::Element that shares this element's extension attributes
47
- # and child elements
48
- 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 ||= []
49
68
 
50
- # this element's xml:base
51
- attr_accessor :base
52
-
53
- # The following is a DSL for describing an atom element.
69
+ process = lambda do |e,x|
70
+ el = e.get_elem(x, uri, name)
71
+
72
+ if el
73
+ block.call e, el
54
74
 
55
- # this element's attributes
56
- def self.attrs # :nodoc:
57
- @attrs || []
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
58
82
  end
59
83
 
60
- # this element's child elements
61
- def self.elements # :nodoc:
62
- @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
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
63
122
  end
64
123
 
65
- # required child elements
66
- def self.required # :nodoc:
67
- @elements.find { |name,kind,req| 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
134
+ end
135
+
136
+ @on_parse << process
68
137
  end
69
138
 
70
- # copy defined elements and attributes so inheritance works
71
- def self.inherited klass # :nodoc:
72
- elements.each do |name, kind, req|
73
- klass.element name, kind, req
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)
74
144
  end
75
- attrs.each do |name, req|
76
- klass.attrb name, req
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
77
155
  end
78
156
  end
79
157
 
80
- # define a child element
81
- def self.element(name, kind, req = false) # :nodoc:
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)
82
168
  attr_reader name
83
169
 
84
- @elements ||= []
85
- @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
86
176
 
87
- unless kind.respond_to? :single?
88
- self.define_accessor(name,kind)
177
+ instance_variable_set("@#{name}", time)
89
178
  end
90
- end
91
179
 
92
- # define an attribute
93
- def self.attrb(name, req = false) # :nodoc:
94
- @attrs ||= []
180
+ define_method "#{name}!".to_sym do
181
+ set(name, Time.now)
182
+ end
95
183
 
96
- @attrs << [name, req]
184
+ self.parse_plain(ns[1], name)
185
+ self.build_plain(ns, name)
97
186
  end
98
-
99
- # a little bit of magic
100
- def self.define_accessor(name,kind) # :nodoc:
101
- define_method "#{name}=".to_sym do |value|
102
- return unless value
103
-
104
- i = if kind.ancestors.member? Atom::Element
105
- kind.new(value, name.to_s)
106
- else
107
- kind.new(value)
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)
108
208
  end
109
-
110
- set(name, i)
209
+ end
210
+
211
+ def_set name do |value|
212
+ instance_variable_set("@#{name}", klass.new(value))
111
213
  end
112
214
  end
113
215
 
114
- # get the value of an attribute
115
- def [] key
116
- test_key key
117
-
118
- super
216
+ # an element that is parsed by Element descendant 'klass'
217
+ def atom_element(name, klass)
218
+ self.element(['atom', Atom::NS], name, klass)
119
219
  end
120
-
121
- # set the value of an attribute
122
- def []= key, value
123
- test_key key
124
220
 
125
- super
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
126
245
  end
127
246
 
128
- # internal junk you probably don't care about
129
- def initialize name = nil # :nodoc:
130
- @extensions = REXML::Element.new("extensions")
131
- @local_name = name
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
132
267
 
133
- self.class.elements.each do |name,kind,req|
134
- if kind.respond_to? :single?
135
- a = kind.new
136
- set(name, kind.new)
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)
137
272
  end
138
273
  end
139
274
  end
140
275
 
141
- # eg. "feed" or "entry" or "updated" or "title" or ...
142
- def local_name # :nodoc:
143
- @local_name || self.class.name.split("::").last.downcase
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)
144
279
  end
145
-
146
- # convert to a REXML::Element (with no namespace)
147
- def to_element
148
- elem = REXML::Element.new(local_name)
149
280
 
150
- self.class.elements.each do |name,kind,req|
151
- v = get(name)
152
- next if v.nil?
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
153
288
 
154
- if v.respond_to? :to_element
155
- e = v.to_element
156
- e = [ e ] unless e.is_a? Array
289
+ self.on_build do |e,x|
290
+ if v = e.get(name)
291
+ n = name.to_s
157
292
 
158
- e.each do |bit|
159
- elem << bit
293
+ if x.namespace != ns[1]
294
+ x.add_namespace *ns unless x.namespaces[ns[0]]
295
+ n = "#{ns[0]}:#{n}"
160
296
  end
297
+
298
+ x.attributes[n] = v.to_s
299
+ end
300
+ end
301
+ end
302
+
303
+ # an XML attribute in the Atom namespace
304
+ def atom_attrb(name)
305
+ self.attrb(['atom', Atom::NS], name)
306
+ end
307
+
308
+ # a type of Atom Link. specifics defined by Hash 'criteria'
309
+ def atom_link name, criteria
310
+ def_get name do
311
+ existing = find_link(criteria)
312
+
313
+ existing and existing.href
314
+ end
315
+
316
+ def_set name do |value|
317
+ existing = find_link(criteria)
318
+
319
+ if existing
320
+ existing.href = value
161
321
  else
162
- e = REXML::Element.new(name.to_s, elem).text = get(name)
322
+ links.new criteria.merge(:href => value)
163
323
  end
164
324
  end
325
+ end
326
+ end
327
+
328
+ # The Class' methods provide a DSL for describing Atom's structure
329
+ # (and more generally for describing simple namespaced XML)
330
+ class Element
331
+ # this element's xml:base
332
+ attr_accessor :base
333
+
334
+ # xml elements and attributes that have been parsed, but are unknown
335
+ attr_reader :extensions
336
+
337
+ # attaches a name and a namespace to an element
338
+ # this needs to be called on any new element
339
+ def self.is_element ns, name
340
+ meta_def :self_namespace do; ns; end
341
+ meta_def :self_name do; name.to_s; end
342
+ end
343
+
344
+ # wrapper for #is_element
345
+ def self.is_atom_element name
346
+ self.is_element Atom::NS, name
347
+ end
348
+
349
+ # gets a single namespaced child element
350
+ def get_elem xml, ns, name
351
+ REXML::XPath.first xml, "./ns:#{name}", { 'ns' => ns }
352
+ end
353
+
354
+ # gets multiple namespaced child elements
355
+ def get_elems xml, ns, name
356
+ REXML::XPath.match xml, "./ns:#{name}", { 'ns' => ns }
357
+ end
358
+
359
+ # gets a child element in the Atom namespace
360
+ def get_atom_elem xml, name
361
+ get_elem xml, Atom::NS, name
362
+ end
363
+
364
+ # gets multiple child elements in the Atom namespace
365
+ def get_atom_elems xml, name
366
+ get_elems Atom::NS, name
367
+ end
368
+
369
+ # gets an attribute on +xml+
370
+ def get_atom_attrb xml, name
371
+ xml.attributes[name.to_s]
372
+ end
373
+
374
+ # sets an attribute on +xml+
375
+ def set_atom_attrb xml, name, value
376
+ xml.attributes[name.to_s] = value
377
+ end
378
+
379
+ extend Parsers
380
+ extend Converters
381
+
382
+ def self.on_build &block
383
+ @on_build ||= []
384
+ @on_build << block
385
+ end
386
+
387
+ def self.do_parsing e, root
388
+ if ancestors[1].respond_to? :do_parsing
389
+ ancestors[1].do_parsing e, root
390
+ end
165
391
 
166
- self.class.attrs.each do |name,req|
167
- value = self[name.to_s]
168
- elem.attributes[name.to_s] = value if value
392
+ @on_parse ||= []
393
+ @on_parse.each { |p| p.call e, root }
394
+ end
395
+
396
+ def self.builders &block
397
+ if ancestors[1].respond_to? :builders
398
+ ancestors[1].builders &block
169
399
  end
170
400
 
171
- self.extensions.children.each do |element|
172
- elem << element.dup # otherwise they get removed from @extensions
401
+ @on_build ||= []
402
+ @on_build.each &block
403
+ end
404
+
405
+ # turns a String, an IO-like, a REXML::Element, etc. into an Atom::Element
406
+ #
407
+ # the 'base' base URL parameter should be supplied if you know where this
408
+ # XML was fetched from
409
+ #
410
+ # if you want to parse into an existing Atom::Element, it can be passed in
411
+ # as 'element'
412
+ def self.parse xml, base = '', element = nil
413
+ if xml.respond_to? :elements
414
+ root = xml.dup
415
+ else
416
+ xml = xml.read if xml.respond_to? :read
417
+
418
+ begin
419
+ root = REXML::Document.new(xml.to_s).root
420
+ rescue REXML::ParseException => e
421
+ raise Atom::ParseError, e.message
422
+ end
423
+ end
424
+
425
+ unless root.local_name == self.self_name
426
+ raise Atom::ParseError, "expected element named #{self.self_name}, not #{root.local_name}"
173
427
  end
174
428
 
175
- if self.base and not self.base.empty?
176
- elem.attributes["xml:base"] = self.base
429
+ unless root.namespace == self.self_namespace
430
+ raise Atom::ParseError, "expected element in namespace #{self.self_namespace}, not #{root.namespace}"
431
+ end
432
+
433
+ if root.attributes['xml:base']
434
+ base = (base.to_uri + root.attributes['xml:base'])
435
+ end
436
+
437
+ e = element ? element : self.new
438
+ e.base = base
439
+
440
+ # extension elements
441
+ root.elements.each do |c|
442
+ e.extensions << c
443
+ end
444
+
445
+ # extension attributes
446
+ root.attributes.each do |k,v|
447
+ e.extensions.attributes[k] = v
177
448
  end
178
449
 
179
- elem
450
+ # as things are parsed, they're removed from e.extensions. whatever's
451
+ # left over is stored so it can be round-tripped
452
+
453
+ self.do_parsing e, root
454
+
455
+ e
180
456
  end
181
-
182
- # convert to a REXML::Document (properly namespaced)
457
+
458
+ # converts to a REXML::Element
183
459
  def to_xml
184
- doc = REXML::Document.new
185
- root = to_element
186
- root.add_namespace Atom::NS
187
- doc << root
188
- doc
189
- end
190
-
191
- # convert to an XML string
460
+ root = REXML::Element.new self.class.self_name
461
+ root.add_namespace self.class.self_namespace
462
+
463
+ build root
464
+
465
+ root
466
+ end
467
+
468
+ # fill a REXML::Element with the data from this Atom::Element
469
+ def build root
470
+ if self.base and not self.base.empty?
471
+ root.attributes['xml:base'] = self.base
472
+ end
473
+
474
+ self.class.builders do |builder|
475
+ builder.call self, root
476
+ end
477
+
478
+ @extensions.each do |e|
479
+ root << e.dup
480
+ end
481
+
482
+ @extensions.attributes.each do |k,v|
483
+ root.attributes[k] = v
484
+ end
485
+ end
486
+
192
487
  def to_s
193
488
  to_xml.to_s
194
489
  end
195
-
196
- private
197
490
 
198
- # like +valid_key?+ but raises on failure
199
- def test_key key
200
- unless valid_key? key
201
- raise RuntimeError, "this element (#{local_name}) doesn't have that attribute '#{key}'"
491
+ # defines a getter that calls 'block'
492
+ def self.def_get(name, &block)
493
+ define_method name.to_sym, &block
494
+ end
495
+
496
+ # defines a setter that calls 'block'
497
+ def self.def_set(name, &block)
498
+ define_method "#{name}=".to_sym, &block
499
+ end
500
+
501
+ # be sure to call #super if you override this method!
502
+ def initialize defaults = {}
503
+ @extensions = []
504
+
505
+ @extensions.instance_variable_set('@attrs', {})
506
+ def @extensions.attributes
507
+ @attrs
202
508
  end
509
+
510
+ self.class.run_initters do |init|
511
+ self.instance_eval &init
512
+ end
513
+
514
+ defaults.each do |k,v|
515
+ set(k, v)
516
+ end
517
+ end
518
+
519
+ def self.on_init &block
520
+ @on_init ||= []
521
+ @on_init << block
203
522
  end
204
523
 
205
- # tests that an attribute 'key' has been defined
206
- def valid_key? key
207
- self.class.attrs.find { |name,req| name.to_s == key }
524
+ def self.run_initters &block
525
+ @on_init.each(&block) if @on_init
208
526
  end
209
527
 
528
+ # appends an element named 'name' in namespace 'ns' to 'root'
529
+ # ns is either [prefix, namespace] or just a String containing the namespace
530
+ def append_elem(root, ns, name)
531
+ if ns.is_a? Array
532
+ prefix, uri = ns
533
+ else
534
+ prefix, uri = nil, ns
535
+ end
536
+
537
+ name = name.to_s
538
+
539
+ existing_prefix = root.namespaces.find do |k,v|
540
+ v == uri
541
+ end
542
+
543
+ root << if existing_prefix
544
+ prefix = existing_prefix[0]
545
+
546
+ if prefix != 'xmlns'
547
+ name = prefix + ':' + name
548
+ end
549
+
550
+ REXML::Element.new(name)
551
+ elsif prefix
552
+ e = REXML::Element.new(prefix + ':' + name)
553
+ e.add_namespace(prefix, uri)
554
+ e
555
+ else
556
+ e = REXML::Element.new(name)
557
+ e.add_namespace(uri)
558
+ e
559
+ end
560
+ end
561
+
562
+ def base= uri # :nodoc:
563
+ @base = uri.to_s
564
+ end
565
+
566
+ # calls a getter
210
567
  def get name
211
- instance_variable_get "@#{name}"
568
+ send "#{name}".to_sym
212
569
  end
213
570
 
571
+ # calls a setter
214
572
  def set name, value
215
- instance_variable_set "@#{name}", value
573
+ send "#{name}=", value
216
574
  end
217
575
  end
218
-
219
- # this facilitates YAML output
220
- class AttrEl < Atom::Element # :nodoc:
221
- end
222
576
 
223
577
  # A link has the following attributes:
224
578
  #
@@ -228,31 +582,47 @@ module Atom # :nodoc:
228
582
  # hreflang:: the language of the linked item (RFC3066)
229
583
  # title:: human-readable information about the link
230
584
  # length:: a hint about the length (in octets) of the linked item
231
- class Link < Atom::AttrEl
232
- attrb :href, true
233
- attrb :rel
234
- attrb :type
235
- attrb :hreflang
236
- attrb :title
237
- attrb :length
585
+ class Link < Atom::Element
586
+ is_atom_element :link
587
+
588
+ atom_attrb :href
589
+ atom_attrb :rel
590
+ atom_attrb :type
591
+ atom_attrb :hreflang
592
+ atom_attrb :title
593
+ atom_attrb :length
238
594
 
239
- def initialize name = nil # :nodoc:
240
- super name
595
+ include AttrEl
241
596
 
242
- # just setting a default
243
- self["rel"] = "alternate"
597
+ def rel
598
+ @rel or 'alternate'
599
+ end
600
+
601
+ def self.parse xml, base = ''
602
+ e = super
603
+
604
+ # URL absolutization
605
+ if !e.base.empty? and e.href
606
+ e.href = (e.base.to_uri + e.href).to_s
607
+ end
608
+
609
+ e
244
610
  end
245
611
  end
246
-
612
+
247
613
  # A category has the following attributes:
248
614
  #
249
615
  # term (required):: a string that identifies the category
250
616
  # scheme:: an IRI that identifies a categorization scheme
251
617
  # label:: a human-readable label
252
- class Category < Atom::AttrEl
253
- attrb :term, true
254
- attrb :scheme
255
- attrb :label
618
+ class Category < Atom::Element
619
+ is_atom_element :category
620
+
621
+ atom_attrb :term
622
+ atom_attrb :scheme
623
+ atom_attrb :label
624
+
625
+ include AttrEl
256
626
  end
257
627
 
258
628
  # A person construct has the following child elements:
@@ -260,18 +630,56 @@ module Atom # :nodoc:
260
630
  # name (required):: a human-readable name
261
631
  # uri:: an IRI associated with the person
262
632
  # email:: an email address associated with the person
263
- class Author < Atom::Element
264
- element :name, String, true
265
- element :uri, String
266
- element :email, String
633
+ class Person < Atom::Element
634
+ atom_string :name
635
+ atom_string :uri
636
+ atom_string :email
637
+ end
638
+
639
+ class Author < Atom::Person
640
+ is_atom_element :author
641
+ end
642
+
643
+ class Contributor < Atom::Person
644
+ is_atom_element :contributor
267
645
  end
268
-
269
- # same as Atom::Author
270
- class Contributor < Atom::Element
271
- # Author and Contributor should probably inherit from Person, but
272
- # oh well.
273
- element :name, String, true
274
- element :uri, String
275
- element :email, String
646
+
647
+ module HasLinks
648
+ def HasLinks.included(klass)
649
+ klass.atom_elements :link, :links, Atom::Link
650
+ end
651
+
652
+ def find_link(criteria)
653
+ self.links.find do |l|
654
+ criteria.all? { |k,v| l.send(k) == v }
655
+ end
656
+ end
657
+ end
658
+
659
+ module HasCategories
660
+ def HasCategories.included(klass)
661
+ klass.atom_elements :category, :categories, Atom::Category
662
+ end
663
+
664
+ # categorize the entry with each of an array or a space-separated
665
+ # string
666
+ def tag_with(tags, delimiter = ' ')
667
+ return if not tags or tags.empty?
668
+
669
+ tag_list = unless tags.is_a?(String)
670
+ tags
671
+ else
672
+ tags = tags.split(delimiter)
673
+ tags.map! { |t| t.strip }
674
+ tags.reject! { |t| t.empty? }
675
+ tags.uniq
676
+ end
677
+
678
+ tag_list.each do |tag|
679
+ unless categories.any? { |c| c.term == tag }
680
+ categories.new :term => tag
681
+ end
682
+ end
683
+ end
276
684
  end
277
685
  end