julienXX-www-delicious 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/CHANGELOG.rdoc +61 -0
  2. data/LICENSE.rdoc +25 -0
  3. data/Manifest +46 -0
  4. data/README.rdoc +201 -0
  5. data/Rakefile +57 -0
  6. data/lib/www/delicious/bundle.rb +73 -0
  7. data/lib/www/delicious/element.rb +73 -0
  8. data/lib/www/delicious/errors.rb +46 -0
  9. data/lib/www/delicious/post.rb +123 -0
  10. data/lib/www/delicious/tag.rb +101 -0
  11. data/lib/www/delicious/version.rb +33 -0
  12. data/lib/www/delicious.rb +941 -0
  13. data/setup.rb +1585 -0
  14. data/test/bundle_test.rb +63 -0
  15. data/test/delicious_test.rb +372 -0
  16. data/test/fixtures/net_response_invalid_account.yml +25 -0
  17. data/test/fixtures/net_response_success.yml +23 -0
  18. data/test/online_test.rb +147 -0
  19. data/test/post_test.rb +68 -0
  20. data/test/tag_test.rb +69 -0
  21. data/test/test_all.rb +19 -0
  22. data/test/test_helper.rb +43 -0
  23. data/test/testcases/element/bundle.xml +1 -0
  24. data/test/testcases/element/invalid_root.xml +2 -0
  25. data/test/testcases/element/post.xml +2 -0
  26. data/test/testcases/element/post_unshared.xml +2 -0
  27. data/test/testcases/element/tag.xml +1 -0
  28. data/test/testcases/response/bundles_all.xml +5 -0
  29. data/test/testcases/response/bundles_all_empty.xml +2 -0
  30. data/test/testcases/response/bundles_delete.xml +2 -0
  31. data/test/testcases/response/bundles_set.xml +2 -0
  32. data/test/testcases/response/bundles_set_error.xml +2 -0
  33. data/test/testcases/response/posts_add.xml +2 -0
  34. data/test/testcases/response/posts_all.xml +12 -0
  35. data/test/testcases/response/posts_dates.xml +14 -0
  36. data/test/testcases/response/posts_dates_with_tag.xml +14 -0
  37. data/test/testcases/response/posts_delete.xml +2 -0
  38. data/test/testcases/response/posts_get.xml +7 -0
  39. data/test/testcases/response/posts_get_with_tag.xml +6 -0
  40. data/test/testcases/response/posts_recent.xml +19 -0
  41. data/test/testcases/response/posts_recent_with_tag.xml +19 -0
  42. data/test/testcases/response/tags_get.xml +5 -0
  43. data/test/testcases/response/tags_get_empty.xml +2 -0
  44. data/test/testcases/response/tags_rename.xml +2 -0
  45. data/test/testcases/response/update.delicious1.xml +2 -0
  46. data/test/testcases/response/update.xml +3 -0
  47. data/www-delicious.gemspec +44 -0
  48. metadata +151 -0
@@ -0,0 +1,73 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ module WWW
18
+ class Delicious
19
+
20
+ #
21
+ # = Abstract structure
22
+ #
23
+ # Represent the most basic structure all Struc(s) must inherith from.
24
+ #
25
+ class Element
26
+
27
+ #
28
+ # Initializes a new instance and populate attributes from +attrs+.
29
+ #
30
+ # class User < Element
31
+ # attr_accessor :first_name
32
+ # attr_accessor :last_name
33
+ # end
34
+ #
35
+ # User.new
36
+ # User.new(:first_name => 'foo')
37
+ # User.new(:first_name => 'John', :last_name => 'Doe')
38
+ #
39
+ # You can even use a block.
40
+ # The following statements are equals:
41
+ #
42
+ # User.new(:first_name => 'John', :last_name => 'Doe')
43
+ #
44
+ # User.new do |user|
45
+ # user.first_name => 'John'
46
+ # user.last_name => 'Doe'
47
+ # end
48
+ #
49
+ # Warning. In order to set an attribute a valid attribute writer must be available,
50
+ # otherwise this method will raise an exception.
51
+ #
52
+ def initialize(attrs = {}, &block)
53
+ attrs.each { |key, value| self.send("#{key}=".to_sym, value) }
54
+ yield self if block_given?
55
+ self
56
+ end
57
+
58
+
59
+ class << self
60
+
61
+ #
62
+ # Creates and returns new instance from a REXML +element+.
63
+ #
64
+ def from_rexml(element, options)
65
+ raise NotImplementedError
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,46 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ module WWW
18
+ class Delicious
19
+
20
+
21
+ #
22
+ # = WWW::Delicious::Error
23
+ #
24
+ # Base exception for all WWW::Delicious errors.
25
+ #
26
+ class Error < StandardError; end
27
+
28
+ #
29
+ # = WWW::Delicious::HTTPError
30
+ #
31
+ # HTTP connection related error.
32
+ # Raised when an HTTP request fails or in case of unexpected behavior.
33
+ #
34
+ class HTTPError < Error; end
35
+
36
+ #
37
+ # = WWW::Delicious::ResponseError
38
+ #
39
+ # Response related error.
40
+ # Usually raised in case of a malformed, invalid or empty XML response.
41
+ #
42
+ class ResponseError < Error; end
43
+
44
+
45
+ end
46
+ end
@@ -0,0 +1,123 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ require 'www/delicious/element'
18
+
19
+
20
+ module WWW
21
+ class Delicious
22
+
23
+ class Post < Element
24
+
25
+ # The Post URL
26
+ attr_accessor :url
27
+
28
+ # The title of the Post
29
+ attr_accessor :title
30
+
31
+ # The extended description for the Post
32
+ attr_accessor :notes
33
+
34
+ # The number of other users who saved this Post
35
+ attr_accessor :others
36
+
37
+ # The unique Id for this Post
38
+ attr_accessor :uid
39
+
40
+ # Tags for this Post
41
+ attr_accessor :tags
42
+
43
+ # Timestamp this Post was last saved at
44
+ attr_accessor :time
45
+
46
+ # Whether this Post must replace previous version of the same Post.
47
+ attr_accessor :replace
48
+
49
+ # Whether this Post is private
50
+ attr_accessor :shared
51
+
52
+
53
+ # Returns the value for <tt>shared</tt> attribute.
54
+ def shared
55
+ !(@shared == false)
56
+ end
57
+
58
+ # Returns the value for <tt>replace</tt> attribute.
59
+ def replace
60
+ !(@replace == false)
61
+ end
62
+
63
+ # Returns a params-style representation suitable for API calls.
64
+ def to_params()
65
+ params = {}
66
+ params[:url] = url
67
+ params[:description] = title
68
+ params[:extended] = notes if notes
69
+ params[:shared] = shared
70
+ params[:tags] = tags
71
+ params[:replace] = replace
72
+ params[:dt] = WWW::Delicious::TIME_CONVERTER.call(time) if time
73
+ params
74
+ end
75
+
76
+
77
+ #
78
+ # Returns whether this object is valid for an API request.
79
+ #
80
+ # To be valid +url+ and +title+ must not be empty.
81
+ #
82
+ # === Examples
83
+ #
84
+ # post = WWW::Delicious::Post.new(:url => 'http://localhost', :title => 'foo')
85
+ # post.api_valid?
86
+ # # => true
87
+ #
88
+ # post = WWW::Delicious::Post.new(:url => 'http://localhost')
89
+ # post.api_valid?
90
+ # # => false
91
+ #
92
+ def api_valid?
93
+ return !(url.nil? or url.empty? or title.nil? or title.empty?)
94
+ end
95
+
96
+
97
+ class << self
98
+
99
+ #
100
+ # Creates and returns new instance from a REXML +element+.
101
+ #
102
+ # Implements Element#from_rexml.
103
+ #
104
+ def from_rexml(element)
105
+ raise ArgumentError, "`element` expected to be a `REXML::Element`" unless element.kind_of? REXML::Element
106
+ self.new do |instance|
107
+ instance.url = element.if_attribute_value(:href) { |v| URI.parse(v) }
108
+ instance.title = element.if_attribute_value(:description)
109
+ instance.notes = element.if_attribute_value(:extended)
110
+ instance.others = element.if_attribute_value(:others).to_i # cast nil to 0
111
+ instance.uid = element.if_attribute_value(:hash)
112
+ instance.tags = element.if_attribute_value(:tag) { |v| v.split(' ') }.to_a
113
+ instance.time = element.if_attribute_value(:time) { |v| Time.parse(v) }
114
+ instance.shared = element.if_attribute_value(:shared) { |v| v == 'no' ? false : true }
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,101 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ require 'www/delicious/element'
18
+
19
+
20
+ module WWW
21
+ class Delicious
22
+
23
+ #
24
+ # = Delicious Tag
25
+ #
26
+ # Represents a single Tag element.
27
+ #
28
+ class Tag < Element
29
+
30
+ # The name of the tag.
31
+ attr_accessor :name
32
+
33
+ # The number of links tagged with this tag.
34
+ # It should be set only from an API response.
35
+ attr_accessor :count
36
+
37
+
38
+ # Returns value for <tt>name</tt> attribute.
39
+ # Value is always normalized as lower string.
40
+ def name
41
+ @name.to_s.strip unless @name.nil?
42
+ end
43
+
44
+ # Returns value for <tt>count</tt> attribute.
45
+ # Value is always normalized to Fixnum.
46
+ def count
47
+ @count.to_i
48
+ end
49
+
50
+ #
51
+ # Returns a string representation of this Tag.
52
+ # In case name is nil this method will return an empty string.
53
+ #
54
+ def to_s
55
+ name.to_s
56
+ end
57
+
58
+
59
+ public
60
+ #
61
+ # Returns wheter this object is valid for an API request.
62
+ #
63
+ # To be valid +name+ must not be empty.
64
+ # +count+ can be 0.
65
+ #
66
+ # === Examples
67
+ #
68
+ # tag = WWW::Delicious::Tag.new(:name => 'foo')
69
+ # tag.api_api_valid?
70
+ # # => true
71
+ #
72
+ # tag = WWW::Delicious::Tag.new(:name => ' ')
73
+ # tag.api_api_valid?
74
+ # # => false
75
+ #
76
+ def api_valid?
77
+ return !name.empty?
78
+ end
79
+
80
+
81
+ class << self
82
+
83
+ #
84
+ # Creates and returns new instance from a REXML +element+.
85
+ #
86
+ # Implements Element#from_rexml.
87
+ #
88
+ def from_rexml(element)
89
+ raise ArgumentError, "`element` expected to be a `REXML::Element`" unless element.kind_of? REXML::Element
90
+ self.new do |instance|
91
+ instance.name = element.if_attribute_value(:tag)
92
+ instance.count = element.if_attribute_value(:count) { |value| value.to_i }
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ module WWW
18
+ class Delicious
19
+
20
+ module Version
21
+ MAJOR = 0
22
+ MINOR = 3
23
+ TINY = 0
24
+
25
+ STRING = [MAJOR, MINOR, TINY].join('.')
26
+ end
27
+
28
+ VERSION = Version::STRING
29
+ STATUS = 'beta'
30
+ BUILD = ''.match(/(\d+)/).to_a.first
31
+
32
+ end
33
+ end
@@ -0,0 +1,941 @@
1
+ #
2
+ # = WWW::Delicious
3
+ #
4
+ # Ruby client for del.icio.us API.
5
+ #
6
+ #
7
+ # Category:: WWW
8
+ # Package:: WWW::Delicious
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # License:: MIT License
11
+ #
12
+ #--
13
+ #
14
+ #++
15
+
16
+
17
+ require 'net/https'
18
+ require 'rexml/document'
19
+ require 'time'
20
+ require 'www/delicious/bundle'
21
+ require 'www/delicious/post'
22
+ require 'www/delicious/tag'
23
+ require 'www/delicious/errors'
24
+ require 'www/delicious/version'
25
+
26
+
27
+ module WWW #:nodoc:
28
+
29
+
30
+ #
31
+ # = WWW::Delicious
32
+ #
33
+ # WWW::Delicious is a Ruby client for http://del.icio.us XML API.
34
+ #
35
+ # It provides both read and write functionalities.
36
+ # You can read user Posts, Tags and Bundles
37
+ # but you can create new Posts, Tags and Bundles as well.
38
+ #
39
+ #
40
+ # == Basic Usage
41
+ #
42
+ # The following is just a basic demonstration of the main features.
43
+ # See the README file for a deeper explanation about how to get the best
44
+ # from WWW::Delicious library.
45
+ #
46
+ # The examples in this page make the following assumptions
47
+ # * you have a valid del.icio.us account
48
+ # * +username+ is your account username
49
+ # * +password+ is your account password
50
+ #
51
+ # In order to make a query you first need to create
52
+ # a new WWW::Delicious instance as follows:
53
+ #
54
+ # require 'www/delicious'
55
+ #
56
+ # username = 'my delicious username'
57
+ # password = 'my delicious password'
58
+ #
59
+ # d = WWW::Delicious.new(username, password)
60
+ #
61
+ # The constructor accepts some additional options.
62
+ # For instance, if you want to customize the user agent:
63
+ #
64
+ # d = WWW::Delicious.new(username, password, :user_agent => 'FooAgent')
65
+ #
66
+ # Now you can use any of the API methods available.
67
+ #
68
+ # For example, you may want to know when your account was last updated
69
+ # to check whether someone else made some changes on behalf of you:
70
+ #
71
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
72
+ #
73
+ # Because the answer is a valid +Time+ instance, you can format it with +strftime+.
74
+ #
75
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
76
+ # datetime.strftime('%Y') # => 2008
77
+ #
78
+ class Delicious
79
+
80
+ NAME = 'WWW::Delicious'
81
+ GEM = 'www-delicious'
82
+ AUTHOR = 'Simone Carletti <weppos@weppos.net>'
83
+
84
+ # del.icio.us account username
85
+ attr_reader :username
86
+
87
+ # del.icio.us account password
88
+ attr_reader :password
89
+
90
+ # base URI for del.icio.us API
91
+ attr_reader :base_uri
92
+
93
+
94
+ # API Base URL
95
+ API_BASE_URI = 'https://api.del.icio.us'
96
+
97
+ # API Path Update
98
+ API_PATH_UPDATE = '/v1/posts/update';
99
+
100
+ # API Path All Bundles
101
+ API_PATH_BUNDLES_ALL = '/v1/tags/bundles/all';
102
+ # API Path Set Bundle
103
+ API_PATH_BUNDLES_SET = '/v1/tags/bundles/set';
104
+ # API Path Delete Bundle
105
+ API_PATH_BUNDLES_DELETE = '/v1/tags/bundles/delete';
106
+
107
+ # API Path Get Tags
108
+ API_PATH_TAGS_GET = '/v1/tags/get';
109
+ # API Path Rename Tag
110
+ API_PATH_TAGS_RENAME = '/v1/tags/rename';
111
+
112
+ # API Path Get Posts
113
+ API_PATH_POSTS_GET = '/v1/posts/get';
114
+ # API Path Recent Posts
115
+ API_PATH_POSTS_RECENT = '/v1/posts/recent';
116
+ # API Path All Posts
117
+ API_PATH_POSTS_ALL = '/v1/posts/all';
118
+ # API Path Posts by Dates
119
+ API_PATH_POSTS_DATES = '/v1/posts/dates';
120
+ # API Path Add Post
121
+ API_PATH_POSTS_ADD = '/v1/posts/add';
122
+ # API Path Delete Post
123
+ API_PATH_POSTS_DELETE = '/v1/posts/delete';
124
+
125
+ # Time to wait before sending a new request, in seconds
126
+ SECONDS_BEFORE_NEW_REQUEST = 1
127
+
128
+ # Time converter converts a Time instance into the format
129
+ # requested by Delicious API
130
+ TIME_CONVERTER = lambda { |time| time.iso8601() }
131
+
132
+
133
+ #
134
+ # Constructs a new <tt>WWW::Delicious</tt> object
135
+ # with given +username+ and +password+.
136
+ #
137
+ # # create a new object with username 'user' and password 'psw
138
+ # obj = WWW::Delicious('user', 'psw')
139
+ # # => self
140
+ #
141
+ # If a block is given, the instance is passed to the block
142
+ # but this method always returns the instance itself.
143
+ #
144
+ # WWW::Delicious('user', 'psw') do |d|
145
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
146
+ # end
147
+ # # => self
148
+ #
149
+ # You can also specify some additional options, including a custom user agent
150
+ # or the base URI for del.icio.us API.
151
+ #
152
+ # WWW::Delicious('user', 'psw', :base_uri => 'https://ma.gnolia.com/api/mirrord') do |d|
153
+ # # the following call is mirrored by ma.gnolia
154
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
155
+ # end
156
+ # # => self
157
+ #
158
+ # === Options
159
+ # This class accepts a Hash with additional options.
160
+ # Here's the list of valid keys:
161
+ #
162
+ # <tt>:user_agent</tt>:: User agent to display in HTTP requests.
163
+ # <tt>:base_uri</tt>:: The base URI to del.icio.us API.
164
+ #
165
+ def initialize(username, password, options = {}, &block) # :yields: delicious
166
+ @username, @password = username.to_s, password.to_s
167
+
168
+ # set API base URI
169
+ @base_uri = URI.parse(options[:base_uri] || API_BASE_URI)
170
+
171
+ init_user_agent(options)
172
+ init_http_client(options)
173
+
174
+ yield self if block_given?
175
+ self # ensure to always return self even if block is given
176
+ end
177
+
178
+
179
+ #
180
+ # Returns the reference to current <tt>@http_client</tt>.
181
+ # The http is always valid unless it has been previously set to +nil+.
182
+ #
183
+ # # nil client
184
+ # obj.http_client # => nil
185
+ #
186
+ # # valid client
187
+ # obj.http_client # => Net::HTTP
188
+ #
189
+ def http_client()
190
+ return @http_client
191
+ end
192
+
193
+ #
194
+ # Sets the internal <tt>@http_client</tt> to +client+.
195
+ #
196
+ # # nil client
197
+ # obj.http_client = nil
198
+ #
199
+ # # http client
200
+ # obj.http_client = Net::HTTP.new()
201
+ #
202
+ # # invalid client
203
+ # obj.http_client = 'foo' # => ArgumentError
204
+ #
205
+ def http_client=(client)
206
+ unless client.kind_of?(Net::HTTP) or client.nil?
207
+ raise ArgumentError, "`client` expected to be a kind of `Net::HTTP`, `#{client.class}` given"
208
+ end
209
+ @http_client = client
210
+ end
211
+
212
+ # Returns current user agent string.
213
+ def user_agent()
214
+ return @headers['User-Agent']
215
+ end
216
+
217
+
218
+ #
219
+ # Returns true if given account credentials are valid.
220
+ #
221
+ # d = WWW::Delicious.new('username', 'password')
222
+ # d.valid_account? # => true
223
+ #
224
+ # d = WWW::Delicious.new('username', 'invalid_password')
225
+ # d.valid_account? # => false
226
+ #
227
+ # This method is not "exception safe".
228
+ # It doesn't return false if an HTTP error or any kind of other error occurs,
229
+ # it raises back the exception to the caller instead.
230
+ #
231
+ #
232
+ # Raises:: WWW::Delicious::Error
233
+ # Raises:: WWW::Delicious::HTTPError
234
+ # Raises:: WWW::Delicious::ResponseError
235
+ #
236
+ def valid_account?
237
+ update()
238
+ return true
239
+ rescue HTTPError => e
240
+ return false if e.message =~ /invalid username or password/i
241
+ raise
242
+ end
243
+
244
+ #
245
+ # Checks to see when a user last posted an item
246
+ # and returns the last update +Time+ for the user.
247
+ #
248
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
249
+ #
250
+ #
251
+ # Raises:: WWW::Delicious::Error
252
+ # Raises:: WWW::Delicious::HTTPError
253
+ # Raises:: WWW::Delicious::ResponseError
254
+ #
255
+ def update()
256
+ response = request(API_PATH_UPDATE)
257
+ return parse_update_response(response.body)
258
+ end
259
+
260
+ #
261
+ # Retrieves all of a user's bundles
262
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
263
+ #
264
+ # d.bundles_all() # => [#<WWW::Delicious::Bundle>, #<WWW::Delicious::Bundle>, ...]
265
+ # d.bundles_all() # => []
266
+ #
267
+ #
268
+ # Raises:: WWW::Delicious::Error
269
+ # Raises:: WWW::Delicious::HTTPError
270
+ # Raises:: WWW::Delicious::ResponseError
271
+ #
272
+ def bundles_all()
273
+ response = request(API_PATH_BUNDLES_ALL)
274
+ return parse_bundle_collection(response.body)
275
+ end
276
+
277
+ #
278
+ # Assignes a set of tags to a single bundle,
279
+ # wipes away previous settings for bundle.
280
+ #
281
+ # # create from a bundle
282
+ # d.bundles_set(WWW::Delicious::Bundle.new('MyBundle'), %w(foo bar))
283
+ #
284
+ # # create from a string
285
+ # d.bundles_set('MyBundle', %w(foo bar))
286
+ #
287
+ #
288
+ # Raises:: WWW::Delicious::Error
289
+ # Raises:: WWW::Delicious::HTTPError
290
+ # Raises:: WWW::Delicious::ResponseError
291
+ #
292
+ def bundles_set(bundle_or_name, tags = [])
293
+ params = prepare_bundles_set_params(bundle_or_name, tags)
294
+ response = request(API_PATH_BUNDLES_SET, params)
295
+ return parse_and_eval_execution_response(response.body)
296
+ end
297
+
298
+ #
299
+ # Deletes +bundle_or_name+ bundle from del.icio.us.
300
+ # +bundle_or_name+ can be either a WWW::Delicious::Bundle instance
301
+ # or a string with the name of the bundle.
302
+ #
303
+ # This method doesn't care whether the exists.
304
+ # If not, the execution will silently return without rising any error.
305
+ #
306
+ # # delete from a bundle
307
+ # d.bundles_delete(WWW::Delicious::Bundle.new('MyBundle'))
308
+ #
309
+ # # delete from a string
310
+ # d.bundles_delete('MyBundle', %w(foo bar))
311
+ #
312
+ #
313
+ # Raises:: WWW::Delicious::Error
314
+ # Raises:: WWW::Delicious::HTTPError
315
+ # Raises:: WWW::Delicious::ResponseError
316
+ #
317
+ def bundles_delete(bundle_or_name)
318
+ params = prepare_bundles_delete_params(bundle_or_name)
319
+ response = request(API_PATH_BUNDLES_DELETE, params)
320
+ return parse_and_eval_execution_response(response.body)
321
+ end
322
+
323
+ #
324
+ # Retrieves the list of tags and number of times used by the user
325
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
326
+ #
327
+ # d.tags_get() # => [#<WWW::Delicious::Tag>, #<WWW::Delicious::Tag>, ...]
328
+ # d.tags_get() # => []
329
+ #
330
+ #
331
+ # Raises:: WWW::Delicious::Error
332
+ # Raises:: WWW::Delicious::HTTPError
333
+ # Raises:: WWW::Delicious::ResponseError
334
+ #
335
+ def tags_get()
336
+ response = request(API_PATH_TAGS_GET)
337
+ return parse_tag_collection(response.body)
338
+ end
339
+
340
+ #
341
+ # Renames an existing tag with a new tag name.
342
+ #
343
+ # # rename from a tag
344
+ # d.bundles_set(WWW::Delicious::Tag.new('old'), WWW::Delicious::Tag.new('new'))
345
+ #
346
+ # # rename from a string
347
+ # d.bundles_set('old', 'new')
348
+ #
349
+ #
350
+ # Raises:: WWW::Delicious::Error
351
+ # Raises:: WWW::Delicious::HTTPError
352
+ # Raises:: WWW::Delicious::ResponseError
353
+ #
354
+ def tags_rename(from_name_or_tag, to_name_or_tag)
355
+ params = prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
356
+ response = request(API_PATH_TAGS_RENAME, params)
357
+ return parse_and_eval_execution_response(response.body)
358
+ end
359
+
360
+ #
361
+ # Returns an array of <tt>WWW::Delicious::Post</tt> matching +options+.
362
+ # If no option is given, the last post is returned.
363
+ # If no date or url is given, most recent date will be used.
364
+ #
365
+ # d.posts_get() # => [#<WWW::Delicious::Post>, #<WWW::Delicious::Post>, ...]
366
+ # d.posts_get() # => []
367
+ #
368
+ # # get all posts tagged with ruby
369
+ # d.posts_get(:tag => WWW::Delicious::Tag.new('ruby))
370
+ #
371
+ # # get all posts matching URL 'http://www.simonecarletti.com'
372
+ # d.posts_get(:url => URI.parse('http://www.simonecarletti.com'))
373
+ #
374
+ # # get all posts tagged with ruby and matching URL 'http://www.simonecarletti.com'
375
+ # d.posts_get(:tag => WWW::Delicious::Tag.new('ruby),
376
+ # :url => URI.parse('http://www.simonecarletti.com'))
377
+ #
378
+ #
379
+ # === Options
380
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
381
+ # <tt>:dt</tt>:: a +Time+ with a date to filter by.
382
+ # <tt>:url</tt>:: a valid URI to filter by. It can be either an instance of +URI+ or a +String+.
383
+ #
384
+ # Raises:: WWW::Delicious::Error
385
+ # Raises:: WWW::Delicious::HTTPError
386
+ # Raises:: WWW::Delicious::ResponseError
387
+ #
388
+ def posts_get(options = {})
389
+ params = prepare_posts_params(options.clone, [:dt, :tag, :url])
390
+ response = request(API_PATH_POSTS_GET, params)
391
+ return parse_post_collection(response.body)
392
+ end
393
+
394
+ #
395
+ # Returns a list of the most recent posts, filtered by argument.
396
+ #
397
+ # # get the most recent posts
398
+ # d.posts_recent()
399
+ #
400
+ # # get the 10 most recent posts
401
+ # d.posts_recent(:count => 10)
402
+ #
403
+ #
404
+ # === Options
405
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
406
+ # <tt>:count</tt>:: number of items to retrieve. (default: 15, maximum: 100).
407
+ #
408
+ def posts_recent(options = {})
409
+ params = prepare_posts_params(options.clone, [:count, :tag])
410
+ response = request(API_PATH_POSTS_RECENT, params)
411
+ return parse_post_collection(response.body)
412
+ end
413
+
414
+ #
415
+ # Returns a list of all posts, filtered by argument.
416
+ #
417
+ # # get all (this is a very expensive query)
418
+ # d.posts_all
419
+ #
420
+ # # get all posts matching ruby
421
+ # d.posts_all(:tag => WWW::Delicious::Tag.new('ruby'))
422
+ #
423
+ #
424
+ # === Options
425
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
426
+ #
427
+ def posts_all(options = {})
428
+ params = prepare_posts_params(options.clone, [:tag])
429
+ response = request(API_PATH_POSTS_ALL, params)
430
+ return parse_post_collection(response.body)
431
+ end
432
+
433
+ #
434
+ # Returns a list of dates with the number of posts at each date.
435
+ #
436
+ # # get number of posts per date
437
+ # d.posts_dates
438
+ # # => { '2008-05-05' => 12, '2008-05-06' => 3, ... }
439
+ #
440
+ # # get number posts per date tagged as ruby
441
+ # d.posts_dates(:tag => WWW::Delicious::Tag.new('ruby'))
442
+ # # => { '2008-05-05' => 10, '2008-05-06' => 3, ... }
443
+ #
444
+ #
445
+ # === Options
446
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
447
+ #
448
+ def posts_dates(options = {})
449
+ params = prepare_posts_params(options.clone, [:tag])
450
+ response = request(API_PATH_POSTS_DATES, params)
451
+ return parse_posts_dates_response(response.body)
452
+ end
453
+
454
+ #
455
+ # Add a post to del.icio.us.
456
+ # +post_or_values+ can be either a +WWW::Delicious::Post+ instance
457
+ # or a Hash of params. This method accepts all params available
458
+ # to initialize a new +WWW::Delicious::Post+.
459
+ #
460
+ # # add a post from WWW::Delicious::Post
461
+ # d.posts_add(WWW::Delicious::Post.new(:url => 'http://www.foobar.com', :title => 'Hello world!'))
462
+ #
463
+ # # add a post from values
464
+ # d.posts_add(:url => 'http://www.foobar.com', :title => 'Hello world!')
465
+ #
466
+ #
467
+ def posts_add(post_or_values)
468
+ params = prepare_param_post(post_or_values).to_params
469
+ response = request(API_PATH_POSTS_ADD, params)
470
+ return parse_and_eval_execution_response(response.body)
471
+ end
472
+
473
+ #
474
+ # Deletes the post matching given +url+ from del.icio.us.
475
+ # +url+ can be either an URI instance or a string representation of a valid URL.
476
+ #
477
+ # This method doesn't care whether a post with given +url+ exists.
478
+ # If not, the execution will silently return without rising any error.
479
+ #
480
+ # # delete a post from URI
481
+ # d.post_delete(URI.parse('http://www.foobar.com/'))
482
+ #
483
+ # # delete a post from a string
484
+ # d.post_delete('http://www.foobar.com/')
485
+ #
486
+ #
487
+ def posts_delete(url)
488
+ params = prepare_posts_params({:url => url}, [:url])
489
+ response = request(API_PATH_POSTS_DELETE, params)
490
+ return parse_and_eval_execution_response(response.body)
491
+ end
492
+
493
+
494
+ protected
495
+
496
+ # Initializes the HTTP client.
497
+ # It automatically enable +use_ssl+ flag according to +@base_uri+ scheme.
498
+ def init_http_client(options)
499
+ http = Net::HTTP.new(@base_uri.host, 443)
500
+ http.use_ssl = true if @base_uri.scheme == "https"
501
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: not 100% supported
502
+ self.http_client = http
503
+ end
504
+
505
+ # Initializes user agent value for HTTP requests.
506
+ def init_user_agent(options)
507
+ user_agent = options[:user_agent] || default_user_agent()
508
+ @headers ||= {}
509
+ @headers['User-Agent'] = user_agent
510
+ end
511
+
512
+ #
513
+ # Creates and returns the default user agent string.
514
+ #
515
+ # By default, the user agent is composed by the following schema:
516
+ # <tt>NAME/VERSION (Ruby/RUBY_VERSION)</tt>
517
+ #
518
+ # * +NAME+ is the constant representing this library name
519
+ # * +VERSION+ is the constant representing current library version
520
+ # * +RUBY_VERSION+ is the version of Ruby interpreter the library is interpreted by
521
+ #
522
+ # default_user_agent
523
+ # # => WWW::Delicious/0.1.0 (Ruby/1.8.6)
524
+ #
525
+ def default_user_agent
526
+ return "#{NAME}/#{VERSION} (Ruby/#{RUBY_VERSION})"
527
+ end
528
+
529
+
530
+ #
531
+ # Composes an HTTP query string from an hash of +options+.
532
+ # The result is URI encoded.
533
+ #
534
+ # http_build_query(:foo => 'baa', :bar => 'boo')
535
+ # # => foo=baa&bar=boo
536
+ #
537
+ def http_build_query(params = {})
538
+ return params.collect do |k,v|
539
+ "#{URI.encode(k.to_s)}=#{URI.encode(v.to_s)}" unless v.nil?
540
+ end.compact.join('&')
541
+ end
542
+
543
+ #
544
+ # Sends an HTTP GET request to +path+ and appends given +params+.
545
+ #
546
+ # This method is 100% compliant with Delicious API reference.
547
+ # It waits at least 1 second between each HTTP request and
548
+ # provides an identifiable user agent by default,
549
+ # or the custom user agent set by +user_agent+ option
550
+ # when this istance has been created.
551
+ #
552
+ # request('/v1/api/path', :foo => 1, :bar => 2)
553
+ # # => sends a GET request to /v1/api/path?foo=1&bar=2
554
+ #
555
+ def request(path, params = {})
556
+ raise Error, 'Invalid HTTP Client' unless http_client
557
+ wait_before_new_request
558
+
559
+ uri = @base_uri.merge(path)
560
+ uri.query = http_build_query(params) unless params.empty?
561
+
562
+ begin
563
+ @last_request = Time.now # see #wait_before_new_request
564
+ @last_request_uri = uri # useful for debug
565
+ response = make_request(uri)
566
+ rescue => e # catch EOFError, SocketError and more
567
+ raise HTTPError, e.message
568
+ end
569
+
570
+ case response
571
+ when Net::HTTPSuccess
572
+ return response
573
+ when Net::HTTPUnauthorized # 401
574
+ raise HTTPError, 'Invalid username or password'
575
+ when Net::HTTPServiceUnavailable # 503
576
+ raise HTTPError, 'You have been throttled.' +
577
+ 'Please ensure you are waiting at least one second before each request.'
578
+ else
579
+ raise HTTPError, "HTTP #{response.code}: #{response.message}"
580
+ end
581
+ end
582
+
583
+ # Makes the real HTTP request to given +uri+ and returns the +response+.
584
+ # This method exists basically to simplify unit testing with mocha.
585
+ def make_request(uri)
586
+ http_client.start do |http|
587
+ req = Net::HTTP::Get.new(uri.request_uri, @headers)
588
+ req.basic_auth(@username, @password)
589
+ http.request(req)
590
+ end
591
+ end
592
+
593
+ #
594
+ # Delicious API reference requests to wait AT LEAST ONE SECOND
595
+ # between queries or the client is likely to get automatically throttled.
596
+ #
597
+ # This method calculates the difference between current time
598
+ # and the last request time and wait for the necessary time to meet
599
+ # SECONDS_BEFORE_NEW_REQUEST requirement.
600
+ #
601
+ # The difference is not rounded. If you only have to wait for 0.034 seconds
602
+ # then your don't have to wait 0 or 1 seconds, but 0.034 seconds!
603
+ #
604
+ def wait_before_new_request
605
+ return unless @last_request # this is the first request
606
+ # puts "Last request at #{TIME_CONVERTER.call(@last_request)}" if debug?
607
+ diff = Time.now - @last_request
608
+ if diff < SECONDS_BEFORE_NEW_REQUEST
609
+ # puts "Sleeping for #{diff} before new request..." if debug?
610
+ sleep(SECONDS_BEFORE_NEW_REQUEST - diff)
611
+ end
612
+ end
613
+
614
+
615
+ #
616
+ # Parses the response <tt>body</tt> and runs a common set of validators.
617
+ # Returns <tt>body</tt> as parsed REXML::Document on success.
618
+ #
619
+ # Raises:: WWW::Delicious::ResponseError in case of invalid response.
620
+ #
621
+ def parse_and_validate_response(body, options = {})
622
+ dom = REXML::Document.new(body)
623
+
624
+ if (value = options[:root_name]) && dom.root.name != value
625
+ raise ResponseError, "Invalid response, root node is not `#{value}`"
626
+ end
627
+ if (value = options[:root_text]) && dom.root.text != value
628
+ raise ResponseError, value
629
+ end
630
+
631
+ return dom
632
+ end
633
+
634
+ #
635
+ # Parses and evaluates the response returned by an execution,
636
+ # usually an update/delete/insert operation.
637
+ #
638
+ # Raises:: WWW::Delicious::ResponseError in case of invalid response
639
+ # Raises:: WWW::Delicious::Error in case of execution error
640
+ #
641
+ def parse_and_eval_execution_response(body)
642
+ dom = parse_and_validate_response(body, :root_name => 'result')
643
+ response = dom.root.if_attribute_value(:code)
644
+ response = dom.root.text if response.nil?
645
+ raise Error, "Invalid response, #{response}" unless %w(done ok).include?(response)
646
+ true
647
+ end
648
+
649
+ # Parses the response of an Update request
650
+ # and returns the update Timestamp.
651
+ def parse_update_response(body)
652
+ dom = parse_and_validate_response(body, :root_name => 'update')
653
+ dom.root.if_attribute_value(:time) { |v| Time.parse(v) }
654
+ end
655
+
656
+ # Parses a response containing a collection of Bundles
657
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
658
+ def parse_bundle_collection(body)
659
+ dom = parse_and_validate_response(body, :root_name => 'bundles')
660
+ dom.root.elements.collect('bundle') { |xml| Bundle.from_rexml(xml) }
661
+ end
662
+
663
+ # Parses a response containing a collection of Tags
664
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
665
+ def parse_tag_collection(body)
666
+ dom = parse_and_validate_response(body, :root_name => 'tags')
667
+ dom.root.elements.collect('tag') { |xml| Tag.from_rexml(xml) }
668
+ end
669
+
670
+ # Parses a response containing a collection of Posts
671
+ # and returns an array of <tt>WWW::Delicious::Post</tt>.
672
+ def parse_post_collection(body)
673
+ dom = parse_and_validate_response(body, :root_name => 'posts')
674
+ dom.root.elements.collect('post') { |xml| Post.from_rexml(xml) }
675
+ end
676
+
677
+ # Parses the response of a <tt>posts_dates</tt> request
678
+ # and returns a +Hash+ of date => count.
679
+ def parse_posts_dates_response(body)
680
+ dom = parse_and_validate_response(body, :root_name => 'dates')
681
+ return dom.root.get_elements('date').inject({}) do |collection, xml|
682
+ date = xml.if_attribute_value(:date)
683
+ count = xml.if_attribute_value(:count)
684
+ collection.merge({ date => count })
685
+ end
686
+ end
687
+
688
+
689
+ #
690
+ # Prepares the params for a `bundles_set` call
691
+ # and returns a Hash with the params ready for the HTTP request.
692
+ #
693
+ # Raises:: WWW::Delicious::Error
694
+ #
695
+ def prepare_bundles_set_params(name_or_bundle, tags = [])
696
+ bundle = prepare_param_bundle(name_or_bundle, tags) do |b|
697
+ raise Error, "Bundle name is empty" if b.name.empty?
698
+ raise Error, "Bundle must contain at least one tag" if b.tags.empty?
699
+ end
700
+ return { :bundle => bundle.name, :tags => bundle.tags.join(' ') }
701
+ end
702
+
703
+ #
704
+ # Prepares the params for a `bundles_set` call
705
+ # and returns a Hash with the params ready for the HTTP request.
706
+ #
707
+ # Raises:: WWW::Delicious::Error
708
+ #
709
+ def prepare_bundles_delete_params(name_or_bundle)
710
+ bundle = prepare_param_bundle(name_or_bundle) do |b|
711
+ raise Error, "Bundle name is empty" if b.name.empty?
712
+ end
713
+ return { :bundle => bundle.name }
714
+ end
715
+
716
+ #
717
+ # Prepares the params for a `tags_rename` call
718
+ # and returns a Hash with the params ready for the HTTP request.
719
+ #
720
+ # Raises:: WWW::Delicious::Error
721
+ #
722
+ def prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
723
+ from, to = [from_name_or_tag, to_name_or_tag].collect do |v|
724
+ prepare_param_tag(v)
725
+ end
726
+ return { :old => from, :new => to }
727
+ end
728
+
729
+ #
730
+ # Prepares the params for a `post_*` call
731
+ # and returns a Hash with the params ready for the HTTP request.
732
+ #
733
+ # Raises:: WWW::Delicious::Error
734
+ #
735
+ def prepare_posts_params(params, allowed_params = [])
736
+ compare_params(params, allowed_params)
737
+
738
+ # we don't need to check whether the following parameters
739
+ # are valid for this request because compare_params
740
+ # would raise if an invalid param is supplied
741
+
742
+ params[:tag] = prepare_param_tag(params[:tag]) if params[:tag]
743
+ params[:dt] = TIME_CONVERTER.call(params[:dt]) if params[:dt]
744
+ params[:url] = URI.parse(params[:url]) if params[:url]
745
+ params[:count] = if value = params[:count]
746
+ raise Error, 'Expected `count` <= 100' if value.to_i() > 100 # requirement
747
+ value.to_i
748
+ else
749
+ 15 # default value
750
+ end
751
+
752
+ return params
753
+ end
754
+
755
+
756
+ #
757
+ # Prepares the +post+ param for an API request.
758
+ #
759
+ # Creates and returns a <tt>WWW::Delicious::Post</tt> instance from <tt>post_or_values</tt>.
760
+ # <tt>post_or_values</tt> can be either an Hash with post attributes
761
+ # or a <tt>WWW::Delicious::Post</tt> instance.
762
+ #
763
+ def prepare_param_post(post_or_values, &block)
764
+ post = case post_or_values
765
+ when WWW::Delicious::Post
766
+ post_or_values
767
+ when Hash
768
+ Post.new(post_or_values)
769
+ else
770
+ raise ArgumentError, 'Expected `args` to be `WWW::Delicious::Post` or `Hash`'
771
+ end
772
+
773
+ yield(post) if block_given?
774
+ # TODO: validate post with post.validate!
775
+ raise ArgumentError, 'Both `url` and `title` are required' unless post.api_valid?
776
+ post
777
+ end
778
+
779
+ #
780
+ # Prepares the +bundle+ param for an API request.
781
+ #
782
+ # Creates and returns a <tt>WWW::Delicious::Bundle</tt> instance from <tt>name_or_bundle</tt>.
783
+ # <tt>name_or_bundle</tt> can be either a string holding bundle name
784
+ # or a <tt>WWW::Delicious::Bundle</tt> instance.
785
+ #
786
+ def prepare_param_bundle(name_or_bundle, tags = [], &block) # :yields: bundle
787
+ bundle = case name_or_bundle
788
+ when WWW::Delicious::Bundle
789
+ name_or_bundle
790
+ else
791
+ Bundle.new(:name => name_or_bundle, :tags => tags)
792
+ end
793
+
794
+ yield(bundle) if block_given?
795
+ # TODO: validate bundle with bundle.validate!
796
+ bundle
797
+ end
798
+
799
+ #
800
+ # Prepares the +tag+ param for an API request.
801
+ #
802
+ # Creates and returns a <tt>WWW::Delicious::Tag</tt> instance from <tt>name_or_tag</tt>.
803
+ # <tt>name_or_tag</tt> can be either a string holding tag name
804
+ # or a <tt>WWW::Delicious::Tag</tt> instance.
805
+ #
806
+ def prepare_param_tag(name_or_tag, &block) # :yields: tag
807
+ tag = case name_or_tag
808
+ when WWW::Delicious::Tag
809
+ name_or_tag
810
+ else
811
+ Tag.new(:name => name_or_tag.to_s)
812
+ end
813
+
814
+ yield(tag) if block_given?
815
+ # TODO: validate tag with tag.validate!
816
+ raise "Invalid `tag` value supplied" unless tag.api_valid?
817
+ tag
818
+ end
819
+
820
+ #
821
+ # Checks whether user given +params+ are valid against a defined collection of +valid_params+.
822
+ #
823
+ # === Examples
824
+ #
825
+ # params = {:foo => 1, :bar => 2}
826
+ #
827
+ # compare_params(params, [:foo, :bar])
828
+ # # => valid
829
+ #
830
+ # compare_params(params, [:foo, :bar, :baz])
831
+ # # => raises
832
+ #
833
+ # compare_params(params, [:foo])
834
+ # # => raises
835
+ #
836
+ # Raises:: WWW::Delicious::Error
837
+ #
838
+ def compare_params(params, valid_params)
839
+ raise ArgumentError, "Expected `params` to be a kind of `Hash`" unless params.kind_of?(Hash)
840
+ raise ArgumentError, "Expected `valid_params` to be a kind of `Array`" unless valid_params.kind_of?(Array)
841
+
842
+ # compute options difference
843
+ difference = params.keys - valid_params
844
+ raise Error, "Invalid params: `#{difference.join('`, `')}`" unless difference.empty?
845
+ end
846
+
847
+
848
+ module XMLUtils #:nodoc:
849
+
850
+ #
851
+ # Returns the +xmlattr+ attribute value for current <tt>REXML::Element</tt>.
852
+ #
853
+ # If block is given and attribute value is not nil,
854
+ # the content of the block is executed.
855
+ #
856
+ # === Examples
857
+ #
858
+ # dom = REXML::Document.new('<a name="1"><b>foo</b><b>bar</b></a>')
859
+ #
860
+ # dom.root.if_attribute_value(:name)
861
+ # # => "1"
862
+ #
863
+ # dom.root.if_attribute_value(:name) { |v| v.to_i }
864
+ # # => 1
865
+ #
866
+ # dom.root.if_attribute_value(:foo)
867
+ # # => nil
868
+ #
869
+ # dom.root.if_attribute_value(:name) { |v| v.to_i }
870
+ # # => nil
871
+ #
872
+ def if_attribute_value(xmlattr, &block) #:nodoc:
873
+ value = if attr = self.attribute(xmlattr.to_s)
874
+ attr.value
875
+ else
876
+ nil
877
+ end
878
+ value = yield value if !value.nil? and block_given?
879
+ value
880
+ end
881
+
882
+ #
883
+ # Returns the value of +expression+ child of this element, if it exists.
884
+ # If blog is given, block is called on +expression+ element value
885
+ # and the result is returned.
886
+ #
887
+ def if_element_value(expression, &block)
888
+ if_element(expression) do |element|
889
+ value = element.text
890
+ value = yield value if block_given?
891
+ value
892
+ end
893
+ end
894
+
895
+ #
896
+ # Executes the content of +block+ on +expression+
897
+ # child of this element, if it exists.
898
+ # Returns the result or +nil+ if +xmlelement+ doesn't exist.
899
+ #
900
+ def if_element(expression, &block)
901
+ raise LocalJumpError, "no block given" unless block_given?
902
+ if element = self.elements[expression.to_s]
903
+ yield element
904
+ else
905
+ nil
906
+ end
907
+ end
908
+
909
+ end # XMLUtils
910
+
911
+ end
912
+ end
913
+
914
+
915
+ class Object
916
+
917
+ # An object is blank if it's false, empty, or a whitespace string.
918
+ # For example, "", " ", +nil+, [], and {} are blank.
919
+ #
920
+ # This simplifies
921
+ #
922
+ # if !address.nil? && !address.empty?
923
+ #
924
+ # to
925
+ #
926
+ # if !address.blank?
927
+ #
928
+ # Object#blank? comes from the GEM ActiveSupport 2.1.
929
+ #
930
+ def blank?
931
+ respond_to?(:empty?) ? empty? : !self
932
+ end unless Object.method_defined? :blank?
933
+
934
+ end
935
+
936
+
937
+ module REXML # :nodoc:
938
+ class Element < Parent # :nodoc:
939
+ include WWW::Delicious::XMLUtils
940
+ end
941
+ end