pinboard_api 0.1.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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