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