pinboard_api 0.1.0 → 0.7.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 (49) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +8 -0
  3. data/Gemfile +5 -1
  4. data/README.md +81 -16
  5. data/Rakefile +10 -0
  6. data/lib/core_ext/array.rb +11 -0
  7. data/lib/core_ext/hash.rb +8 -0
  8. data/lib/pinboard_api.rb +24 -18
  9. data/lib/pinboard_api/post.rb +103 -21
  10. data/lib/pinboard_api/tag.rb +11 -9
  11. data/lib/pinboard_api/user.rb +1 -1
  12. data/lib/pinboard_api/version.rb +1 -1
  13. data/spec/core_ext/array_spec.rb +11 -0
  14. data/spec/core_ext/hash_spec.rb +18 -0
  15. data/spec/fixtures/vcr_cassettes/posts/all/custom_count.json +1 -0
  16. data/spec/fixtures/vcr_cassettes/posts/all/custom_tag.json +1 -0
  17. data/spec/fixtures/vcr_cassettes/posts/all/custom_times.json +1 -0
  18. data/spec/fixtures/vcr_cassettes/posts/all/default_values.json +1 -0
  19. data/spec/fixtures/vcr_cassettes/posts/all/not_found.json +1 -0
  20. data/spec/fixtures/vcr_cassettes/posts/dates/custom_tag.json +1 -0
  21. data/spec/fixtures/vcr_cassettes/posts/dates/default_values.json +1 -0
  22. data/spec/fixtures/vcr_cassettes/posts/delete/unsuccessful_class.json +1 -0
  23. data/spec/fixtures/vcr_cassettes/posts/destroy/successful_class.json +1 -0
  24. data/spec/fixtures/vcr_cassettes/posts/destroy/successful_instance.json +1 -0
  25. data/spec/fixtures/vcr_cassettes/posts/destroy/unsuccessful_instance.json +1 -0
  26. data/spec/fixtures/vcr_cassettes/posts/find/found.json +1 -1
  27. data/spec/fixtures/vcr_cassettes/posts/find/not_found.json +1 -1
  28. data/spec/fixtures/vcr_cassettes/posts/recent/custom_count.json +1 -0
  29. data/spec/fixtures/vcr_cassettes/posts/recent/custom_tag.json +1 -0
  30. data/spec/fixtures/vcr_cassettes/posts/recent/default_values.json +1 -0
  31. data/spec/fixtures/vcr_cassettes/posts/suggest.json +1 -0
  32. data/spec/fixtures/vcr_cassettes/posts/update.json +1 -1
  33. data/spec/fixtures/vcr_cassettes/tags/all.json +1 -1
  34. data/spec/fixtures/vcr_cassettes/tags/destroy/successful_class.json +1 -0
  35. data/spec/fixtures/vcr_cassettes/tags/destroy/successful_instance.json +1 -0
  36. data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_class.json +1 -0
  37. data/spec/fixtures/vcr_cassettes/tags/destroy/unsuccessful_instance.json +1 -0
  38. data/spec/fixtures/vcr_cassettes/tags/find/found.json +1 -1
  39. data/spec/fixtures/vcr_cassettes/tags/find/not_found.json +1 -1
  40. data/spec/fixtures/vcr_cassettes/tags/rename/successful.json +1 -1
  41. data/spec/fixtures/vcr_cassettes/tags/rename/unsuccessful.json +1 -1
  42. data/spec/fixtures/vcr_cassettes/user/secret.json +1 -1
  43. data/spec/pinboard_api_spec.rb +0 -4
  44. data/spec/post_spec.rb +358 -29
  45. data/spec/spec_helper.rb +8 -4
  46. data/spec/tag_spec.rb +27 -11
  47. metadata +48 -7
  48. data/spec/fixtures/vcr_cassettes/tags/delete/successful.json +0 -1
  49. data/spec/fixtures/vcr_cassettes/tags/delete/unsuccessful.json +0 -1
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  *.rbc
3
+ .rbx/
3
4
  .bundle
4
5
  .config
5
6
  .yardoc
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ # - rbx-19mode
7
+ # env:
8
+ # - RBXOPT=-X19
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in pinboard_api.gemspec
4
4
  gemspec
5
+
6
+ platforms :jruby do
7
+ gem "jruby-openssl"
8
+ end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PinboardApi
1
+ # PinboardApi [![Build Status](https://secure.travis-ci.org/phlipper/pinboard_api.png?branch=master)](http://travis-ci.org/phlipper/pinboard_api) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/phlipper/pinboard_api)
2
2
 
3
3
  ## Description
4
4
 
@@ -9,8 +9,13 @@ This client aims to cover all of the Pinboard [API v1](https://pinboard.in/api/)
9
9
 
10
10
  ## Requirements
11
11
 
12
- * This library requires Ruby 1.9.2 or newer.
13
12
  * You must have a paid Pinboard account to use the API. It is a great service and you can [signup here](https://pinboard.in/signup/) if you don't already have an account.
13
+ * Currently tested on the following Ruby versions:
14
+ * 1.9.2
15
+ * 1.9.3
16
+ * JRuby (1.9 mode)
17
+
18
+ _Note:_ Specs are currently passing on Rubinius with `RBXOPT=-X19` on my local machine but there is a failing spec on [Travis CI](http://travis-ci.org/#!/phlipper/pinboard_api). I will update the `README` with official support for Rubinus once everything runs smoothly on Travis.
14
19
 
15
20
 
16
21
  ## Installation
@@ -34,6 +39,22 @@ $ gem install pinboard_api
34
39
  ```
35
40
 
36
41
 
42
+ ## Getting Started
43
+
44
+ You will need to set your username and password for the Pinboard service.
45
+
46
+ ```ruby
47
+ PinboardApi.username = "phlipper"
48
+ PinboardApi.password = "[REDACTED]"
49
+ ```
50
+
51
+ You may also set the SSL options which will be passed through to [Faraday](https://github.com/technoweenie/faraday#readme):
52
+
53
+ ```ruby
54
+ PinboardApi.ssl_options = { ca_file: "/opt/local/share/curl/curl-ca-bundle.crt" }
55
+ ```
56
+
57
+
37
58
  ## Usage
38
59
 
39
60
  The `PinboardApi` namespace implements the 3 primary object types: `Post`, `Tag`, and `User`.
@@ -50,24 +71,63 @@ PinboardApi::Post.update
50
71
  ```
51
72
 
52
73
  * ~~[posts/add](https://pinboard.in/api#posts_add) - add a new bookmark~~
53
- * ~~[posts/delete](https://pinboard.in/api#posts_delete) - delete an existing bookmark~~
74
+ * [posts/delete](https://pinboard.in/api#posts_delete) - delete an existing bookmark
75
+
76
+ ```ruby
77
+ post = PinboardApi::Post.find(url: "https://pinboard.in/u:phlipper").first
78
+ post.destroy
79
+ # => #<PinboardApi::Post:0x007ffcb5166cf0 @description="Pinboard - antisocial bookmarking", @extended="", @hash="bc857ba651d134be0c9a5267e943c3ce", @url="https://pinboard.in/u:phlipper", @meta=nil, @tags="test", @time="2012-07-11T09:16:14Z">
80
+
81
+ PinboardApi::Post.destroy("https://pinboard.in/u:phlipper")
82
+ # => #<PinboardApi::Post:0x007f98d6946d78 @description="Pinboard - antisocial bookmarking", @extended="", @hash="bc857ba651d134be0c9a5267e943c3ce", @url="https://pinboard.in/u:phlipper", @meta=nil, @tags="test", @time="2012-07-11T09:17:36Z">
83
+ ```
84
+
54
85
  * [posts/get](https://pinboard.in/api#posts_get) - get bookmark for a single date, or fetch specific items by URL
55
86
 
56
87
  ```ruby
57
88
  PinboardApi::Post.find(tag: "test")
58
- # => [#<PinboardApi::Post:0x007fdce4547388 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @href="http://test.com/", @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
89
+ # => [#<PinboardApi::Post:0x007fdce4547388 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @url="http://test.com/", @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
59
90
 
60
91
  PinboardApi::Post.find(hash: "dbb720d788ffaeb0afb7572104072f4a", meta: "yes")
61
- # => [#<PinboardApi::Post:0x007fac2b9d6690 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @href="http://test.com/", @meta="73b192512e3e4829806f5eee0a6b456d", @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
62
-
63
92
  PinboardApi::Post.find(dt: Date.parse("2012-07-07"))
64
- # => [#<PinboardApi::Post:0x007fac2ba0fdf0 @description="Test.com – Certification Program Management – Create Online Tests with This Authoring, Management, Training and E-Learning Software", @extended="", @hash="dbb720d788ffaeb0afb7572104072f4a", @href="http://test.com/", @meta=nil, @tags="test junk", @time="2012-07-07T04:18:28Z">, ...]
65
93
  ```
66
94
 
67
- * ~~[posts/dates](https://pinboard.in/api#posts_dates) - list dates on which bookmarks were posted~~
68
- * ~~[posts/recent](https://pinboard.in/api#posts_recent) - fetch recent bookmarks~~
69
- * ~~[posts/all](https://pinboard.in/api#posts_all) - fetch all bookmarks by date, tag, or range~~
70
- * ~~[posts/suggest](https://pinboard.in/api#posts_suggest) - fetch popular and recommended tags for a url~~
95
+ * [posts/dates](https://pinboard.in/api#posts_dates) - list dates on which bookmarks were posted
96
+
97
+ ```ruby
98
+ PinboardApi::Post.dates
99
+ # => [{"count"=>1, "date"=>#<Date: 2012-07-10 ((2456119j,0s,0n),+0s,2299161j)>}, {"count"=>3, "date"=>#<Date: 2012-07-08 ((2456117j,0s,0n),+0s,2299161j)>}, ...]
100
+
101
+ PinboardApi::Post.dates(tag: "ruby")
102
+ ```
103
+
104
+ * [posts/recent](https://pinboard.in/api#posts_recent) - fetch recent bookmarks
105
+
106
+ ```ruby
107
+ PinboardApi::Post.recent
108
+ # => [#<PinboardApi::Post:0x007ffe150e1fd0 @description="Techniques to Secure Your Website with Ruby on Rails..."> ...]
109
+
110
+ PinboardApi::Post.recent(count: 3)
111
+ PinboardApi::Post.recent(tag: "ruby")
112
+ PinboardApi::Post.recent(count: 25, tag: ["ruby", "programming"])
113
+ ```
114
+
115
+ * [posts/all](https://pinboard.in/api#posts_all) - fetch all bookmarks by date, tag, or range
116
+
117
+ ```ruby
118
+ PinboardApi::Post.all
119
+ # => [#<PinboardApi::Post:0x007ffe150e1fd0 @description="Techniques to Secure Your Website with Ruby on Rails..."> ...]
120
+
121
+ PinboardApi::Post.all(tag: %w[ruby programming], meta: true, results: 30)
122
+ PinboardApi::Post.all(start: 50, fromdt: 2.weeks.ago, todt: 1.week.ago)
123
+ ```
124
+
125
+ * [posts/suggest](https://pinboard.in/api#posts_suggest) - fetch popular and recommended tags for a url
126
+
127
+ ```ruby
128
+ PinboardApi::Post.suggest("http://blog.com")
129
+ # => {"popular"=>["hosting", "blogs", "blog", "free"], "recommended"=>["blog", "blogging", "blogs", "free"]}
130
+ ```
71
131
 
72
132
 
73
133
  ### Tag
@@ -86,13 +146,11 @@ PinboardApi::Tag.find("leadership")
86
146
 
87
147
  ```ruby
88
148
  tag = PinboardApi::Tag.find("foo")
149
+ tag.destroy
89
150
  # => #<PinboardApi::Tag:0x007fdce45f56e0 @name="foo", @count=1>
90
151
 
91
- tag.delete
92
- # => #<PinboardApi::Tag:0x007fdce45f56e0 @name="foo", @count=1>
93
-
94
- tag = PinboardApi::Tag.find("foo")
95
- # => nil
152
+ PinboardApi::Tag.destroy("foo")
153
+ # => #<PinboardApi::Tag:0x007fdce45f20f8 @name="foo", @count=1>
96
154
  ```
97
155
 
98
156
  * [tags/rename](https://pinboard.in/api#tags_rename) - rename a tag
@@ -116,6 +174,13 @@ PinboardApi::User.secret
116
174
  ```
117
175
 
118
176
 
177
+ ## TODO
178
+
179
+ * Implement Post.add/create
180
+ * Implement support for the new `[auth_token](http://pinboard.in/api/#authentication)`
181
+ * Cleanup/refactor internal exception handling
182
+
183
+
119
184
  ## Contributing
120
185
 
121
186
  1. Fork it
data/Rakefile CHANGED
@@ -1,2 +1,12 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ task :default => :spec
6
+
7
+ Rake::TestTask.new(:spec) do |t|
8
+ t.libs << "lib"
9
+ t.libs << "spec"
10
+ t.pattern = "spec/**/*_spec.rb"
11
+ t.verbose = false
12
+ end
@@ -0,0 +1,11 @@
1
+ class Array
2
+ def self.wrap(object)
3
+ if object.nil?
4
+ []
5
+ elsif object.respond_to?(:to_ary)
6
+ object.to_ary || [object]
7
+ else
8
+ [object]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def stringify_keys!
3
+ keys.each do |key|
4
+ self[key.to_s] = delete(key)
5
+ end
6
+ self
7
+ end
8
+ end
data/lib/pinboard_api.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require "faraday"
2
2
  require "faraday_middleware"
3
3
 
4
+ require "core_ext/array"
5
+ require "core_ext/hash"
6
+
4
7
  require "pinboard_api/post"
5
8
  require "pinboard_api/tag"
6
9
  require "pinboard_api/user"
@@ -10,30 +13,33 @@ module PinboardApi
10
13
 
11
14
  class << self
12
15
  attr_accessor :username, :password, :adapter, :ssl_options
16
+ end
13
17
 
14
- def adapter
15
- @adapter ||= :net_http
16
- end
18
+ def self.adapter
19
+ @adapter ||= :net_http
20
+ end
17
21
 
18
- def ssl_options
19
- @ssl_options ||= {}
20
- end
22
+ def self.ssl_options
23
+ @ssl_options ||= {}
24
+ end
21
25
 
22
- def api_version
23
- "v1"
24
- end
26
+ def self.api_version
27
+ "v1"
28
+ end
25
29
 
26
- def api_url
27
- "https://#{username}:#{password}@api.pinboard.in"
28
- end
30
+ def self.api_url
31
+ "https://#{username}:#{password}@api.pinboard.in"
32
+ end
29
33
 
30
- def connection
31
- Faraday.new(url: api_url, ssl: ssl_options) do |builder|
32
- builder.response :logger if ENV["PINBOARD_LOGGER"]
33
- builder.response :xml, content_type: /\bxml$/
34
- builder.adapter adapter
35
- end
34
+ def self.connection
35
+ Faraday.new(url: api_url, ssl: ssl_options) do |builder|
36
+ builder.response :logger if ENV["PINBOARD_LOGGER"]
37
+ builder.response :xml, content_type: /\bxml$/
38
+ builder.adapter adapter
36
39
  end
37
40
  end
38
41
 
42
+ def self.request(path, options = {}, &blk)
43
+ PinboardApi.connection.get(path, options, &blk)
44
+ end
39
45
  end
@@ -1,55 +1,137 @@
1
1
  module PinboardApi
2
2
  class Post
3
3
 
4
- attr_reader :description, :extended, :hash, :href, :meta
4
+ attr_reader :description, :extended, :hash, :meta, :url
5
5
 
6
6
  def initialize(attributes = {})
7
+ attributes.stringify_keys!
8
+
7
9
  @description = attributes["description"]
8
10
  @extended = attributes["extended"]
9
11
  @hash = attributes["hash"]
10
- @href = attributes["href"]
11
12
  @meta = attributes["meta"]
13
+ @url = attributes["url"] || attributes["href"]
12
14
  @tags = attributes["tags"] || attributes["tag"]
13
15
  @time = attributes["time"] || Time.now
14
16
  end
15
17
 
16
18
  def time
17
- if @time.is_a?(Time)
18
- @time
19
- elsif @time.is_a?(Date)
20
- @time.to_time
21
- else
22
- Time.parse(@time)
23
- end
19
+ @time.is_a?(String) ? Time.parse(@time) : @time.to_time
24
20
  end
25
21
 
26
22
  def tags
27
23
  @tags.is_a?(String) ? @tags.split(/\s+/) : @tags
28
24
  end
29
25
 
26
+ def destroy
27
+ path = "/#{PinboardApi.api_version}/posts/delete"
28
+ body = PinboardApi.request(path, url: @url).body["result"]
29
+
30
+ if body && body.fetch("code", "") == "done"
31
+ self
32
+ else
33
+ raise RuntimeError, "unknown response"
34
+ end
35
+ end
36
+
37
+ def self.destroy(url)
38
+ if post = find(url: url).first
39
+ post.destroy
40
+ else
41
+ raise RuntimeError, "unknown response"
42
+ end
43
+ end
44
+
45
+ def self.all(options = {})
46
+ 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
+
61
+ extract_posts(response.body["posts"])
62
+ end
30
63
 
31
64
  def self.find(options = {})
32
65
  path = "/#{PinboardApi.api_version}/posts/get"
33
- response = PinboardApi.connection.get(path) do |req|
66
+ response = PinboardApi.request(path) do |req|
34
67
  options.each_pair { |k,v| req.params[k.to_s] = v }
35
68
  end
69
+ extract_posts(response.body["posts"])
70
+ end
71
+
72
+ def self.last_update
73
+ path = "/#{PinboardApi.api_version}/posts/update"
74
+ body = PinboardApi.request(path).body
75
+ Time.parse(body["update"]["time"])
76
+ end
36
77
 
37
- posts = response.body["posts"]
38
- if posts.keys.include?("post")
39
- posts.inject([]) do |collection, tuple|
40
- key, attrs = tuple
41
- Array(collection) << new(attrs) if key == "post"
78
+ def self.suggest(url)
79
+ path = "/#{PinboardApi.api_version}/posts/suggest"
80
+ response = PinboardApi.request(path, url: url)
81
+ response.body["suggested"]
82
+ end
83
+
84
+ def self.recent(options = {})
85
+ 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
+
94
+ extract_posts(response.body["posts"])
95
+ end
96
+
97
+ def self.dates(options = {})
98
+ path = "/#{PinboardApi.api_version}/posts/dates"
99
+ tag = tag_param_string(options[:tag])
100
+
101
+ response = PinboardApi.request(path) do |req|
102
+ req.params["tag"] = tag if tag
103
+ end
104
+
105
+ dates = response.body["dates"]["date"]
106
+ dates.map do |date|
107
+ { "count" => date["count"].to_i, "date" => Date.parse(date["date"]) }
108
+ end
109
+ end
110
+
111
+
112
+ def self.extract_posts(payload)
113
+ unless payload.respond_to?(:keys) && payload.keys.include?("post")
114
+ return Array.new
115
+ end
116
+
117
+ # response.body["posts"] - "429 Too Many Requests. Wait 60 seconds before fetching posts/all again."
118
+
119
+ payload.inject([]) do |collection, (key, attrs)|
120
+ if key == "post"
121
+ Array.wrap(attrs).each do |post|
122
+ Array.wrap(collection) << new(post)
123
+ end
42
124
  end
43
- else
44
- Array.new
125
+ collection
45
126
  end
46
127
  end
47
128
 
48
- def self.update
49
- path = "/#{PinboardApi.api_version}/posts/update"
50
- body = PinboardApi.connection.get(path).body
51
- Time.parse(body["update"]["time"])
129
+ def self.dt_param_string(time)
130
+ time.nil? ? nil : time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
52
131
  end
53
132
 
133
+ def self.tag_param_string(tags)
134
+ tags.nil? ? nil : Array.wrap(tags).join(",")
135
+ end
54
136
  end
55
137
  end
@@ -9,25 +9,22 @@ module PinboardApi
9
9
 
10
10
  def rename(new_name)
11
11
  path = "/#{PinboardApi.api_version}/tags/rename"
12
- response = PinboardApi.connection.get(path) do |req|
13
- req.params["old"] = self.name
12
+ response = PinboardApi.request(path) do |req|
13
+ req.params["old"] = @name
14
14
  req.params["new"] = new_name.to_s
15
15
  end
16
16
  body = response.body
17
17
 
18
18
  if body["result"] == "done"
19
- Tag.new("name" => new_name, "count" => self.count)
19
+ Tag.new("name" => new_name, "count" => @count)
20
20
  else
21
21
  raise body["result"].to_s
22
22
  end
23
23
  end
24
24
 
25
- def delete
25
+ def destroy
26
26
  path = "/#{PinboardApi.api_version}/tags/delete"
27
- response = PinboardApi.connection.get(path) do |req|
28
- req.params["tag"] = self.name
29
- end
30
- body = response.body
27
+ body = PinboardApi.request(path, tag: @name).body
31
28
 
32
29
  if body["result"] == "done"
33
30
  self
@@ -38,13 +35,18 @@ module PinboardApi
38
35
 
39
36
  def self.all
40
37
  path = "/#{PinboardApi.api_version}/tags/get"
41
- body = PinboardApi.connection.get(path).body
38
+ body = PinboardApi.request(path).body
42
39
  body["tags"]["tag"].map { |tag| new(tag) }
40
+ rescue
41
+ raise RuntimeError, "unknown response"
43
42
  end
44
43
 
45
44
  def self.find(name)
46
45
  all.detect { |t| t.name == name }
47
46
  end
48
47
 
48
+ def self.destroy(tag)
49
+ find(tag).destroy
50
+ end
49
51
  end
50
52
  end