pinboard_api 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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