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 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