thumbtack 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/lib/thumbtack.rb +2 -1
- data/lib/thumbtack/adapters/basic_adapter.rb +83 -0
- data/lib/thumbtack/client.rb +26 -19
- data/lib/thumbtack/hash_to_digest.rb +12 -7
- data/lib/thumbtack/note.rb +1 -1
- data/lib/thumbtack/note_summary.rb +1 -1
- data/lib/thumbtack/post.rb +1 -1
- data/lib/thumbtack/posts.rb +13 -5
- data/lib/thumbtack/suggestion.rb +18 -4
- data/lib/thumbtack/symbolize_keys.rb +22 -0
- data/lib/thumbtack/types/boolean.rb +1 -1
- data/lib/thumbtack/types/date.rb +1 -1
- data/lib/thumbtack/types/date_time.rb +1 -1
- data/lib/thumbtack/types/identity.rb +1 -1
- data/lib/thumbtack/types/integer.rb +1 -1
- data/lib/thumbtack/types/length_validation.rb +10 -8
- data/lib/thumbtack/types/md5.rb +16 -3
- data/lib/thumbtack/types/range_validation.rb +5 -3
- data/lib/thumbtack/types/tags.rb +16 -3
- data/lib/thumbtack/types/text.rb +1 -1
- data/lib/thumbtack/types/title.rb +1 -1
- data/lib/thumbtack/types/url.rb +1 -1
- data/lib/thumbtack/version.rb +1 -1
- data/test/test_helper.rb +2 -2
- data/test/thumbtack/client_test.rb +29 -1
- data/test/thumbtack/integration/basic_adapter_test.rb +24 -0
- data/test/thumbtack/note_summary_test.rb +1 -1
- data/test/thumbtack/notes_test.rb +34 -34
- data/test/thumbtack/post_test.rb +1 -1
- data/test/thumbtack/posts_test.rb +113 -95
- data/test/thumbtack/specification_test.rb +2 -1
- data/test/thumbtack/suggestion_test.rb +4 -4
- data/test/thumbtack/tags_test.rb +30 -24
- data/test/thumbtack/types/date_test.rb +2 -2
- data/test/thumbtack/types/date_time_test.rb +4 -3
- data/test/thumbtack/types/identity_test.rb +1 -1
- data/test/thumbtack/types/md5_test.rb +3 -3
- data/test/thumbtack/types/tags_test.rb +2 -2
- data/test/thumbtack/types/text_test.rb +2 -2
- data/test/thumbtack/user_test.rb +10 -6
- metadata +26 -11
- data/lib/thumbtack/types.rb +0 -9
- data/test/thumbtack/integration/client_test.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dfceebca8bd2653fbb6a32085aa3449e3a872c1d
|
4
|
+
data.tar.gz: f3be0d3c15e9f9ca487bba144f9192fe300941b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57742f4274a670de859c45b9e4373176205d01ba219a1d8b0eccc3443ef61a8f171881c37917a8bb7a174fc531171c89163df9e7b65172f52c5bc2eef703c6e8
|
7
|
+
data.tar.gz: 392b5f79e1ecd490bf8fa7c336f48c150ba47b19c7f8c010c6be8eea4c8327d3239c3279fd5d4b95ce1fec6b1f3552a56e80581992740895c35e7a1e7d7dba8f
|
data/lib/thumbtack.rb
CHANGED
@@ -25,7 +25,6 @@ require 'json'
|
|
25
25
|
require 'net/http'
|
26
26
|
require 'uri'
|
27
27
|
|
28
|
-
require 'thumbtack/types'
|
29
28
|
require 'thumbtack/types/range_validation'
|
30
29
|
require 'thumbtack/types/length_validation'
|
31
30
|
require 'thumbtack/types/boolean'
|
@@ -45,8 +44,10 @@ require 'thumbtack/posts'
|
|
45
44
|
require 'thumbtack/tags'
|
46
45
|
require 'thumbtack/user'
|
47
46
|
require 'thumbtack/hash_to_digest'
|
47
|
+
require 'thumbtack/symbolize_keys'
|
48
48
|
require 'thumbtack/note'
|
49
49
|
require 'thumbtack/note_summary'
|
50
50
|
require 'thumbtack/notes'
|
51
|
+
require 'thumbtack/adapters/basic_adapter'
|
51
52
|
require 'thumbtack/client'
|
52
53
|
require 'thumbtack/version'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Thumbtack
|
4
|
+
module Adapters
|
5
|
+
# A basic adapter using Ruby's builtin HTTP and JSON parsing libraries
|
6
|
+
class BasicAdapter
|
7
|
+
# The status code for rate limited responses from the Pinboard API
|
8
|
+
TOO_MANY_REQUESTS_CODE = '429'.freeze
|
9
|
+
|
10
|
+
# The response format requested from the Pinboard API
|
11
|
+
RESPONSE_FORMAT = 'json'.freeze
|
12
|
+
|
13
|
+
# The base Pinboard API URL.
|
14
|
+
BASE_URL = 'https://api.pinboard.in/v1'.freeze
|
15
|
+
|
16
|
+
# Initialize a BasicAdapter
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# adapter = BasicAdapter.new(username, token)
|
20
|
+
#
|
21
|
+
# @param [String] username
|
22
|
+
# the user to authenticate with
|
23
|
+
# @param [String] token
|
24
|
+
# the API token for the user, found on the Pinboard settings page
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def initialize(username, token)
|
28
|
+
@username = username
|
29
|
+
@token = token
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve JSON from the Pinboard API
|
33
|
+
#
|
34
|
+
# @param [String] path
|
35
|
+
# the path to fetch from, relative to the base Pinboard API URL
|
36
|
+
#
|
37
|
+
# @param [Hash] params
|
38
|
+
# query parameters to append to the URL
|
39
|
+
#
|
40
|
+
# @return [Hash] the response parsed from the JSON
|
41
|
+
#
|
42
|
+
# @raise [RateLimitError] if the response is rate-limited
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
def get(path, params = EMPTY_HASH)
|
46
|
+
uri = URI("#{BASE_URL}#{path}")
|
47
|
+
uri.query = extend_parameters(params)
|
48
|
+
JSON.parse(http_response(uri).body)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Retrieve HTTP response from a URI
|
54
|
+
#
|
55
|
+
# @param [URI] uri
|
56
|
+
# the uri to fetch from
|
57
|
+
# @return [Net::HTTPResponse]
|
58
|
+
# the response
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
# @raise [RateLimitError] if the response is rate-limited
|
62
|
+
def http_response(uri)
|
63
|
+
Net::HTTP.get_response(uri).tap do |response|
|
64
|
+
fail RateLimitError if response.code == TOO_MANY_REQUESTS_CODE
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Extend parameters with authentication and format parameters
|
69
|
+
#
|
70
|
+
# @param [Hash{#to_s => #to_s}] parameters
|
71
|
+
# the parameters to extend
|
72
|
+
# @return [Hash{#to_s => #to_s}]
|
73
|
+
# the extended parameters
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
def extend_parameters(parameters)
|
77
|
+
base_params = { auth_token: "#{@username}:#{@token}",
|
78
|
+
format: RESPONSE_FORMAT }
|
79
|
+
URI.encode_www_form(parameters.merge(base_params))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/thumbtack/client.rb
CHANGED
@@ -3,12 +3,6 @@
|
|
3
3
|
module Thumbtack
|
4
4
|
# Wraps each interaction with the Pinboard API
|
5
5
|
class Client
|
6
|
-
# The status code for rate limited responses from the Pinboard API
|
7
|
-
TOO_MANY_REQUESTS_CODE = '429'.freeze
|
8
|
-
|
9
|
-
# The base Pinboard API URL.
|
10
|
-
BASE_URL = 'https://api.pinboard.in/v1'.freeze
|
11
|
-
|
12
6
|
# Username used by the client to make authenticated requests
|
13
7
|
#
|
14
8
|
# @example
|
@@ -29,6 +23,16 @@ module Thumbtack
|
|
29
23
|
# @api public
|
30
24
|
attr_reader :token
|
31
25
|
|
26
|
+
# Token used by the client to make authenticated requests
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# client.adapter # => #<Thumbtack::Adapters::BasicAdapter...
|
30
|
+
#
|
31
|
+
# @return [Adapter]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
attr_reader :adapter
|
35
|
+
|
32
36
|
# Initialize a Client
|
33
37
|
#
|
34
38
|
# @example
|
@@ -38,10 +42,18 @@ module Thumbtack
|
|
38
42
|
# the user to authenticate with
|
39
43
|
# @param [String] token
|
40
44
|
# the API token for the user account, found on the Pinboard settings page
|
45
|
+
# @param [Hash] options
|
46
|
+
# options for the construction of the client
|
47
|
+
# @option options [BasicAdapter] :adapter
|
48
|
+
# an adapter to use for communicating with the Pinboard API
|
41
49
|
#
|
42
50
|
# @api public
|
43
|
-
def initialize(username, token)
|
44
|
-
@username
|
51
|
+
def initialize(username, token, options = EMPTY_HASH)
|
52
|
+
@username = username
|
53
|
+
@token = token
|
54
|
+
@adapter = options.fetch(:adapter) do
|
55
|
+
Adapters::BasicAdapter.new(@username, @token)
|
56
|
+
end
|
45
57
|
end
|
46
58
|
|
47
59
|
# Retrieve JSON from the Pinboard API
|
@@ -58,14 +70,7 @@ module Thumbtack
|
|
58
70
|
#
|
59
71
|
# @api private
|
60
72
|
def get(path, params = EMPTY_HASH)
|
61
|
-
|
62
|
-
|
63
|
-
base_params = { auth_token: "#{@username}:#{@token}", format: 'json' }
|
64
|
-
uri.query = URI.encode_www_form(params.merge(base_params))
|
65
|
-
|
66
|
-
response = Net::HTTP.get_response(uri)
|
67
|
-
fail RateLimitError if response.code == TOO_MANY_REQUESTS_CODE
|
68
|
-
JSON.parse(response.body)
|
73
|
+
@adapter.get(path, params)
|
69
74
|
end
|
70
75
|
|
71
76
|
# Perform an action request against the Pinboard API
|
@@ -83,9 +88,11 @@ module Thumbtack
|
|
83
88
|
#
|
84
89
|
# @api private
|
85
90
|
# @see https://pinboard.in/api/#errors
|
86
|
-
def action(path, params
|
87
|
-
response = get(path, params)
|
88
|
-
|
91
|
+
def action(path, params)
|
92
|
+
response = @adapter.get(path, params)
|
93
|
+
unless response['result_code'] == 'done'
|
94
|
+
fail ResultError, response['result_code']
|
95
|
+
end
|
89
96
|
self
|
90
97
|
end
|
91
98
|
|
@@ -1,22 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Thumbtack
|
2
4
|
# Handles renaming the hash attribute to digest from response hashes.
|
3
5
|
#
|
4
6
|
# @api private
|
5
7
|
class HashToDigest
|
8
|
+
HASH = 'hash'.freeze
|
9
|
+
DIGEST = 'digest'.freeze
|
10
|
+
|
6
11
|
# Rename any attribute called hash to digest
|
7
12
|
#
|
8
13
|
# @example
|
9
|
-
# HashToDigest.rename('hash' => '1234') # => {
|
14
|
+
# HashToDigest.rename('hash' => '1234') # => { 'digest' => '1234' }
|
10
15
|
#
|
11
|
-
# @param [Hash{String => Object}]
|
12
|
-
#
|
16
|
+
# @param [Hash{String => Object}] hash
|
17
|
+
# the response hash
|
13
18
|
#
|
14
|
-
# @return [Hash{
|
15
|
-
# a hash with any key
|
19
|
+
# @return [Hash{String => Object}]
|
20
|
+
# a hash with any key 'hash' renamed to 'digest'
|
16
21
|
def self.rename(hash)
|
17
22
|
attrs = hash.dup
|
18
|
-
digest = attrs.delete(
|
19
|
-
|
23
|
+
digest = attrs.delete(HASH)
|
24
|
+
attrs.merge(DIGEST => digest)
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
data/lib/thumbtack/note.rb
CHANGED
data/lib/thumbtack/post.rb
CHANGED
data/lib/thumbtack/posts.rb
CHANGED
@@ -226,11 +226,7 @@ module Thumbtack
|
|
226
226
|
def dates(options = EMPTY_HASH)
|
227
227
|
parameters = Specification.new(tag: Types::Tags).parameters(options)
|
228
228
|
response = @client.get('/posts/dates', parameters)
|
229
|
-
|
230
|
-
response.fetch('dates', EMPTY_HASH).map do |date, count|
|
231
|
-
[Types::Date.from_parameter(date), count.to_i]
|
232
|
-
end
|
233
|
-
]
|
229
|
+
dates_with_counts_from(response)
|
234
230
|
end
|
235
231
|
|
236
232
|
private
|
@@ -245,5 +241,17 @@ module Thumbtack
|
|
245
241
|
Post.from_hash(post_hash)
|
246
242
|
end
|
247
243
|
end
|
244
|
+
|
245
|
+
# Create Hash of dates to counts from dates response
|
246
|
+
#
|
247
|
+
# @return [Hash{Date => Integer}]
|
248
|
+
#
|
249
|
+
# @api private
|
250
|
+
def dates_with_counts_from(response)
|
251
|
+
entries = response.fetch('dates', EMPTY_HASH).map do |date, count|
|
252
|
+
[Types::Date.from_parameter(date), count.to_i]
|
253
|
+
end
|
254
|
+
Hash[entries]
|
255
|
+
end
|
248
256
|
end
|
249
257
|
end
|
data/lib/thumbtack/suggestion.rb
CHANGED
@@ -56,10 +56,24 @@ module Thumbtack
|
|
56
56
|
# @api private
|
57
57
|
# @see Client#get
|
58
58
|
def self.from_array(array)
|
59
|
-
popular
|
60
|
-
|
61
|
-
new(popular: popular.fetch(POPULAR_KEY, EMPTY_ARRAY),
|
62
|
-
recommended: recommended.fetch(RECOMMENDED_KEY, EMPTY_ARRAY))
|
59
|
+
new(popular: entries_for(POPULAR_KEY, array),
|
60
|
+
recommended: entries_for(RECOMMENDED_KEY, array))
|
63
61
|
end
|
62
|
+
|
63
|
+
# Extracts the entries for a given key in an array of hashes
|
64
|
+
#
|
65
|
+
# @param [String] key
|
66
|
+
# A key from which to extract the entries
|
67
|
+
#
|
68
|
+
# @param [Array<Hash{String => Array<String>}>] array
|
69
|
+
# An array of strings associated with the key
|
70
|
+
#
|
71
|
+
# @return [Array[String]]
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
def self.entries_for(key, array)
|
75
|
+
array.find { |hash| hash.key?(key) }.fetch(key, EMPTY_ARRAY)
|
76
|
+
end
|
77
|
+
private_class_method :entries_for
|
64
78
|
end
|
65
79
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Thumbtack
|
4
|
+
# Handles converting string keys to symbols in a response hash
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class SymbolizeKeys
|
8
|
+
# Convert any keys to symbols
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# SymbolizeKeys.symbolize('digest' => '1234') # => { :digest => '1234' }
|
12
|
+
#
|
13
|
+
# @param [Hash{#to_sym => Object}] hash
|
14
|
+
# the response hash
|
15
|
+
#
|
16
|
+
# @return [Hash{Symbol => Object}]
|
17
|
+
# a hash with symbol keys
|
18
|
+
def self.symbolize(hash)
|
19
|
+
Hash[hash.map { |key, value| [key.to_sym, value] }]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/thumbtack/types/date.rb
CHANGED
@@ -1,26 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Thumbtack
|
2
4
|
module Types
|
3
|
-
# Handles validation of
|
4
|
-
# Pinboard
|
5
|
+
# Handles validation of the length of values
|
5
6
|
#
|
6
7
|
# @api private
|
7
8
|
class LengthValidation
|
8
9
|
# Validate a value
|
9
10
|
#
|
10
|
-
# @param [Object]
|
11
|
+
# @param [Object] maximum_length
|
11
12
|
# the maximum length
|
12
13
|
# @param [Object] value
|
13
14
|
# the value to validate
|
14
15
|
#
|
15
|
-
# @return [
|
16
|
+
# @return [self]
|
16
17
|
#
|
17
18
|
# @raise [Types::ValidationError]
|
18
|
-
# if the value is not
|
19
|
-
def self.validate(value,
|
20
|
-
unless value.length <=
|
19
|
+
# if the value is not less or equal to the maximum length
|
20
|
+
def self.validate(value, maximum_length)
|
21
|
+
unless value.length <= maximum_length
|
21
22
|
fail ValidationError,
|
22
|
-
"#{value} cannot be greater than #{
|
23
|
+
"#{value} cannot be greater than #{maximum_length} characters"
|
23
24
|
end
|
25
|
+
self
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/thumbtack/types/md5.rb
CHANGED
@@ -16,18 +16,31 @@ module Thumbtack
|
|
16
16
|
# @param [String] value
|
17
17
|
# the MD5 to validate
|
18
18
|
#
|
19
|
-
# @return [
|
19
|
+
# @return [self]
|
20
20
|
#
|
21
21
|
# @raise [Types::ValidationError]
|
22
22
|
# if the value is not a 32 character hexadecimal MD5 hash
|
23
23
|
def self.validate(value)
|
24
|
-
unless value
|
25
|
-
value.each_char.all? { |char| CHARACTERS.include?(char) }
|
24
|
+
unless valid_md5?(value)
|
26
25
|
fail ValidationError,
|
27
26
|
"#{value} must be a 32 character hexadecimal MD5 hash"
|
28
27
|
end
|
29
28
|
self
|
30
29
|
end
|
30
|
+
|
31
|
+
# If true, the value is a valid MD5 string
|
32
|
+
#
|
33
|
+
# @param [String] value
|
34
|
+
# the MD5 to validate
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
#
|
38
|
+
# @api private
|
39
|
+
def self.valid_md5?(value)
|
40
|
+
value.length == LENGTH &&
|
41
|
+
value.each_char.all? { |char| CHARACTERS.include?(char) }
|
42
|
+
end
|
43
|
+
private_class_method :valid_md5?
|
31
44
|
end
|
32
45
|
end
|
33
46
|
end
|