typeform_data 1.0.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: edd3d6b08e718fd4c2429253873d9c5b269774d6
4
- data.tar.gz: 8939f372f3a5fba107d4879b56ee04c02c2a885b
3
+ metadata.gz: d70cdb5034873b422938f7cf426b0b1341a992ab
4
+ data.tar.gz: a36e89135238d96c2ebe7ea0466c56fbf2f2b23f
5
5
  SHA512:
6
- metadata.gz: d54650fab170d77b860f49d9839d88039f2280ccf24401272f089ba303ce51cc1839628f8980bb2e5bb35256d481318c5cccd4ceb6672c42b821980c4d534111
7
- data.tar.gz: dd0308219f6e615bb0ac9e2218b450246c116212ac54fc1876261f4432aee99e53c9621ad9cb7fd4127b02ffc15e90631f65189e2a9ed45a85dce8bb17156b95
6
+ metadata.gz: 5ec3a03e56573d5b75fed0aff79d2f1a54dfeb176433c7afc1621e6532a76e7fb31a0ca9ca8477ca41c78ba04b9ef8b05ea4b0b5c9c88df2a7e870ea3ea47ed1
7
+ data.tar.gz: 38532360add588301c0e3c3621088e72dba3ccc7758cecd1a84f43ae4eb27a4824010a32db4ae0d874088573f5a6b3fd1ebf14ec517ed3b869f989124394858a
data/.rubocop.yml CHANGED
@@ -152,3 +152,6 @@ Style/DoubleNegation:
152
152
  # trusted source.
153
153
  Security/MarshalLoad:
154
154
  Enabled: false
155
+
156
+ Style/NumericPredicate:
157
+ Enabled: false
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
@@ -2,8 +2,7 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
5
+ t.libs = ['test', 'lib']
7
6
  t.test_files = FileList['test/**/*_test.rb']
8
7
  end
9
8
 
data/lib/typeform_data.rb CHANGED
@@ -5,6 +5,7 @@ require 'net/https'
5
5
  require 'uri'
6
6
  require 'json'
7
7
 
8
+ require 'typeform_data/utils'
8
9
  require 'typeform_data/version'
9
10
  require 'typeform_data/client'
10
11
  require 'typeform_data/errors'
@@ -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
- ::TypeformData::Typeform.new(@config, form_hash)
30
+ TypeformData::Typeform.new(@config, form_hash)
31
31
  end
32
32
  end
33
33
 
34
34
  def typeform(id)
35
- ::TypeformData::Typeform.new(@config, id: id)
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
@@ -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
@@ -1,13 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
  module TypeformData
3
+
3
4
  class Error < StandardError; end
4
- class InvalidApiKey < Error; end
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
- private_class_method :request_path
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
- response = Net::HTTP.new(config.host, config.port).tap { |http|
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
- case response
35
- when Net::HTTPNotFound then
36
- raise TypeformData::InvalidEndpointOrMissingResource, path
37
- when Net::HTTPForbidden then
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 ::TypeformData::ArgumentError, 'Duplicate keys' if params.key?(sym.to_s)
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 ::TypeformData::ArgumentError, "Invalid/unsupported param: #{key}"
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 ::TypeformData::ArgumentError, "The maximum limit is #{MAX_PAGE_SIZE}. You "\
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 ::TypeformData::ArgumentError, "'token' may not be combined with other filters"
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 argument'
9
+ raise TypeformData::ArgumentError, 'Expected a TypeformData::Config instance as the first '\
10
+ 'argument'
10
11
  end
11
12
  @config = config
12
13
 
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module TypeformData
3
- VERSION = '1.0.0'
3
+ VERSION = '2.0.0'
4
4
  end
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: 1.0.0
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-01-17 00:00:00.000000000 Z
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