lgs-www-delicious 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +52 -0
- data/LICENSE.rdoc +25 -0
- data/Manifest +48 -0
- data/README.rdoc +206 -0
- data/Rakefile +55 -0
- data/init.rb +1 -0
- data/lib/www/delicious.rb +942 -0
- data/lib/www/delicious/bundle.rb +73 -0
- data/lib/www/delicious/element.rb +73 -0
- data/lib/www/delicious/errors.rb +46 -0
- data/lib/www/delicious/post.rb +123 -0
- data/lib/www/delicious/tag.rb +101 -0
- data/lib/www/delicious/version.rb +33 -0
- data/setup.rb +1585 -0
- data/test/bundle_test.rb +63 -0
- data/test/delicious_test.rb +369 -0
- data/test/fixtures/net_response_invalid_account.yml +25 -0
- data/test/fixtures/net_response_success.yml +23 -0
- data/test/online_test.rb +147 -0
- data/test/post_test.rb +68 -0
- data/test/tag_test.rb +69 -0
- data/test/test_all.rb +19 -0
- data/test/test_helper.rb +43 -0
- data/test/testcases/element/bundle.xml +1 -0
- data/test/testcases/element/invalid_root.xml +2 -0
- data/test/testcases/element/post.xml +2 -0
- data/test/testcases/element/post_unshared.xml +2 -0
- data/test/testcases/element/tag.xml +1 -0
- data/test/testcases/response/bundles_all.xml +5 -0
- data/test/testcases/response/bundles_all_empty.xml +2 -0
- data/test/testcases/response/bundles_delete.xml +2 -0
- data/test/testcases/response/bundles_set.xml +2 -0
- data/test/testcases/response/bundles_set_error.xml +2 -0
- data/test/testcases/response/posts_add.xml +2 -0
- data/test/testcases/response/posts_all.xml +12 -0
- data/test/testcases/response/posts_dates.xml +14 -0
- data/test/testcases/response/posts_dates_with_tag.xml +14 -0
- data/test/testcases/response/posts_delete.xml +2 -0
- data/test/testcases/response/posts_get.xml +7 -0
- data/test/testcases/response/posts_get_with_tag.xml +6 -0
- data/test/testcases/response/posts_recent.xml +19 -0
- data/test/testcases/response/posts_recent_with_tag.xml +19 -0
- data/test/testcases/response/tags_get.xml +5 -0
- data/test/testcases/response/tags_get_empty.xml +2 -0
- data/test/testcases/response/tags_rename.xml +2 -0
- data/test/testcases/response/update.delicious1.xml +2 -0
- data/test/testcases/response/update.xml +3 -0
- data/www-delicious.gemspec +44 -0
- metadata +148 -0
data/CHANGELOG.rdoc
ADDED
@@ -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.
|
data/LICENSE.rdoc
ADDED
@@ -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
|
+
|
data/Manifest
ADDED
@@ -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
|
data/README.rdoc
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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
|