pinboard_api 0.7.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/.travis.yml +2 -1
  2. data/README.md +26 -6
  3. data/lib/core_ext/blank.rb +26 -0
  4. data/lib/pinboard_api.rb +8 -2
  5. data/lib/pinboard_api/exceptions.rb +5 -0
  6. data/lib/pinboard_api/post.rb +54 -40
  7. data/lib/pinboard_api/request_utils.rb +22 -0
  8. data/lib/pinboard_api/tag.rb +10 -8
  9. data/lib/pinboard_api/version.rb +1 -1
  10. data/spec/core_ext/blank_spec.rb +19 -0
  11. data/spec/exceptions_spec.rb +7 -0
  12. data/spec/pinboard_api_spec.rb +9 -6
  13. data/spec/post_spec.rb +124 -42
  14. data/spec/support/pinboard_configuration.rb +1 -0
  15. data/spec/support/request_utils_spec.rb +29 -0
  16. data/spec/support/vcr.rb +13 -3
  17. data/spec/tag_spec.rb +15 -13
  18. data/spec/user_spec.rb +1 -5
  19. data/spec/vcr_cassettes/posts/all/custom_count.json +1 -0
  20. data/spec/vcr_cassettes/posts/all/custom_tag.json +1 -0
  21. data/spec/vcr_cassettes/posts/all/custom_times.json +1 -0
  22. data/spec/vcr_cassettes/posts/all/default_values.json +1 -0
  23. data/spec/vcr_cassettes/posts/all/not_found.json +1 -0
  24. data/spec/vcr_cassettes/posts/all/with_meta.json +1 -0
  25. data/spec/vcr_cassettes/posts/create.json +1 -0
  26. data/spec/vcr_cassettes/posts/dates/custom_tag.json +1 -0
  27. data/spec/vcr_cassettes/posts/dates/default_values.json +1 -0
  28. data/spec/vcr_cassettes/posts/delete/unsuccessful_class.json +1 -0
  29. data/spec/vcr_cassettes/posts/destroy/successful_class.json +1 -0
  30. data/spec/vcr_cassettes/posts/destroy/successful_instance.json +1 -0
  31. data/spec/vcr_cassettes/posts/destroy/unsuccessful_instance.json +1 -0
  32. data/spec/vcr_cassettes/posts/find/found.json +1 -0
  33. data/spec/vcr_cassettes/posts/find/not_found.json +1 -0
  34. data/spec/vcr_cassettes/posts/recent/custom_count.json +1 -0
  35. data/spec/vcr_cassettes/posts/recent/custom_tag.json +1 -0
  36. data/spec/vcr_cassettes/posts/recent/default_values.json +1 -0
  37. data/spec/vcr_cassettes/posts/save.json +1 -0
  38. data/spec/vcr_cassettes/posts/suggest.json +1 -0
  39. data/spec/vcr_cassettes/posts/update.json +1 -0
  40. data/spec/vcr_cassettes/tags/all.json +1 -0
  41. data/spec/vcr_cassettes/tags/destroy/successful_class.json +1 -0
  42. data/spec/vcr_cassettes/tags/destroy/successful_instance.json +1 -0
  43. data/spec/vcr_cassettes/tags/destroy/unsuccessful_class.json +1 -0
  44. data/spec/vcr_cassettes/tags/destroy/unsuccessful_instance.json +1 -0
  45. data/spec/vcr_cassettes/tags/find/found.json +1 -0
  46. data/spec/vcr_cassettes/tags/find/not_found.json +1 -0
  47. data/spec/vcr_cassettes/tags/rename/successful.json +1 -0
  48. data/spec/vcr_cassettes/tags/rename/unsuccessful.json +1 -0
  49. data/spec/vcr_cassettes/user/secret.json +1 -0
  50. metadata +73 -58
  51. data/spec/fixtures/vcr_cassettes/posts/all/custom_count.json +0 -1
  52. data/spec/fixtures/vcr_cassettes/posts/all/custom_tag.json +0 -1
  53. data/spec/fixtures/vcr_cassettes/posts/all/custom_times.json +0 -1
  54. data/spec/fixtures/vcr_cassettes/posts/all/default_values.json +0 -1
  55. data/spec/fixtures/vcr_cassettes/posts/all/not_found.json +0 -1
  56. data/spec/fixtures/vcr_cassettes/posts/dates/custom_tag.json +0 -1
  57. data/spec/fixtures/vcr_cassettes/posts/dates/default_values.json +0 -1
  58. data/spec/fixtures/vcr_cassettes/posts/delete/unsuccessful_class.json +0 -1
  59. data/spec/fixtures/vcr_cassettes/posts/destroy/successful_class.json +0 -1
  60. data/spec/fixtures/vcr_cassettes/posts/destroy/successful_instance.json +0 -1
  61. data/spec/fixtures/vcr_cassettes/posts/destroy/unsuccessful_instance.json +0 -1
  62. data/spec/fixtures/vcr_cassettes/posts/find/found.json +0 -1
  63. data/spec/fixtures/vcr_cassettes/posts/find/not_found.json +0 -1
  64. data/spec/fixtures/vcr_cassettes/posts/recent/custom_count.json +0 -1
  65. data/spec/fixtures/vcr_cassettes/posts/recent/custom_tag.json +0 -1
  66. data/spec/fixtures/vcr_cassettes/posts/recent/default_values.json +0 -1
  67. data/spec/fixtures/vcr_cassettes/posts/suggest.json +0 -1
  68. data/spec/fixtures/vcr_cassettes/posts/update.json +0 -1
  69. data/spec/fixtures/vcr_cassettes/tags/all.json +0 -1
  70. data/spec/fixtures/vcr_cassettes/tags/destroy/successful_class.json +0 -1
  71. data/spec/fixtures/vcr_cassettes/tags/destroy/successful_instance.json +0 -1
  72. data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_class.json +0 -1
  73. data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_instance.json +0 -1
  74. data/spec/fixtures/vcr_cassettes/tags/find/found.json +0 -1
  75. data/spec/fixtures/vcr_cassettes/tags/find/not_found.json +0 -1
  76. data/spec/fixtures/vcr_cassettes/tags/rename/successful.json +0 -1
  77. data/spec/fixtures/vcr_cassettes/tags/rename/unsuccessful.json +0 -1
  78. data/spec/fixtures/vcr_cassettes/user/secret.json +0 -1
@@ -4,5 +4,6 @@ rvm:
4
4
  - 1.9.3
5
5
  - jruby-19mode # JRuby in 1.9 mode
6
6
  # - rbx-19mode
7
- # env:
7
+ env:
8
+ - PINBOARD_USERNAME=phlipper
8
9
  # - RBXOPT=-X19
data/README.md CHANGED
@@ -41,13 +41,19 @@ $ gem install pinboard_api
41
41
 
42
42
  ## Getting Started
43
43
 
44
- You will need to set your username and password for the Pinboard service.
44
+ You will need to set your username and password for the Pinboard service:
45
45
 
46
46
  ```ruby
47
47
  PinboardApi.username = "phlipper"
48
48
  PinboardApi.password = "[REDACTED]"
49
49
  ```
50
50
 
51
+ Alternately, you may use the new [`auth_token`](http://pinboard.in/api/#authentication) method:
52
+
53
+ ```ruby
54
+ PinboardApi.auth_token = "[REDACTED]"
55
+ ```
56
+
51
57
  You may also set the SSL options which will be passed through to [Faraday](https://github.com/technoweenie/faraday#readme):
52
58
 
53
59
  ```ruby
@@ -66,11 +72,27 @@ The `PinboardApi` namespace implements the 3 primary object types: `Post`, `Tag`
66
72
  * [posts/update](https://pinboard.in/api#update) - Check to see when a user last posted an item.
67
73
 
68
74
  ```ruby
69
- PinboardApi::Post.update
75
+ PinboardApi::Post.last_update
70
76
  # => 2012-07-07 04:18:28 UTC
71
77
  ```
72
78
 
73
- * ~~[posts/add](https://pinboard.in/api#posts_add) - add a new bookmark~~
79
+ * [posts/add](https://pinboard.in/api#posts_add) - add a new bookmark
80
+
81
+ ```ruby
82
+ attributes = { url: "http://phlippers.net/pinboard_api", description: "A Ruby client for the Pinboard.in API", tags: %[ruby awesomesauce] }
83
+
84
+ post = PinboardApi::Post.new(attributes)
85
+ post.save
86
+ # => #<PinboardApi::Post:0x007fb42d905a68 @description="A Ruby client for the Pinboard.in API", @extended=nil, @hash=nil, @meta=nil, @url="http://phlippers.net/pinboard_api", @tags="ruby awesomesauce", @time=2012-07-13 23:03:34 -0700>
87
+
88
+ post = PinboardApi::Post.new
89
+ post.url = attributes[:url]
90
+ post.description = attributes[:description]
91
+ post.save
92
+
93
+ PinboardApi::Post.create(attributes)
94
+ ```
95
+
74
96
  * [posts/delete](https://pinboard.in/api#posts_delete) - delete an existing bookmark
75
97
 
76
98
  ```ruby
@@ -176,9 +198,7 @@ PinboardApi::User.secret
176
198
 
177
199
  ## TODO
178
200
 
179
- * Implement Post.add/create
180
- * Implement support for the new `[auth_token](http://pinboard.in/api/#authentication)`
181
- * Cleanup/refactor internal exception handling
201
+ * Implement support for rate limiting
182
202
 
183
203
 
184
204
  ## Contributing
@@ -0,0 +1,26 @@
1
+ class NilClass
2
+ def blank?
3
+ true
4
+ end
5
+ end
6
+
7
+ class FalseClass
8
+ def blank?
9
+ true
10
+ end
11
+ end
12
+
13
+ class Object
14
+ def blank?
15
+ respond_to?(:empty?) ? empty? : !self
16
+ end
17
+ end
18
+
19
+ class String
20
+ # 0x3000: fullwidth whitespace
21
+ NON_WHITESPACE_REGEXP = /[^\s#{[0x3000].pack("U")}]/
22
+
23
+ def blank?
24
+ self !~ NON_WHITESPACE_REGEXP
25
+ end
26
+ end
@@ -2,8 +2,12 @@ require "faraday"
2
2
  require "faraday_middleware"
3
3
 
4
4
  require "core_ext/array"
5
+ require "core_ext/blank"
5
6
  require "core_ext/hash"
6
7
 
8
+ require "pinboard_api/exceptions"
9
+ require "pinboard_api/request_utils"
10
+
7
11
  require "pinboard_api/post"
8
12
  require "pinboard_api/tag"
9
13
  require "pinboard_api/user"
@@ -12,7 +16,7 @@ require "pinboard_api/version"
12
16
  module PinboardApi
13
17
 
14
18
  class << self
15
- attr_accessor :username, :password, :adapter, :ssl_options
19
+ attr_accessor :username, :password, :auth_token, :adapter, :ssl_options
16
20
  end
17
21
 
18
22
  def self.adapter
@@ -28,18 +32,20 @@ module PinboardApi
28
32
  end
29
33
 
30
34
  def self.api_url
31
- "https://#{username}:#{password}@api.pinboard.in"
35
+ "https://api.pinboard.in"
32
36
  end
33
37
 
34
38
  def self.connection
35
39
  Faraday.new(url: api_url, ssl: ssl_options) do |builder|
36
40
  builder.response :logger if ENV["PINBOARD_LOGGER"]
37
41
  builder.response :xml, content_type: /\bxml$/
42
+ builder.basic_auth username, password if auth_token.blank?
38
43
  builder.adapter adapter
39
44
  end
40
45
  end
41
46
 
42
47
  def self.request(path, options = {}, &blk)
48
+ options.merge!(auth_token: auth_token) unless auth_token.blank?
43
49
  PinboardApi.connection.get(path, options, &blk)
44
50
  end
45
51
  end
@@ -0,0 +1,5 @@
1
+ module PinboardApi
2
+ class PinboardApiError < StandardError; end
3
+ class InvalidPostError < PinboardApiError; end
4
+ class InvalidResponseError < PinboardApiError; end
5
+ end
@@ -1,5 +1,6 @@
1
1
  module PinboardApi
2
2
  class Post
3
+ include PinboardApi::RequestUtils
3
4
 
4
5
  attr_reader :description, :extended, :hash, :meta, :url
5
6
 
@@ -23,41 +24,71 @@ module PinboardApi
23
24
  @tags.is_a?(String) ? @tags.split(/\s+/) : @tags
24
25
  end
25
26
 
27
+ def save(options = {})
28
+ validate!
29
+ path = "/#{PinboardApi.api_version}/posts/add"
30
+ params = {
31
+ url: @url,
32
+ description: @description,
33
+ extended: @extended,
34
+ tags: Post.tag_param_string(tags),
35
+ dt: options[:dt],
36
+ replace: yes_no(options[:replace]),
37
+ shared: yes_no(options[:shared]),
38
+ toread: yes_no(options[:toread])
39
+ }
40
+
41
+ result = PinboardApi.request(path, params).body["result"]
42
+ parse_result_code(result)
43
+ end
44
+
26
45
  def destroy
27
46
  path = "/#{PinboardApi.api_version}/posts/delete"
28
- body = PinboardApi.request(path, url: @url).body["result"]
47
+ result = PinboardApi.request(path, url: @url).body["result"]
48
+ parse_result_code(result)
49
+ end
29
50
 
30
- if body && body.fetch("code", "") == "done"
31
- self
32
- else
33
- raise RuntimeError, "unknown response"
51
+ def validate!
52
+ if @url.blank?
53
+ raise InvalidPostError, "url cannot be blank"
54
+ end
55
+ if @description.blank?
56
+ raise InvalidPostError, "description cannot be blank"
57
+ end
58
+ end
59
+
60
+ def parse_result_code(result)
61
+ unless result && code = result.fetch("code", false)
62
+ raise InvalidResponseError, "unknown response"
34
63
  end
64
+
65
+ code == "done" ? self : raise(InvalidResponseError, code.to_s)
66
+ end
67
+
68
+ def self.create(attributes)
69
+ new(attributes).save
35
70
  end
36
71
 
37
72
  def self.destroy(url)
38
73
  if post = find(url: url).first
39
74
  post.destroy
40
75
  else
41
- raise RuntimeError, "unknown response"
76
+ raise InvalidResponseError, "unknown response"
42
77
  end
43
78
  end
44
79
 
45
80
  def self.all(options = {})
46
81
  path = "/#{PinboardApi.api_version}/posts/all"
47
-
48
- tag = tag_param_string(options[:tag])
49
- fromdt = dt_param_string(options[:fromdt])
50
- todt = dt_param_string(options[:todt])
51
-
52
- response = PinboardApi.request(path) do |req|
53
- req.params["tag"] = tag if tag
54
- req.params["start"] = options[:start] if options[:start]
55
- req.params["results"] = options[:results] if options[:results]
56
- req.params["fromdt"] = fromdt if fromdt
57
- req.params["todt"] = todt if todt
58
- req.params["meta"] = 1 if options[:meta]
59
- end
60
-
82
+ params = {
83
+ tag: tag_param_string(options[:tag]),
84
+ start: options[:start],
85
+ results: options[:results],
86
+ fromdt: dt_param_string(options[:fromdt]),
87
+ todt: dt_param_string(options[:todt]),
88
+ meta: (options[:meta] ? 1 : 0)
89
+ }
90
+
91
+ response = PinboardApi.request(path, params)
61
92
  extract_posts(response.body["posts"])
62
93
  end
63
94
 
@@ -83,24 +114,15 @@ module PinboardApi
83
114
 
84
115
  def self.recent(options = {})
85
116
  path = "/#{PinboardApi.api_version}/posts/recent"
86
- tag = tag_param_string(options[:tag])
87
- count = options[:count]
88
-
89
- response = PinboardApi.request(path) do |req|
90
- req.params["tag"] = tag if tag
91
- req.params["count"] = count if count
92
- end
93
-
117
+ params = { tag: tag_param_string(options[:tag]), count: options[:count] }
118
+ response = PinboardApi.request(path, params)
94
119
  extract_posts(response.body["posts"])
95
120
  end
96
121
 
97
122
  def self.dates(options = {})
98
123
  path = "/#{PinboardApi.api_version}/posts/dates"
99
124
  tag = tag_param_string(options[:tag])
100
-
101
- response = PinboardApi.request(path) do |req|
102
- req.params["tag"] = tag if tag
103
- end
125
+ response = PinboardApi.request(path, tag: tag)
104
126
 
105
127
  dates = response.body["dates"]["date"]
106
128
  dates.map do |date|
@@ -125,13 +147,5 @@ module PinboardApi
125
147
  collection
126
148
  end
127
149
  end
128
-
129
- def self.dt_param_string(time)
130
- time.nil? ? nil : time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
131
- end
132
-
133
- def self.tag_param_string(tags)
134
- tags.nil? ? nil : Array.wrap(tags).join(",")
135
- end
136
150
  end
137
151
  end
@@ -0,0 +1,22 @@
1
+ module PinboardApi
2
+ module RequestUtils
3
+ def self.included(receiver)
4
+ receiver.extend ClassMethods
5
+ end
6
+
7
+ def yes_no(value)
8
+ return nil if value.nil?
9
+ value ? "yes" : "no"
10
+ end
11
+
12
+ module ClassMethods
13
+ def tag_param_string(tags)
14
+ tags.nil? ? nil : Array.wrap(tags).join(",")
15
+ end
16
+
17
+ def dt_param_string(time)
18
+ time.nil? ? nil : time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,6 +3,8 @@ module PinboardApi
3
3
  attr_reader :name, :count
4
4
 
5
5
  def initialize(attributes = {})
6
+ attributes.stringify_keys!
7
+
6
8
  @name = attributes["name"] || attributes["tag"]
7
9
  @count = attributes["count"].to_i
8
10
  end
@@ -13,23 +15,23 @@ module PinboardApi
13
15
  req.params["old"] = @name
14
16
  req.params["new"] = new_name.to_s
15
17
  end
16
- body = response.body
18
+ result = response.body["result"]
17
19
 
18
- if body["result"] == "done"
19
- Tag.new("name" => new_name, "count" => @count)
20
+ if result == "done"
21
+ Tag.new(name: new_name, count: @count)
20
22
  else
21
- raise body["result"].to_s
23
+ raise InvalidResponseError, result.to_s
22
24
  end
23
25
  end
24
26
 
25
27
  def destroy
26
28
  path = "/#{PinboardApi.api_version}/tags/delete"
27
- body = PinboardApi.request(path, tag: @name).body
29
+ result = PinboardApi.request(path, tag: @name).body["result"]
28
30
 
29
- if body["result"] == "done"
31
+ if result == "done"
30
32
  self
31
33
  else
32
- raise body["result"].to_s
34
+ raise InvalidResponseError, result.to_s
33
35
  end
34
36
  end
35
37
 
@@ -38,7 +40,7 @@ module PinboardApi
38
40
  body = PinboardApi.request(path).body
39
41
  body["tags"]["tag"].map { |tag| new(tag) }
40
42
  rescue
41
- raise RuntimeError, "unknown response"
43
+ raise InvalidResponseError, "unknown response"
42
44
  end
43
45
 
44
46
  def self.find(name)
@@ -1,3 +1,3 @@
1
1
  module PinboardApi
2
- VERSION = "0.7.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+
3
+ describe NilClass do
4
+ it { nil.must_be :blank? }
5
+ end
6
+
7
+ describe FalseClass do
8
+ it { false.must_be :blank? }
9
+ end
10
+
11
+ describe Object do
12
+ it { [].must_be :blank? }
13
+ it { ["foo"].wont_be :blank? }
14
+ end
15
+
16
+ describe String do
17
+ it { "".must_be :blank? }
18
+ it { "foo".wont_be :blank? }
19
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe PinboardApi do
4
+ it { PinboardApi::PinboardApiError.new.must_be_kind_of StandardError }
5
+ it { PinboardApi::InvalidPostError.new.must_be_kind_of PinboardApi::PinboardApiError }
6
+ it { PinboardApi::InvalidResponseError.new.must_be_kind_of PinboardApi::PinboardApiError }
7
+ end
@@ -4,18 +4,22 @@ describe PinboardApi do
4
4
  before do
5
5
  @username = PinboardApi.username
6
6
  @password = PinboardApi.password
7
+ @auth_token = PinboardApi.auth_token
7
8
 
8
9
  PinboardApi.username = nil
9
10
  PinboardApi.password = nil
11
+ PinboardApi.auth_token = nil
10
12
  end
11
13
 
12
14
  after do
13
15
  PinboardApi.username = @username
14
16
  PinboardApi.password = @password
17
+ PinboardApi.auth_token = @auth_token
15
18
  end
16
19
 
17
20
  it { PinboardApi.must_respond_to :username }
18
21
  it { PinboardApi.must_respond_to :password }
22
+ it { PinboardApi.must_respond_to :auth_token }
19
23
 
20
24
  it { PinboardApi.must_respond_to :api_version }
21
25
  it { PinboardApi.api_version.must_equal "v1" }
@@ -27,7 +31,7 @@ describe PinboardApi do
27
31
  it { PinboardApi.ssl_options.must_be_kind_of Hash }
28
32
 
29
33
  it { PinboardApi.must_respond_to :api_url }
30
- it { PinboardApi.api_url.must_equal "https://:@api.pinboard.in" }
34
+ it { PinboardApi.api_url.must_equal "https://api.pinboard.in" }
31
35
 
32
36
  it { PinboardApi.must_respond_to :connection }
33
37
  it { PinboardApi.connection.must_be_kind_of Faraday::Connection }
@@ -36,23 +40,22 @@ describe PinboardApi do
36
40
  before do
37
41
  @username = PinboardApi.username
38
42
  @password = PinboardApi.password
43
+ @auth_token = PinboardApi.auth_token
39
44
 
40
45
  PinboardApi.username = "username"
41
46
  PinboardApi.password = "password"
47
+ PinboardApi.auth_token = "auth_token"
42
48
  end
43
49
 
44
50
  after do
45
51
  PinboardApi.username = @username
46
52
  PinboardApi.password = @password
53
+ PinboardApi.auth_token = @auth_token
47
54
  end
48
55
 
49
56
  it { PinboardApi.username.must_equal "username" }
50
57
  it { PinboardApi.password.must_equal "password" }
51
-
52
- it "adds credentials to the api_url" do
53
- url = "https://username:password@api.pinboard.in"
54
- PinboardApi.api_url.must_equal url
55
- end
58
+ it { PinboardApi.auth_token.must_equal "auth_token" }
56
59
  end
57
60
 
58
61
  describe "ssl_options" do