lgs-www-delicious 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG.rdoc +52 -0
  2. data/LICENSE.rdoc +25 -0
  3. data/Manifest +48 -0
  4. data/README.rdoc +206 -0
  5. data/Rakefile +55 -0
  6. data/init.rb +1 -0
  7. data/lib/www/delicious.rb +942 -0
  8. data/lib/www/delicious/bundle.rb +73 -0
  9. data/lib/www/delicious/element.rb +73 -0
  10. data/lib/www/delicious/errors.rb +46 -0
  11. data/lib/www/delicious/post.rb +123 -0
  12. data/lib/www/delicious/tag.rb +101 -0
  13. data/lib/www/delicious/version.rb +33 -0
  14. data/setup.rb +1585 -0
  15. data/test/bundle_test.rb +63 -0
  16. data/test/delicious_test.rb +369 -0
  17. data/test/fixtures/net_response_invalid_account.yml +25 -0
  18. data/test/fixtures/net_response_success.yml +23 -0
  19. data/test/online_test.rb +147 -0
  20. data/test/post_test.rb +68 -0
  21. data/test/tag_test.rb +69 -0
  22. data/test/test_all.rb +19 -0
  23. data/test/test_helper.rb +43 -0
  24. data/test/testcases/element/bundle.xml +1 -0
  25. data/test/testcases/element/invalid_root.xml +2 -0
  26. data/test/testcases/element/post.xml +2 -0
  27. data/test/testcases/element/post_unshared.xml +2 -0
  28. data/test/testcases/element/tag.xml +1 -0
  29. data/test/testcases/response/bundles_all.xml +5 -0
  30. data/test/testcases/response/bundles_all_empty.xml +2 -0
  31. data/test/testcases/response/bundles_delete.xml +2 -0
  32. data/test/testcases/response/bundles_set.xml +2 -0
  33. data/test/testcases/response/bundles_set_error.xml +2 -0
  34. data/test/testcases/response/posts_add.xml +2 -0
  35. data/test/testcases/response/posts_all.xml +12 -0
  36. data/test/testcases/response/posts_dates.xml +14 -0
  37. data/test/testcases/response/posts_dates_with_tag.xml +14 -0
  38. data/test/testcases/response/posts_delete.xml +2 -0
  39. data/test/testcases/response/posts_get.xml +7 -0
  40. data/test/testcases/response/posts_get_with_tag.xml +6 -0
  41. data/test/testcases/response/posts_recent.xml +19 -0
  42. data/test/testcases/response/posts_recent_with_tag.xml +19 -0
  43. data/test/testcases/response/tags_get.xml +5 -0
  44. data/test/testcases/response/tags_get_empty.xml +2 -0
  45. data/test/testcases/response/tags_rename.xml +2 -0
  46. data/test/testcases/response/update.delicious1.xml +2 -0
  47. data/test/testcases/response/update.xml +3 -0
  48. data/www-delicious.gemspec +44 -0
  49. metadata +148 -0
@@ -0,0 +1,52 @@
1
+ = Changelog
2
+
3
+
4
+ == development
5
+
6
+ * FIXED: Compatibility fixes for Ruby 1.9. WWW::Delicious is now 100% compatible with 1.9. You should remember to define the proper content encoding with magic comments when working with UTF-8/MultiByte XML or Ruby files, see http://redmine.ruby-lang.org/wiki/ruby-19/ScriptEncoding (closes #142).
7
+
8
+ * CHANGED: Don't use File.dirname(__FILE__) in require statement to prevent recursive inclusions.
9
+
10
+ == Release 0.2.1 by Luca G.Soave
11
+
12
+ * ADDED: plugin install ability : ruby ./script/plugin install git://github.com/lgs/www-delicious.git
13
+ * FIXED: Error installing ideaoforder-www-delicious gem : ideaoforder-www-delicious requires RubyGems version = 1.2
14
+
15
+ == Release 0.2.0
16
+
17
+ * ADDED: :base_uri initialization option allows to create a new instance specifying a custom base_uri for all API calls. This is useful, for example, if you want to use ma.gno.lia Mirror'd APIs (http://wiki.ma.gnolia.com/Mirror%27d_API) instead the del.icio.us one (thanks to Jörg Battermann).
18
+
19
+ * ADDED: two new REXML::Element core extension elements to enhance interaction with node elements.
20
+
21
+ * FIXED: a wrong indentation in README file causes all list items to be rendered as source code.
22
+
23
+ * FIXED: Missing WWW::Delicious::Bundle#to_s method causes a class ID representation to be returned.
24
+
25
+ * FIXED: Missing unit tests for post_ calls (closes #18).
26
+
27
+ * FIXED: Added test for `shared` Post attribute and fixed an issue with duplicate `replace` method definition (closes #11).
28
+
29
+ * CHANGED: improved documentation and added more examples (closes #21).
30
+
31
+ * CHANGED: REXML::Element#attribute_value core extension has been renamed to REXML::Element#if_attribute_value.
32
+
33
+ * CHANGED: Renamed TESTCASE_PATH to TESTCASES_PATH.
34
+
35
+ * CHANGED: WWW::Delicious::Tag, WWW::Delicious::Bundle, WWW::Delicious::Post now extend WWW::Delicious::Element. Simplified classes.
36
+
37
+ * CHANGED: WWW::Delicious::Tag#to_s always returns a string even if name is nil.
38
+
39
+ * CHANGED: WWW::Delicious::Tag :count attribute is now stored and returned as Fixnum instead of String.
40
+
41
+ * CHANGED: Unit test reorganization (closes #22).
42
+
43
+ * CHANGED: Simplified and tidyfied test system with Mocha (closes #19).
44
+
45
+ * CHANGED: Various internal API methods have been renamed for coherence with their new scope.
46
+
47
+ * CHANGED: Integrated Echoe, cleaned Rakefile (closes #23).
48
+
49
+
50
+ == Release 0.1.0 (2008-05-11)
51
+
52
+ * Initial public release.
@@ -0,0 +1,25 @@
1
+ = License
2
+
3
+ (The MIT License)
4
+
5
+ Copyright (c) 2008 Simone Carletti <weppos@weppos.net>
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
@@ -0,0 +1,48 @@
1
+ lib/www/delicious/bundle.rb
2
+ lib/www/delicious/tag.rb
3
+ lib/www/delicious/element.rb
4
+ lib/www/delicious/version.rb
5
+ lib/www/delicious/post.rb
6
+ lib/www/delicious/errors.rb
7
+ lib/www/delicious.rb
8
+ test/post_test.rb
9
+ test/test_all.rb
10
+ test/fixtures/net_response_success.yml
11
+ test/fixtures/net_response_invalid_account.yml
12
+ test/delicious_test.rb
13
+ test/online_test.rb
14
+ test/test_helper.rb
15
+ test/bundle_test.rb
16
+ test/tag_test.rb
17
+ test/testcases/response/posts_get_with_tag.xml
18
+ test/testcases/response/tags_get_empty.xml
19
+ test/testcases/response/update.xml
20
+ test/testcases/response/bundles_set.xml
21
+ test/testcases/response/bundles_delete.xml
22
+ test/testcases/response/posts_get.xml
23
+ test/testcases/response/posts_add.xml
24
+ test/testcases/response/bundles_set_error.xml
25
+ test/testcases/response/posts_all.xml
26
+ test/testcases/response/posts_dates_with_tag.xml
27
+ test/testcases/response/posts_dates.xml
28
+ test/testcases/response/bundles_all.xml
29
+ test/testcases/response/posts_delete.xml
30
+ test/testcases/response/posts_recent_with_tag.xml
31
+ test/testcases/response/tags_get.xml
32
+ test/testcases/response/bundles_all_empty.xml
33
+ test/testcases/response/posts_recent.xml
34
+ test/testcases/response/update.delicious1.xml
35
+ test/testcases/response/tags_rename.xml
36
+ test/testcases/element/invalid_root.xml
37
+ test/testcases/element/tag.xml
38
+ test/testcases/element/post.xml
39
+ test/testcases/element/bundle.xml
40
+ test/testcases/element/post_unshared.xml
41
+ Manifest
42
+ Rakefile
43
+ LICENSE.rdoc
44
+ CHANGELOG.rdoc
45
+ www-delicious.gemspec
46
+ setup.rb
47
+ README.rdoc
48
+ init.rb
@@ -0,0 +1,206 @@
1
+ = WWW::Delicious
2
+
3
+ WWW::Delicious is a Ruby client for http://del.icio.us XML API.
4
+
5
+ It provides both read and write functionality. You can read user Posts, Tags
6
+ and Bundles but you can create new Posts, Tags and Bundles as well.
7
+
8
+
9
+ == Overview
10
+
11
+ WWW::Delicious maps all the original del.icio.us API calls and provides some
12
+ additional convenient methods to perform common tasks.
13
+ Please read the official documentation (http://del.icio.us/help/api/)
14
+ to learn more about del.icio.us API.
15
+
16
+ WWW::Delicious is 100% compatible with all del.icio.us API constraints,
17
+ including the requirement to set a valid user agent or wait at least
18
+ one second between queries.
19
+ Basically, the main benefit from using this library is that you don't need
20
+ to take care of all these low level details, if you don't want:
21
+ WWW::Delicious will try to give you the most with less efforts.
22
+
23
+
24
+ == Dependencies
25
+
26
+ * Ruby >= 1.8.6 (not tested with previous versions)
27
+
28
+
29
+ == Download and Installation
30
+
31
+ RubyGems[http://rubyforge.org/projects/rubygems/] is the preferred install method.
32
+ To get the latest version, simply type the following instruction into your command prompt:
33
+
34
+ $ sudo gem install www-delicious
35
+
36
+ Depending on your system, you might need su privileges.
37
+
38
+ To install the library manually, downlad the latest version from
39
+ navigate to the root library directory and enter:
40
+
41
+ $ sudo ruby setup.rb
42
+
43
+ If you need the latest development version you can download the source code
44
+ from one of the GIT repositories listed above.
45
+ Beware that the code might not be as stable as the official release.
46
+
47
+ == Plugin installation on Rails
48
+
49
+ Since the version 0.2.1 WWW::Delicious can be installed via plugin,
50
+ running the following command :
51
+
52
+ $ ruby ./script/plugin install git://github.com/lgs/www-delicious.git
53
+
54
+ == Getting Started
55
+
56
+ In order to use this library you need a valid del.icio.us account.
57
+ Go to http://del.icio.us/ and register for a new account if you don't already have one.
58
+
59
+ Then create a valid instance of WWW::Delicious providing your account credentials.
60
+
61
+ require 'www/delicious'
62
+
63
+ # create a new instance with given username and password
64
+ d = WWW::Delicious.new('username', 'password')
65
+
66
+ Now you can use your instance to interact with the API interface.
67
+
68
+
69
+ === Last account update
70
+
71
+ The following example show you how to get the last account update Time.
72
+
73
+ require 'www/delicious'
74
+ d = WWW::Delicious.new('username', 'password')
75
+
76
+ time = d.update # => Fri May 02 18:02:48 UTC 2008
77
+
78
+
79
+ === Reading Posts
80
+
81
+ You can fetch your posts in 3 different ways:
82
+
83
+ require 'www/delicious'
84
+ d = WWW::Delicious.new('username', 'password')
85
+
86
+ # 1. get all posts
87
+ posts = d.posts_all
88
+
89
+ # 2. get recent posts
90
+ posts = d.posts_recent
91
+
92
+ # 3. get a single post (the latest one if no criteria is given)
93
+ posts = d.posts_get(:tag => 'ruby')
94
+
95
+ Each post call accepts some options to refine your search.
96
+ For example, you can always search for posts matching a specific tag.
97
+
98
+ posts = d.posts_all(:tag => 'ruby')
99
+ posts = d.posts_recent(:tag => 'ruby')
100
+ posts = d.posts_get(:tag => 'ruby')
101
+
102
+
103
+ === Creating a new Post
104
+
105
+ require 'www/delicious'
106
+ d = WWW::Delicious.new('username', 'password')
107
+
108
+ # add a post from options
109
+ d.posts_add(:url => 'http://www.simonecarletti.com/', :title => 'Cool site!')
110
+
111
+ # add a post from WWW::Delicious::Post
112
+ d.posts_add(WWW::Delicious::Post.new(:url => 'http://www.simonecarletti.com/', :title => 'Cool site!'))
113
+
114
+
115
+ === Deleting a Posts
116
+
117
+ require 'www/delicious'
118
+ d = WWW::Delicious.new('username', 'password')
119
+
120
+ # delete given post (the URL can be either a string or an URI)
121
+ d.posts_delete('http://www.foobar.com/')
122
+
123
+ Note. Actually you cannot delete a post from a WWW::Delicious::Post instance.
124
+ It means, the following example doesn't work as some ActiveRecord user might expect.
125
+
126
+ post = WWW::Delicious::Post.new(:url => 'http://www.foobar.com/')
127
+ post.delete
128
+
129
+ This feature is already in the TODO list. For now, use the following workaround
130
+ to delete a given Post.
131
+
132
+ # delete a post from an existing post = WWW::Delicious::Post
133
+ d.posts_delete(post.url)
134
+
135
+
136
+ === Tags
137
+
138
+ Working with tags it's really easy. You can get all your tags or rename an existing tag.
139
+
140
+ require 'www/delicious'
141
+ d = WWW::Delicious.new('username', 'password')
142
+
143
+ # get all tags
144
+ tags = d.tags_get
145
+
146
+ # print all tag names
147
+ tags.each { |t| puts t.name }
148
+
149
+ # rename the tag gems to gem
150
+ d.tags_rename('gems', 'gem')
151
+
152
+
153
+ === Bundles
154
+
155
+ WWW::Delicious enables you to get all bundles from given account.
156
+
157
+ require 'www/delicious'
158
+ d = WWW::Delicious.new('username', 'password')
159
+
160
+ # get all bundles
161
+ bundles = d.bundles_all
162
+
163
+ # print all bundle names
164
+ bundles.each { |b| puts b.name }
165
+
166
+ You can also create new bundles or delete existing ones.
167
+
168
+ require 'www/delicious'
169
+ d = WWW::Delicious.new('username', 'password')
170
+
171
+ # set a new bundle for tags ruby, rails and gem
172
+ d.bundles_set('MyBundle', %w(ruby rails gem))
173
+
174
+ # delete the old bundle
175
+ d.bundles_delete('OldBundle')
176
+
177
+
178
+ == Author
179
+
180
+ {Simone Carletti}[http://www.simonecarletti.com/] <weppos@weppos.net>
181
+
182
+
183
+ == Resources
184
+
185
+ * {Homepage}[http://code.simonecarletti.com/www-delicious]
186
+ * {API}[http://www-delicious.rubyforge.org/]
187
+ * {GitHub}[http://github.com/weppos/www-delicious/]
188
+ * {RubyForge}[http://rubyforge.org/projects/www-delicious/]
189
+
190
+
191
+ == FeedBack and Bug reports
192
+
193
+ Feel free to email {Simone Carletti}[mailto:weppos@weppos.net] with any questions or feedback.
194
+
195
+ Please use the {Ticket System}[http://code.simonecarletti.com/projects/show/www-delicious] to submit bug reports or feature request.
196
+
197
+
198
+ == Changelog
199
+
200
+ See the CHANGELOG.rdoc file for details.
201
+
202
+
203
+ == License
204
+
205
+ Copyright (c) 2008 Simone Carletti, WWW::Delicious is released under the MIT license.
206
+
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ $:.unshift(File.dirname(__FILE__) + "/lib")
6
+ require 'www/delicious'
7
+
8
+
9
+ # Common package properties
10
+ PKG_NAME = ENV['PKG_NAME'] || WWW::Delicious::GEM
11
+ PKG_VERSION = ENV['PKG_VERSION'] || WWW::Delicious::VERSION
12
+ PKG_SUMMARY = "Ruby client for del.icio.us API."
13
+ PKG_FILES = FileList.new("{lib,test}/**/*.rb") do |files|
14
+ files.include %w(README.rdoc CHANGELOG.rdoc LICENSE.rdoc)
15
+ files.include %w(Rakefile setup.rb)
16
+ end
17
+ RUBYFORGE_PROJECT = 'www-delicious'
18
+
19
+ if ENV['SNAPSHOT'].to_i == 1
20
+ PKG_VERSION << "." << Time.now.utc.strftime("%Y%m%d%H%M%S")
21
+ end
22
+
23
+
24
+ Echoe.new(PKG_NAME, PKG_VERSION) do |p|
25
+ p.author = "Simone Carletti"
26
+ p.email = "weppos@weppos.net"
27
+ p.summary = PKG_SUMMARY
28
+ p.description = <<-EOF
29
+ WWW::Delicious is a del.icio.us API client implemented in Ruby. \
30
+ It provides access to all available del.icio.us API queries \
31
+ and returns the original XML response as a friendly Ruby object.
32
+ EOF
33
+ p.url = "http://code.simonecarletti.com/www-delicious"
34
+ p.project = RUBYFORGE_PROJECT
35
+
36
+ p.need_zip = true
37
+ p.rcov_options = ["--main << README.rdoc -x Rakefile -x mocha -x rcov"]
38
+ p.rdoc_pattern = /^(lib|CHANGELOG.rdoc|README.rdoc)/
39
+
40
+ p.development_dependencies += ["rake >=0.8",
41
+ "echoe >=3.0",
42
+ "mocha >=0.9"]
43
+ end
44
+
45
+
46
+ begin
47
+ require 'code_statistics'
48
+ desc "Show library's code statistics"
49
+ task :stats do
50
+ CodeStatistics.new(["WWW::Delicious", "lib"],
51
+ ["Tests", "test"]).to_s
52
+ end
53
+ rescue LoadError
54
+ puts "CodeStatistics (Rails) is not available"
55
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'www/delicious'
@@ -0,0 +1,942 @@
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
+ # SVN: $Id$
14
+ #++
15
+
16
+
17
+ require 'rubygems'
18
+ require 'net/https'
19
+ require 'rexml/document'
20
+ require 'time'
21
+ require 'www/delicious/bundle'
22
+ require 'www/delicious/post'
23
+ require 'www/delicious/tag'
24
+ require 'www/delicious/errors'
25
+ require 'www/delicious/version'
26
+
27
+
28
+ module WWW #:nodoc:
29
+
30
+
31
+ #
32
+ # = WWW::Delicious
33
+ #
34
+ # WWW::Delicious is a Ruby client for http://del.icio.us XML API.
35
+ #
36
+ # It provides both read and write functionalities.
37
+ # You can read user Posts, Tags and Bundles
38
+ # but you can create new Posts, Tags and Bundles as well.
39
+ #
40
+ #
41
+ # == Basic Usage
42
+ #
43
+ # The following is just a basic demonstration of the main features.
44
+ # See the README file for a deeper explanation about how to get the best
45
+ # from WWW::Delicious library.
46
+ #
47
+ # The examples in this page make the following assumptions
48
+ # * you have a valid del.icio.us account
49
+ # * +username+ is your account username
50
+ # * +password+ is your account password
51
+ #
52
+ # In order to make a query you first need to create
53
+ # a new WWW::Delicious instance as follows:
54
+ #
55
+ # require 'www/delicious'
56
+ #
57
+ # username = 'my delicious username'
58
+ # password = 'my delicious password'
59
+ #
60
+ # d = WWW::Delicious.new(username, password)
61
+ #
62
+ # The constructor accepts some additional options.
63
+ # For instance, if you want to customize the user agent:
64
+ #
65
+ # d = WWW::Delicious.new(username, password, :user_agent => 'FooAgent')
66
+ #
67
+ # Now you can use any of the API methods available.
68
+ #
69
+ # For example, you may want to know when your account was last updated
70
+ # to check whether someone else made some changes on behalf of you:
71
+ #
72
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
73
+ #
74
+ # Because the answer is a valid +Time+ instance, you can format it with +strftime+.
75
+ #
76
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
77
+ # datetime.strftime('%Y') # => 2008
78
+ #
79
+ class Delicious
80
+
81
+ NAME = 'WWW::Delicious'
82
+ GEM = 'www-delicious'
83
+ AUTHOR = 'Simone Carletti <weppos@weppos.net>'
84
+
85
+ # del.icio.us account username
86
+ attr_reader :username
87
+
88
+ # del.icio.us account password
89
+ attr_reader :password
90
+
91
+ # base URI for del.icio.us API
92
+ attr_reader :base_uri
93
+
94
+
95
+ # API Base URL
96
+ API_BASE_URI = 'https://api.del.icio.us'
97
+
98
+ # API Path Update
99
+ API_PATH_UPDATE = '/v1/posts/update';
100
+
101
+ # API Path All Bundles
102
+ API_PATH_BUNDLES_ALL = '/v1/tags/bundles/all';
103
+ # API Path Set Bundle
104
+ API_PATH_BUNDLES_SET = '/v1/tags/bundles/set';
105
+ # API Path Delete Bundle
106
+ API_PATH_BUNDLES_DELETE = '/v1/tags/bundles/delete';
107
+
108
+ # API Path Get Tags
109
+ API_PATH_TAGS_GET = '/v1/tags/get';
110
+ # API Path Rename Tag
111
+ API_PATH_TAGS_RENAME = '/v1/tags/rename';
112
+
113
+ # API Path Get Posts
114
+ API_PATH_POSTS_GET = '/v1/posts/get';
115
+ # API Path Recent Posts
116
+ API_PATH_POSTS_RECENT = '/v1/posts/recent';
117
+ # API Path All Posts
118
+ API_PATH_POSTS_ALL = '/v1/posts/all';
119
+ # API Path Posts by Dates
120
+ API_PATH_POSTS_DATES = '/v1/posts/dates';
121
+ # API Path Add Post
122
+ API_PATH_POSTS_ADD = '/v1/posts/add';
123
+ # API Path Delete Post
124
+ API_PATH_POSTS_DELETE = '/v1/posts/delete';
125
+
126
+ # Time to wait before sending a new request, in seconds
127
+ SECONDS_BEFORE_NEW_REQUEST = 1
128
+
129
+ # Time converter converts a Time instance into the format
130
+ # requested by Delicious API
131
+ TIME_CONVERTER = lambda { |time| time.iso8601() }
132
+
133
+
134
+ #
135
+ # Constructs a new <tt>WWW::Delicious</tt> object
136
+ # with given +username+ and +password+.
137
+ #
138
+ # # create a new object with username 'user' and password 'psw
139
+ # obj = WWW::Delicious('user', 'psw')
140
+ # # => self
141
+ #
142
+ # If a block is given, the instance is passed to the block
143
+ # but this method always returns the instance itself.
144
+ #
145
+ # WWW::Delicious('user', 'psw') do |d|
146
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
147
+ # end
148
+ # # => self
149
+ #
150
+ # You can also specify some additional options, including a custom user agent
151
+ # or the base URI for del.icio.us API.
152
+ #
153
+ # WWW::Delicious('user', 'psw', :base_uri => 'https://ma.gnolia.com/api/mirrord') do |d|
154
+ # # the following call is mirrored by ma.gnolia
155
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
156
+ # end
157
+ # # => self
158
+ #
159
+ # === Options
160
+ # This class accepts a Hash with additional options.
161
+ # Here's the list of valid keys:
162
+ #
163
+ # <tt>:user_agent</tt>:: User agent to display in HTTP requests.
164
+ # <tt>:base_uri</tt>:: The base URI to del.icio.us API.
165
+ #
166
+ def initialize(username, password, options = {}, &block) # :yields: delicious
167
+ @username, @password = username.to_s, password.to_s
168
+
169
+ # set API base URI
170
+ @base_uri = URI.parse(options[:base_uri] || API_BASE_URI)
171
+
172
+ init_user_agent(options)
173
+ init_http_client(options)
174
+
175
+ yield self if block_given?
176
+ self # ensure to always return self even if block is given
177
+ end
178
+
179
+
180
+ #
181
+ # Returns the reference to current <tt>@http_client</tt>.
182
+ # The http is always valid unless it has been previously set to +nil+.
183
+ #
184
+ # # nil client
185
+ # obj.http_client # => nil
186
+ #
187
+ # # valid client
188
+ # obj.http_client # => Net::HTTP
189
+ #
190
+ def http_client()
191
+ return @http_client
192
+ end
193
+
194
+ #
195
+ # Sets the internal <tt>@http_client</tt> to +client+.
196
+ #
197
+ # # nil client
198
+ # obj.http_client = nil
199
+ #
200
+ # # http client
201
+ # obj.http_client = Net::HTTP.new()
202
+ #
203
+ # # invalid client
204
+ # obj.http_client = 'foo' # => ArgumentError
205
+ #
206
+ def http_client=(client)
207
+ unless client.kind_of?(Net::HTTP) or client.nil?
208
+ raise ArgumentError, "`client` expected to be a kind of `Net::HTTP`, `#{client.class}` given"
209
+ end
210
+ @http_client = client
211
+ end
212
+
213
+ # Returns current user agent string.
214
+ def user_agent()
215
+ return @headers['User-Agent']
216
+ end
217
+
218
+
219
+ #
220
+ # Returns true if given account credentials are valid.
221
+ #
222
+ # d = WWW::Delicious.new('username', 'password')
223
+ # d.valid_account? # => true
224
+ #
225
+ # d = WWW::Delicious.new('username', 'invalid_password')
226
+ # d.valid_account? # => false
227
+ #
228
+ # This method is not "exception safe".
229
+ # It doesn't return false if an HTTP error or any kind of other error occurs,
230
+ # it raises back the exception to the caller instead.
231
+ #
232
+ #
233
+ # Raises:: WWW::Delicious::Error
234
+ # Raises:: WWW::Delicious::HTTPError
235
+ # Raises:: WWW::Delicious::ResponseError
236
+ #
237
+ def valid_account?
238
+ update()
239
+ return true
240
+ rescue HTTPError => e
241
+ return false if e.message =~ /invalid username or password/i
242
+ raise
243
+ end
244
+
245
+ #
246
+ # Checks to see when a user last posted an item
247
+ # and returns the last update +Time+ for the user.
248
+ #
249
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
250
+ #
251
+ #
252
+ # Raises:: WWW::Delicious::Error
253
+ # Raises:: WWW::Delicious::HTTPError
254
+ # Raises:: WWW::Delicious::ResponseError
255
+ #
256
+ def update()
257
+ response = request(API_PATH_UPDATE)
258
+ return parse_update_response(response.body)
259
+ end
260
+
261
+ #
262
+ # Retrieves all of a user's bundles
263
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
264
+ #
265
+ # d.bundles_all() # => [#<WWW::Delicious::Bundle>, #<WWW::Delicious::Bundle>, ...]
266
+ # d.bundles_all() # => []
267
+ #
268
+ #
269
+ # Raises:: WWW::Delicious::Error
270
+ # Raises:: WWW::Delicious::HTTPError
271
+ # Raises:: WWW::Delicious::ResponseError
272
+ #
273
+ def bundles_all()
274
+ response = request(API_PATH_BUNDLES_ALL)
275
+ return parse_bundle_collection(response.body)
276
+ end
277
+
278
+ #
279
+ # Assignes a set of tags to a single bundle,
280
+ # wipes away previous settings for bundle.
281
+ #
282
+ # # create from a bundle
283
+ # d.bundles_set(WWW::Delicious::Bundle.new('MyBundle'), %w(foo bar))
284
+ #
285
+ # # create from a string
286
+ # d.bundles_set('MyBundle', %w(foo bar))
287
+ #
288
+ #
289
+ # Raises:: WWW::Delicious::Error
290
+ # Raises:: WWW::Delicious::HTTPError
291
+ # Raises:: WWW::Delicious::ResponseError
292
+ #
293
+ def bundles_set(bundle_or_name, tags = [])
294
+ params = prepare_bundles_set_params(bundle_or_name, tags)
295
+ response = request(API_PATH_BUNDLES_SET, params)
296
+ return parse_and_eval_execution_response(response.body)
297
+ end
298
+
299
+ #
300
+ # Deletes +bundle_or_name+ bundle from del.icio.us.
301
+ # +bundle_or_name+ can be either a WWW::Delicious::Bundle instance
302
+ # or a string with the name of the bundle.
303
+ #
304
+ # This method doesn't care whether the exists.
305
+ # If not, the execution will silently return without rising any error.
306
+ #
307
+ # # delete from a bundle
308
+ # d.bundles_delete(WWW::Delicious::Bundle.new('MyBundle'))
309
+ #
310
+ # # delete from a string
311
+ # d.bundles_delete('MyBundle', %w(foo bar))
312
+ #
313
+ #
314
+ # Raises:: WWW::Delicious::Error
315
+ # Raises:: WWW::Delicious::HTTPError
316
+ # Raises:: WWW::Delicious::ResponseError
317
+ #
318
+ def bundles_delete(bundle_or_name)
319
+ params = prepare_bundles_delete_params(bundle_or_name)
320
+ response = request(API_PATH_BUNDLES_DELETE, params)
321
+ return parse_and_eval_execution_response(response.body)
322
+ end
323
+
324
+ #
325
+ # Retrieves the list of tags and number of times used by the user
326
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
327
+ #
328
+ # d.tags_get() # => [#<WWW::Delicious::Tag>, #<WWW::Delicious::Tag>, ...]
329
+ # d.tags_get() # => []
330
+ #
331
+ #
332
+ # Raises:: WWW::Delicious::Error
333
+ # Raises:: WWW::Delicious::HTTPError
334
+ # Raises:: WWW::Delicious::ResponseError
335
+ #
336
+ def tags_get()
337
+ response = request(API_PATH_TAGS_GET)
338
+ return parse_tag_collection(response.body)
339
+ end
340
+
341
+ #
342
+ # Renames an existing tag with a new tag name.
343
+ #
344
+ # # rename from a tag
345
+ # d.bundles_set(WWW::Delicious::Tag.new('old'), WWW::Delicious::Tag.new('new'))
346
+ #
347
+ # # rename from a string
348
+ # d.bundles_set('old', 'new')
349
+ #
350
+ #
351
+ # Raises:: WWW::Delicious::Error
352
+ # Raises:: WWW::Delicious::HTTPError
353
+ # Raises:: WWW::Delicious::ResponseError
354
+ #
355
+ def tags_rename(from_name_or_tag, to_name_or_tag)
356
+ params = prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
357
+ response = request(API_PATH_TAGS_RENAME, params)
358
+ return parse_and_eval_execution_response(response.body)
359
+ end
360
+
361
+ #
362
+ # Returns an array of <tt>WWW::Delicious::Post</tt> matching +options+.
363
+ # If no option is given, the last post is returned.
364
+ # If no date or url is given, most recent date will be used.
365
+ #
366
+ # d.posts_get() # => [#<WWW::Delicious::Post>, #<WWW::Delicious::Post>, ...]
367
+ # d.posts_get() # => []
368
+ #
369
+ # # get all posts tagged with ruby
370
+ # d.posts_get(:tag => WWW::Delicious::Tag.new('ruby))
371
+ #
372
+ # # get all posts matching URL 'http://www.simonecarletti.com'
373
+ # d.posts_get(:url => URI.parse('http://www.simonecarletti.com'))
374
+ #
375
+ # # get all posts tagged with ruby and matching URL 'http://www.simonecarletti.com'
376
+ # d.posts_get(:tag => WWW::Delicious::Tag.new('ruby),
377
+ # :url => URI.parse('http://www.simonecarletti.com'))
378
+ #
379
+ #
380
+ # === Options
381
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
382
+ # <tt>:dt</tt>:: a +Time+ with a date to filter by.
383
+ # <tt>:url</tt>:: a valid URI to filter by. It can be either an instance of +URI+ or a +String+.
384
+ #
385
+ # Raises:: WWW::Delicious::Error
386
+ # Raises:: WWW::Delicious::HTTPError
387
+ # Raises:: WWW::Delicious::ResponseError
388
+ #
389
+ def posts_get(options = {})
390
+ params = prepare_posts_params(options.clone, [:dt, :tag, :url])
391
+ response = request(API_PATH_POSTS_GET, params)
392
+ return parse_post_collection(response.body)
393
+ end
394
+
395
+ #
396
+ # Returns a list of the most recent posts, filtered by argument.
397
+ #
398
+ # # get the most recent posts
399
+ # d.posts_recent()
400
+ #
401
+ # # get the 10 most recent posts
402
+ # d.posts_recent(:count => 10)
403
+ #
404
+ #
405
+ # === Options
406
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
407
+ # <tt>:count</tt>:: number of items to retrieve. (default: 15, maximum: 100).
408
+ #
409
+ def posts_recent(options = {})
410
+ params = prepare_posts_params(options.clone, [:count, :tag])
411
+ response = request(API_PATH_POSTS_RECENT, params)
412
+ return parse_post_collection(response.body)
413
+ end
414
+
415
+ #
416
+ # Returns a list of all posts, filtered by argument.
417
+ #
418
+ # # get all (this is a very expensive query)
419
+ # d.posts_all
420
+ #
421
+ # # get all posts matching ruby
422
+ # d.posts_all(:tag => WWW::Delicious::Tag.new('ruby'))
423
+ #
424
+ #
425
+ # === Options
426
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
427
+ #
428
+ def posts_all(options = {})
429
+ params = prepare_posts_params(options.clone, [:tag])
430
+ response = request(API_PATH_POSTS_ALL, params)
431
+ return parse_post_collection(response.body)
432
+ end
433
+
434
+ #
435
+ # Returns a list of dates with the number of posts at each date.
436
+ #
437
+ # # get number of posts per date
438
+ # d.posts_dates
439
+ # # => { '2008-05-05' => 12, '2008-05-06' => 3, ... }
440
+ #
441
+ # # get number posts per date tagged as ruby
442
+ # d.posts_dates(:tag => WWW::Delicious::Tag.new('ruby'))
443
+ # # => { '2008-05-05' => 10, '2008-05-06' => 3, ... }
444
+ #
445
+ #
446
+ # === Options
447
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
448
+ #
449
+ def posts_dates(options = {})
450
+ params = prepare_posts_params(options.clone, [:tag])
451
+ response = request(API_PATH_POSTS_DATES, params)
452
+ return parse_posts_dates_response(response.body)
453
+ end
454
+
455
+ #
456
+ # Add a post to del.icio.us.
457
+ # +post_or_values+ can be either a +WWW::Delicious::Post+ instance
458
+ # or a Hash of params. This method accepts all params available
459
+ # to initialize a new +WWW::Delicious::Post+.
460
+ #
461
+ # # add a post from WWW::Delicious::Post
462
+ # d.posts_add(WWW::Delicious::Post.new(:url => 'http://www.foobar.com', :title => 'Hello world!'))
463
+ #
464
+ # # add a post from values
465
+ # d.posts_add(:url => 'http://www.foobar.com', :title => 'Hello world!')
466
+ #
467
+ #
468
+ def posts_add(post_or_values)
469
+ params = prepare_param_post(post_or_values).to_params
470
+ response = request(API_PATH_POSTS_ADD, params)
471
+ return parse_and_eval_execution_response(response.body)
472
+ end
473
+
474
+ #
475
+ # Deletes the post matching given +url+ from del.icio.us.
476
+ # +url+ can be either an URI instance or a string representation of a valid URL.
477
+ #
478
+ # This method doesn't care whether a post with given +url+ exists.
479
+ # If not, the execution will silently return without rising any error.
480
+ #
481
+ # # delete a post from URI
482
+ # d.post_delete(URI.parse('http://www.foobar.com/'))
483
+ #
484
+ # # delete a post from a string
485
+ # d.post_delete('http://www.foobar.com/')
486
+ #
487
+ #
488
+ def posts_delete(url)
489
+ params = prepare_posts_params({:url => url}, [:url])
490
+ response = request(API_PATH_POSTS_DELETE, params)
491
+ return parse_and_eval_execution_response(response.body)
492
+ end
493
+
494
+
495
+ protected
496
+
497
+ # Initializes the HTTP client.
498
+ # It automatically enable +use_ssl+ flag according to +@base_uri+ scheme.
499
+ def init_http_client(options)
500
+ http = Net::HTTP.new(@base_uri.host, 443)
501
+ http.use_ssl = true if @base_uri.scheme == "https"
502
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: not 100% supported
503
+ self.http_client = http
504
+ end
505
+
506
+ # Initializes user agent value for HTTP requests.
507
+ def init_user_agent(options)
508
+ user_agent = options[:user_agent] || default_user_agent()
509
+ @headers ||= {}
510
+ @headers['User-Agent'] = user_agent
511
+ end
512
+
513
+ #
514
+ # Creates and returns the default user agent string.
515
+ #
516
+ # By default, the user agent is composed by the following schema:
517
+ # <tt>NAME/VERSION (Ruby/RUBY_VERSION)</tt>
518
+ #
519
+ # * +NAME+ is the constant representing this library name
520
+ # * +VERSION+ is the constant representing current library version
521
+ # * +RUBY_VERSION+ is the version of Ruby interpreter the library is interpreted by
522
+ #
523
+ # default_user_agent
524
+ # # => WWW::Delicious/0.1.0 (Ruby/1.8.6)
525
+ #
526
+ def default_user_agent
527
+ return "#{NAME}/#{VERSION} (Ruby/#{RUBY_VERSION})"
528
+ end
529
+
530
+
531
+ #
532
+ # Composes an HTTP query string from an hash of +options+.
533
+ # The result is URI encoded.
534
+ #
535
+ # http_build_query(:foo => 'baa', :bar => 'boo')
536
+ # # => foo=baa&bar=boo
537
+ #
538
+ def http_build_query(params = {})
539
+ return params.collect do |k,v|
540
+ "#{URI.encode(k.to_s)}=#{URI.encode(v.to_s)}" unless v.nil?
541
+ end.compact.join('&')
542
+ end
543
+
544
+ #
545
+ # Sends an HTTP GET request to +path+ and appends given +params+.
546
+ #
547
+ # This method is 100% compliant with Delicious API reference.
548
+ # It waits at least 1 second between each HTTP request and
549
+ # provides an identifiable user agent by default,
550
+ # or the custom user agent set by +user_agent+ option
551
+ # when this istance has been created.
552
+ #
553
+ # request('/v1/api/path', :foo => 1, :bar => 2)
554
+ # # => sends a GET request to /v1/api/path?foo=1&bar=2
555
+ #
556
+ def request(path, params = {})
557
+ raise Error, 'Invalid HTTP Client' unless http_client
558
+ wait_before_new_request
559
+
560
+ uri = @base_uri.merge(path)
561
+ uri.query = http_build_query(params) unless params.empty?
562
+
563
+ begin
564
+ @last_request = Time.now # see #wait_before_new_request
565
+ @last_request_uri = uri # useful for debug
566
+ response = make_request(uri)
567
+ rescue => e # catch EOFError, SocketError and more
568
+ raise HTTPError, e.message
569
+ end
570
+
571
+ case response
572
+ when Net::HTTPSuccess
573
+ return response
574
+ when Net::HTTPUnauthorized # 401
575
+ raise HTTPError, 'Invalid username or password'
576
+ when Net::HTTPServiceUnavailable # 503
577
+ raise HTTPError, 'You have been throttled.' +
578
+ 'Please ensure you are waiting at least one second before each request.'
579
+ else
580
+ raise HTTPError, "HTTP #{response.code}: #{response.message}"
581
+ end
582
+ end
583
+
584
+ # Makes the real HTTP request to given +uri+ and returns the +response+.
585
+ # This method exists basically to simplify unit testing with mocha.
586
+ def make_request(uri)
587
+ http_client.start do |http|
588
+ req = Net::HTTP::Get.new(uri.request_uri, @headers)
589
+ req.basic_auth(@username, @password)
590
+ http.request(req)
591
+ end
592
+ end
593
+
594
+ #
595
+ # Delicious API reference requests to wait AT LEAST ONE SECOND
596
+ # between queries or the client is likely to get automatically throttled.
597
+ #
598
+ # This method calculates the difference between current time
599
+ # and the last request time and wait for the necessary time to meet
600
+ # SECONDS_BEFORE_NEW_REQUEST requirement.
601
+ #
602
+ # The difference is not rounded. If you only have to wait for 0.034 seconds
603
+ # then your don't have to wait 0 or 1 seconds, but 0.034 seconds!
604
+ #
605
+ def wait_before_new_request
606
+ return unless @last_request # this is the first request
607
+ # puts "Last request at #{TIME_CONVERTER.call(@last_request)}" if debug?
608
+ diff = Time.now - @last_request
609
+ if diff < SECONDS_BEFORE_NEW_REQUEST
610
+ # puts "Sleeping for #{diff} before new request..." if debug?
611
+ sleep(SECONDS_BEFORE_NEW_REQUEST - diff)
612
+ end
613
+ end
614
+
615
+
616
+ #
617
+ # Parses the response <tt>body</tt> and runs a common set of validators.
618
+ # Returns <tt>body</tt> as parsed REXML::Document on success.
619
+ #
620
+ # Raises:: WWW::Delicious::ResponseError in case of invalid response.
621
+ #
622
+ def parse_and_validate_response(body, options = {})
623
+ dom = REXML::Document.new(body)
624
+
625
+ if (value = options[:root_name]) && dom.root.name != value
626
+ raise ResponseError, "Invalid response, root node is not `#{value}`"
627
+ end
628
+ if (value = options[:root_text]) && dom.root.text != value
629
+ raise ResponseError, value
630
+ end
631
+
632
+ return dom
633
+ end
634
+
635
+ #
636
+ # Parses and evaluates the response returned by an execution,
637
+ # usually an update/delete/insert operation.
638
+ #
639
+ # Raises:: WWW::Delicious::ResponseError in case of invalid response
640
+ # Raises:: WWW::Delicious::Error in case of execution error
641
+ #
642
+ def parse_and_eval_execution_response(body)
643
+ dom = parse_and_validate_response(body, :root_name => 'result')
644
+ response = dom.root.if_attribute_value(:code)
645
+ response = dom.root.text if response.nil?
646
+ raise Error, "Invalid response, #{response}" unless %w(done ok).include?(response)
647
+ true
648
+ end
649
+
650
+ # Parses the response of an Update request
651
+ # and returns the update Timestamp.
652
+ def parse_update_response(body)
653
+ dom = parse_and_validate_response(body, :root_name => 'update')
654
+ dom.root.if_attribute_value(:time) { |v| Time.parse(v) }
655
+ end
656
+
657
+ # Parses a response containing a collection of Bundles
658
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
659
+ def parse_bundle_collection(body)
660
+ dom = parse_and_validate_response(body, :root_name => 'bundles')
661
+ dom.root.elements.collect('bundle') { |xml| Bundle.from_rexml(xml) }
662
+ end
663
+
664
+ # Parses a response containing a collection of Tags
665
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
666
+ def parse_tag_collection(body)
667
+ dom = parse_and_validate_response(body, :root_name => 'tags')
668
+ dom.root.elements.collect('tag') { |xml| Tag.from_rexml(xml) }
669
+ end
670
+
671
+ # Parses a response containing a collection of Posts
672
+ # and returns an array of <tt>WWW::Delicious::Post</tt>.
673
+ def parse_post_collection(body)
674
+ dom = parse_and_validate_response(body, :root_name => 'posts')
675
+ dom.root.elements.collect('post') { |xml| Post.from_rexml(xml) }
676
+ end
677
+
678
+ # Parses the response of a <tt>posts_dates</tt> request
679
+ # and returns a +Hash+ of date => count.
680
+ def parse_posts_dates_response(body)
681
+ dom = parse_and_validate_response(body, :root_name => 'dates')
682
+ return dom.root.get_elements('date').inject({}) do |collection, xml|
683
+ date = xml.if_attribute_value(:date)
684
+ count = xml.if_attribute_value(:count)
685
+ collection.merge({ date => count })
686
+ end
687
+ end
688
+
689
+
690
+ #
691
+ # Prepares the params for a `bundles_set` call
692
+ # and returns a Hash with the params ready for the HTTP request.
693
+ #
694
+ # Raises:: WWW::Delicious::Error
695
+ #
696
+ def prepare_bundles_set_params(name_or_bundle, tags = [])
697
+ bundle = prepare_param_bundle(name_or_bundle, tags) do |b|
698
+ raise Error, "Bundle name is empty" if b.name.empty?
699
+ raise Error, "Bundle must contain at least one tag" if b.tags.empty?
700
+ end
701
+ return { :bundle => bundle.name, :tags => bundle.tags.join(' ') }
702
+ end
703
+
704
+ #
705
+ # Prepares the params for a `bundles_set` call
706
+ # and returns a Hash with the params ready for the HTTP request.
707
+ #
708
+ # Raises:: WWW::Delicious::Error
709
+ #
710
+ def prepare_bundles_delete_params(name_or_bundle)
711
+ bundle = prepare_param_bundle(name_or_bundle) do |b|
712
+ raise Error, "Bundle name is empty" if b.name.empty?
713
+ end
714
+ return { :bundle => bundle.name }
715
+ end
716
+
717
+ #
718
+ # Prepares the params for a `tags_rename` call
719
+ # and returns a Hash with the params ready for the HTTP request.
720
+ #
721
+ # Raises:: WWW::Delicious::Error
722
+ #
723
+ def prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
724
+ from, to = [from_name_or_tag, to_name_or_tag].collect do |v|
725
+ prepare_param_tag(v)
726
+ end
727
+ return { :old => from, :new => to }
728
+ end
729
+
730
+ #
731
+ # Prepares the params for a `post_*` call
732
+ # and returns a Hash with the params ready for the HTTP request.
733
+ #
734
+ # Raises:: WWW::Delicious::Error
735
+ #
736
+ def prepare_posts_params(params, allowed_params = [])
737
+ compare_params(params, allowed_params)
738
+
739
+ # we don't need to check whether the following parameters
740
+ # are valid for this request because compare_params
741
+ # would raise if an invalid param is supplied
742
+
743
+ params[:tag] = prepare_param_tag(params[:tag]) if params[:tag]
744
+ params[:dt] = TIME_CONVERTER.call(params[:dt]) if params[:dt]
745
+ params[:url] = URI.parse(params[:url]) if params[:url]
746
+ params[:count] = if value = params[:count]
747
+ raise Error, 'Expected `count` <= 100' if value.to_i() > 100 # requirement
748
+ value.to_i
749
+ else
750
+ 15 # default value
751
+ end
752
+
753
+ return params
754
+ end
755
+
756
+
757
+ #
758
+ # Prepares the +post+ param for an API request.
759
+ #
760
+ # Creates and returns a <tt>WWW::Delicious::Post</tt> instance from <tt>post_or_values</tt>.
761
+ # <tt>post_or_values</tt> can be either an Hash with post attributes
762
+ # or a <tt>WWW::Delicious::Post</tt> instance.
763
+ #
764
+ def prepare_param_post(post_or_values, &block)
765
+ post = case post_or_values
766
+ when WWW::Delicious::Post
767
+ post_or_values
768
+ when Hash
769
+ Post.new(post_or_values)
770
+ else
771
+ raise ArgumentError, 'Expected `args` to be `WWW::Delicious::Post` or `Hash`'
772
+ end
773
+
774
+ yield(post) if block_given?
775
+ # TODO: validate post with post.validate!
776
+ raise ArgumentError, 'Both `url` and `title` are required' unless post.api_valid?
777
+ post
778
+ end
779
+
780
+ #
781
+ # Prepares the +bundle+ param for an API request.
782
+ #
783
+ # Creates and returns a <tt>WWW::Delicious::Bundle</tt> instance from <tt>name_or_bundle</tt>.
784
+ # <tt>name_or_bundle</tt> can be either a string holding bundle name
785
+ # or a <tt>WWW::Delicious::Bundle</tt> instance.
786
+ #
787
+ def prepare_param_bundle(name_or_bundle, tags = [], &block) # :yields: bundle
788
+ bundle = case name_or_bundle
789
+ when WWW::Delicious::Bundle
790
+ name_or_bundle
791
+ else
792
+ Bundle.new(:name => name_or_bundle, :tags => tags)
793
+ end
794
+
795
+ yield(bundle) if block_given?
796
+ # TODO: validate bundle with bundle.validate!
797
+ bundle
798
+ end
799
+
800
+ #
801
+ # Prepares the +tag+ param for an API request.
802
+ #
803
+ # Creates and returns a <tt>WWW::Delicious::Tag</tt> instance from <tt>name_or_tag</tt>.
804
+ # <tt>name_or_tag</tt> can be either a string holding tag name
805
+ # or a <tt>WWW::Delicious::Tag</tt> instance.
806
+ #
807
+ def prepare_param_tag(name_or_tag, &block) # :yields: tag
808
+ tag = case name_or_tag
809
+ when WWW::Delicious::Tag
810
+ name_or_tag
811
+ else
812
+ Tag.new(:name => name_or_tag.to_s)
813
+ end
814
+
815
+ yield(tag) if block_given?
816
+ # TODO: validate tag with tag.validate!
817
+ raise "Invalid `tag` value supplied" unless tag.api_valid?
818
+ tag
819
+ end
820
+
821
+ #
822
+ # Checks whether user given +params+ are valid against a defined collection of +valid_params+.
823
+ #
824
+ # === Examples
825
+ #
826
+ # params = {:foo => 1, :bar => 2}
827
+ #
828
+ # compare_params(params, [:foo, :bar])
829
+ # # => valid
830
+ #
831
+ # compare_params(params, [:foo, :bar, :baz])
832
+ # # => raises
833
+ #
834
+ # compare_params(params, [:foo])
835
+ # # => raises
836
+ #
837
+ # Raises:: WWW::Delicious::Error
838
+ #
839
+ def compare_params(params, valid_params)
840
+ raise ArgumentError, "Expected `params` to be a kind of `Hash`" unless params.kind_of?(Hash)
841
+ raise ArgumentError, "Expected `valid_params` to be a kind of `Array`" unless valid_params.kind_of?(Array)
842
+
843
+ # compute options difference
844
+ difference = params.keys - valid_params
845
+ raise Error, "Invalid params: `#{difference.join('`, `')}`" unless difference.empty?
846
+ end
847
+
848
+
849
+ module XMLUtils #:nodoc:
850
+
851
+ #
852
+ # Returns the +xmlattr+ attribute value for current <tt>REXML::Element</tt>.
853
+ #
854
+ # If block is given and attribute value is not nil,
855
+ # the content of the block is executed.
856
+ #
857
+ # === Examples
858
+ #
859
+ # dom = REXML::Document.new('<a name="1"><b>foo</b><b>bar</b></a>')
860
+ #
861
+ # dom.root.if_attribute_value(:name)
862
+ # # => "1"
863
+ #
864
+ # dom.root.if_attribute_value(:name) { |v| v.to_i }
865
+ # # => 1
866
+ #
867
+ # dom.root.if_attribute_value(:foo)
868
+ # # => nil
869
+ #
870
+ # dom.root.if_attribute_value(:name) { |v| v.to_i }
871
+ # # => nil
872
+ #
873
+ def if_attribute_value(xmlattr, &block) #:nodoc:
874
+ value = if attr = self.attribute(xmlattr.to_s)
875
+ attr.value
876
+ else
877
+ nil
878
+ end
879
+ value = yield value if !value.nil? and block_given?
880
+ value
881
+ end
882
+
883
+ #
884
+ # Returns the value of +expression+ child of this element, if it exists.
885
+ # If blog is given, block is called on +expression+ element value
886
+ # and the result is returned.
887
+ #
888
+ def if_element_value(expression, &block)
889
+ if_element(expression) do |element|
890
+ value = element.text
891
+ value = yield value if block_given?
892
+ value
893
+ end
894
+ end
895
+
896
+ #
897
+ # Executes the content of +block+ on +expression+
898
+ # child of this element, if it exists.
899
+ # Returns the result or +nil+ if +xmlelement+ doesn't exist.
900
+ #
901
+ def if_element(expression, &block)
902
+ raise LocalJumpError, "no block given" unless block_given?
903
+ if element = self.elements[expression.to_s]
904
+ yield element
905
+ else
906
+ nil
907
+ end
908
+ end
909
+
910
+ end # XMLUtils
911
+
912
+ end
913
+ end
914
+
915
+
916
+ class Object
917
+
918
+ # An object is blank if it's false, empty, or a whitespace string.
919
+ # For example, "", " ", +nil+, [], and {} are blank.
920
+ #
921
+ # This simplifies
922
+ #
923
+ # if !address.nil? && !address.empty?
924
+ #
925
+ # to
926
+ #
927
+ # if !address.blank?
928
+ #
929
+ # Object#blank? comes from the GEM ActiveSupport 2.1.
930
+ #
931
+ def blank?
932
+ respond_to?(:empty?) ? empty? : !self
933
+ end unless Object.method_defined? :blanks?
934
+
935
+ end
936
+
937
+
938
+ module REXML # :nodoc:
939
+ class Element < Parent # :nodoc:
940
+ include WWW::Delicious::XMLUtils
941
+ end
942
+ end