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