typeform_data 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +7 -0
- data/Rakefile +1 -2
- data/lib/typeform_data.rb +1 -0
- data/lib/typeform_data/client.rb +3 -3
- data/lib/typeform_data/comparable_by_id_and_config.rb +2 -2
- data/lib/typeform_data/config.rb +1 -1
- data/lib/typeform_data/errors.rb +18 -3
- data/lib/typeform_data/requestor.rb +61 -26
- data/lib/typeform_data/typeform.rb +4 -4
- data/lib/typeform_data/utils.rb +24 -0
- data/lib/typeform_data/value_class.rb +2 -1
- data/lib/typeform_data/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d70cdb5034873b422938f7cf426b0b1341a992ab
|
4
|
+
data.tar.gz: a36e89135238d96c2ebe7ea0466c56fbf2f2b23f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ec3a03e56573d5b75fed0aff79d2f1a54dfeb176433c7afc1621e6532a76e7fb31a0ca9ca8477ca41c78ba04b9ef8b05ea4b0b5c9c88df2a7e870ea3ea47ed1
|
7
|
+
data.tar.gz: 38532360add588301c0e3c3621088e72dba3ccc7758cecd1a84f43ae4eb27a4824010a32db4ae0d874088573f5a6b3fd1ebf14ec517ed3b869f989124394858a
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -62,6 +62,13 @@ deserialized = client.load(serialized)
|
|
62
62
|
=> true
|
63
63
|
```
|
64
64
|
|
65
|
+
### Error-handling
|
66
|
+
|
67
|
+
Unless you've encountered a bug, all exceptions raised by this gem should extend from `TypeformData::Error`. For the full exception hierarchy, see `lib/typeform_data/errors`.
|
68
|
+
|
69
|
+
If a HTTP request to Typeform fails with an error that we expect to be transient (e.g. a 503) we retry the HTTP request up to 3 times, after waiting 1, 2 and 4 seconds. If you'd prefer a fail-fast approach, send us a PR!
|
70
|
+
|
71
|
+
|
65
72
|
## Notes on the API
|
66
73
|
|
67
74
|
So far, we've found Typeform's current Data API to be confusing. In particular, there are a couple design decisions that have been a source of friction for us:
|
data/Rakefile
CHANGED
data/lib/typeform_data.rb
CHANGED
data/lib/typeform_data/client.rb
CHANGED
@@ -11,7 +11,7 @@ module TypeformData
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.new_from_config(config)
|
14
|
-
raise ArgumentError, 'Missing config' unless config
|
14
|
+
raise TypeformData::ArgumentError, 'Missing config' unless config
|
15
15
|
new(api_key: config.api_key)
|
16
16
|
end
|
17
17
|
|
@@ -27,12 +27,12 @@ module TypeformData
|
|
27
27
|
|
28
28
|
def all_typeforms
|
29
29
|
get('forms').parsed_json.map do |form_hash|
|
30
|
-
|
30
|
+
TypeformData::Typeform.new(@config, form_hash)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
34
|
def typeform(id)
|
35
|
-
|
35
|
+
TypeformData::Typeform.new(@config, id: id)
|
36
36
|
end
|
37
37
|
|
38
38
|
def dump(object)
|
@@ -10,14 +10,14 @@ module TypeformData
|
|
10
10
|
|
11
11
|
def ==(other)
|
12
12
|
unless other.respond_to?(:sort_key, true) && other.respond_to?(:config, true)
|
13
|
-
raise ArgumentError, "#{other.inspect} does not specify a sort key and config"
|
13
|
+
raise TypeformData::ArgumentError, "#{other.inspect} does not specify a sort key and config"
|
14
14
|
end
|
15
15
|
other.sort_key == sort_key && other.config == config
|
16
16
|
end
|
17
17
|
|
18
18
|
def <=>(other)
|
19
19
|
unless other.respond_to?(:sort_key)
|
20
|
-
raise ArgumentError, "#{other.inspect} does not specify a sort key"
|
20
|
+
raise TypeformData::ArgumentError, "#{other.inspect} does not specify a sort key"
|
21
21
|
end
|
22
22
|
other.sort_key <=> sort_key
|
23
23
|
end
|
data/lib/typeform_data/config.rb
CHANGED
@@ -5,7 +5,7 @@ module TypeformData
|
|
5
5
|
|
6
6
|
def initialize(api_key:)
|
7
7
|
unless api_key.is_a?(String) && api_key.length.positive?
|
8
|
-
raise ArgumentError, 'An API key (as a nonempty String) is required'
|
8
|
+
raise TypeformData::ArgumentError, 'An API key (as a nonempty String) is required'
|
9
9
|
end
|
10
10
|
@api_key = api_key
|
11
11
|
end
|
data/lib/typeform_data/errors.rb
CHANGED
@@ -1,13 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module TypeformData
|
3
|
+
|
3
4
|
class Error < StandardError; end
|
4
|
-
|
5
|
-
class ConnectionRefused < Error; end
|
6
|
-
class BadRequest < Error; end
|
5
|
+
|
7
6
|
class ArgumentError < Error; end
|
7
|
+
|
8
|
+
class InvalidApiKey < Error; end
|
9
|
+
|
8
10
|
class UnexpectedError < Error; end
|
9
11
|
|
12
|
+
class UnexpectedHttpError < UnexpectedError; end
|
13
|
+
class UnexpectedHttpResponse < UnexpectedHttpError; end
|
14
|
+
class BadRequest < UnexpectedHttpResponse; end
|
15
|
+
class TransientResponseError < UnexpectedHttpResponse; end
|
16
|
+
|
10
17
|
# When using the 'token' field in requests, the API may return a 404 even if the endpoint path
|
11
18
|
# is correct.
|
12
19
|
class InvalidEndpointOrMissingResource < Error; end
|
20
|
+
|
21
|
+
module Errors
|
22
|
+
def self.stringify_error(error)
|
23
|
+
"#{error.backtrace.first}: #{error.message} (#{error.class})\n" +
|
24
|
+
error.backtrace.drop(1).map { |line| "\t#{line}\n" }.join
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
13
28
|
end
|
@@ -1,25 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'typeform_data/utils'
|
2
3
|
|
3
4
|
module TypeformData
|
4
5
|
module Requestor
|
5
6
|
|
7
|
+
RETRY_EXCEPTIONS = [
|
8
|
+
# We wouldn't ordinarily retry in this case, but Typeform appears to have transient issues
|
9
|
+
# with their SSL.
|
10
|
+
OpenSSL::SSL::SSLError,
|
11
|
+
|
12
|
+
Errno::ECONNREFUSED,
|
13
|
+
TypeformData::TransientResponseError,
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
RETRY_RESPONSE_CLASSES = [
|
17
|
+
Net::HTTPServiceUnavailable,
|
18
|
+
Net::HTTPTooManyRequests,
|
19
|
+
Net::HTTPBadGateway,
|
20
|
+
].freeze
|
21
|
+
|
6
22
|
def self.get(config, endpoint, params = nil)
|
7
23
|
request(config, Net::HTTP::Get, request_path(config, endpoint), params)
|
8
24
|
end
|
9
25
|
|
10
|
-
def self.request_path(config, endpoint)
|
26
|
+
private_class_method def self.request_path(config, endpoint)
|
11
27
|
"/v#{config.api_version}/#{endpoint}"
|
12
28
|
end
|
13
29
|
|
14
|
-
|
15
|
-
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
30
|
+
# @raise TypeformData::Error
|
17
31
|
# @return TypeformData::ApiResponse
|
18
|
-
def self.request(config, method_class, path, input_params = {})
|
32
|
+
private_class_method def self.request(config, method_class, path, input_params = {})
|
19
33
|
params = input_params.dup
|
20
34
|
params[:key] = config.api_key
|
21
35
|
|
22
|
-
|
36
|
+
begin
|
37
|
+
Utils.retry_with_exponential_backoff(RETRY_EXCEPTIONS, max_retries: 3) do
|
38
|
+
request_and_validate_response(config, method_class, path, params)
|
39
|
+
end
|
40
|
+
rescue *RETRY_EXCEPTIONS => error
|
41
|
+
raise UnexpectedHttpError, 'Unexpected HTTP error (retried 3 times): ' +
|
42
|
+
TypeformData::Errors.stringify_error(error)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return TypeformData::ApiResponse
|
47
|
+
private_class_method def self.request_and_validate_response(config, method_class, path, params)
|
48
|
+
response = request_response(config, method_class, path, params)
|
49
|
+
|
50
|
+
case response
|
51
|
+
when Net::HTTPSuccess
|
52
|
+
return TypeformData::ApiResponse.new(response)
|
53
|
+
|
54
|
+
when Net::HTTPNotFound
|
55
|
+
raise TypeformData::InvalidEndpointOrMissingResource, path
|
56
|
+
when Net::HTTPForbidden
|
57
|
+
raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
|
58
|
+
when Net::HTTPBadRequest
|
59
|
+
raise TypeformData::BadRequest, 'Response was a Net::HTTPBadRequest with body: '\
|
60
|
+
"#{response.body}. Your request with params: #{params} could not be processed."
|
61
|
+
when *RETRY_RESPONSE_CLASSES
|
62
|
+
raise TypeformData::TransientResponseError, "Response was a #{response.class} "\
|
63
|
+
"(code #{response.code}) with message #{response.message}"
|
64
|
+
else
|
65
|
+
raise TypeformData::UnexpectedHttpResponse, 'Unexpected HTTP response with code: '\
|
66
|
+
"#{response.code} and message: #{response.message}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private_class_method def self.request_response(config, method_class, path, params)
|
71
|
+
Net::HTTP.new(config.host, config.port).tap { |http|
|
23
72
|
http.use_ssl = true
|
24
73
|
|
25
74
|
# Uncomment this line for debugging:
|
@@ -30,28 +79,14 @@ module TypeformData
|
|
30
79
|
'Content-Type' => 'application/json'
|
31
80
|
)
|
32
81
|
)
|
82
|
+
rescue *RETRY_EXCEPTIONS
|
83
|
+
raise # So retry_with_exponential_backoff can catch the exception and retry.
|
33
84
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
raise TypeformData::InvalidApiKey, "Invalid api key: #{config.api_key}"
|
39
|
-
when Net::HTTPBadRequest then
|
40
|
-
raise TypeformData::BadRequest, 'There was an error processing your request: '\
|
41
|
-
"#{response.body}, with params: #{params}"
|
42
|
-
when Net::HTTPSuccess
|
43
|
-
return TypeformData::ApiResponse.new(response)
|
44
|
-
else
|
45
|
-
raise TypeformData::UnexpectedError, "A #{response.code} error has occurred: "\
|
46
|
-
"'#{response.message}'"
|
47
|
-
end
|
48
|
-
|
49
|
-
rescue Errno::ECONNREFUSED
|
50
|
-
raise TypeformData::ConnectionRefused, 'The connection was refused'
|
85
|
+
# Why are we rescuing StandardError? See http://stackoverflow.com/a/11802674/1067145
|
86
|
+
rescue StandardError => error
|
87
|
+
raise UnexpectedHttpError, 'Unexpected HTTP error: ' +
|
88
|
+
TypeformData::Errors.stringify_error(error)
|
51
89
|
end
|
52
|
-
# rubocop:enable Metrics/MethodLength
|
53
|
-
|
54
|
-
private_class_method :request
|
55
90
|
|
56
91
|
end
|
57
92
|
end
|
@@ -142,24 +142,24 @@ module TypeformData
|
|
142
142
|
params = input_params.dup
|
143
143
|
|
144
144
|
params.keys.select { |key| key.is_a?(Symbol) }.each do |sym|
|
145
|
-
raise
|
145
|
+
raise TypeformData::ArgumentError, 'Duplicate keys' if params.key?(sym.to_s)
|
146
146
|
params[sym.to_s] = params[sym]
|
147
147
|
params.delete(sym)
|
148
148
|
end
|
149
149
|
|
150
150
|
params.keys.each do |key|
|
151
151
|
next if PERMITTED_KEYS.key?(key) && params[key].is_a?(PERMITTED_KEYS[key])
|
152
|
-
raise
|
152
|
+
raise TypeformData::ArgumentError, "Invalid/unsupported param: #{key}"
|
153
153
|
end
|
154
154
|
|
155
155
|
if params['limit'] && params['limit'] > MAX_PAGE_SIZE
|
156
|
-
raise
|
156
|
+
raise TypeformData::ArgumentError, "The maximum limit is #{MAX_PAGE_SIZE}. You "\
|
157
157
|
"provided: #{params['limit']}"
|
158
158
|
end
|
159
159
|
|
160
160
|
if params['token']
|
161
161
|
if params.keys.length > 1
|
162
|
-
raise
|
162
|
+
raise TypeformData::ArgumentError, "'token' may not be combined with other filters"
|
163
163
|
end
|
164
164
|
else
|
165
165
|
params['offset'] ||= 0
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# Repeats the block until it succeeds or a limit is reached, waiting twice as long as it
|
5
|
+
# previously did after each failure.
|
6
|
+
# @param rescued_exceptions [Class] Subclasses of Exception.
|
7
|
+
# @param max_retries [Integer]
|
8
|
+
# @param initial_wait [Integer] In seconds.
|
9
|
+
def self.retry_with_exponential_backoff(rescued_exceptions, max_retries: 5, initial_wait: 1)
|
10
|
+
seconds_to_wait = initial_wait
|
11
|
+
|
12
|
+
max_retries.times do |iteration|
|
13
|
+
begin
|
14
|
+
break yield
|
15
|
+
rescue *rescued_exceptions
|
16
|
+
sleep seconds_to_wait
|
17
|
+
seconds_to_wait *= 2
|
18
|
+
|
19
|
+
raise if iteration == max_retries - 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -6,7 +6,8 @@ module TypeformData
|
|
6
6
|
|
7
7
|
def initialize(config, attrs)
|
8
8
|
unless config && config.is_a?(TypeformData::Config)
|
9
|
-
raise ArgumentError, 'Expected a TypeformData::Config instance as the first
|
9
|
+
raise TypeformData::ArgumentError, 'Expected a TypeformData::Config instance as the first '\
|
10
|
+
'argument'
|
10
11
|
end
|
11
12
|
@config = config
|
12
13
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: typeform_data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Wallace
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -169,6 +169,7 @@ files:
|
|
169
169
|
- lib/typeform_data/typeform/question.rb
|
170
170
|
- lib/typeform_data/typeform/response.rb
|
171
171
|
- lib/typeform_data/typeform/stats.rb
|
172
|
+
- lib/typeform_data/utils.rb
|
172
173
|
- lib/typeform_data/value_class.rb
|
173
174
|
- lib/typeform_data/version.rb
|
174
175
|
- typeform_data.gemspec
|