rdstation-ruby-client 1.2.1 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/rdsm-ruby-client-issue-template.md +49 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +167 -0
  5. data/README.md +243 -43
  6. data/lib/rdstation-ruby-client.rb +6 -0
  7. data/lib/rdstation.rb +19 -0
  8. data/lib/rdstation/api_response.rb +3 -3
  9. data/lib/rdstation/authentication.rb +32 -3
  10. data/lib/rdstation/authorization.rb +24 -0
  11. data/lib/rdstation/builder/field.rb +70 -0
  12. data/lib/rdstation/client.rb +17 -74
  13. data/lib/rdstation/contacts.rb +21 -16
  14. data/lib/rdstation/error.rb +23 -15
  15. data/lib/rdstation/error/format.rb +21 -3
  16. data/lib/rdstation/error/formatter.rb +53 -7
  17. data/lib/rdstation/error_handler.rb +31 -26
  18. data/lib/rdstation/error_handler/bad_request.rb +30 -0
  19. data/lib/rdstation/error_handler/unauthorized.rb +17 -9
  20. data/lib/rdstation/events.rb +7 -19
  21. data/lib/rdstation/fields.rb +31 -7
  22. data/lib/rdstation/retryable_request.rb +35 -0
  23. data/lib/rdstation/version.rb +1 -1
  24. data/lib/rdstation/webhooks.rb +25 -17
  25. data/rdstation-ruby-client.gemspec +4 -1
  26. data/spec/lib/rdstation-ruby-client_spec.rb +1 -1
  27. data/spec/lib/rdstation/api_response_spec.rb +34 -0
  28. data/spec/lib/rdstation/authentication_spec.rb +164 -0
  29. data/spec/lib/rdstation/authorization_spec.rb +24 -0
  30. data/spec/lib/rdstation/builder/field_spec.rb +69 -0
  31. data/spec/lib/rdstation/client_spec.rb +37 -0
  32. data/spec/lib/rdstation/contacts_spec.rb +54 -41
  33. data/spec/lib/rdstation/error/format_spec.rb +46 -0
  34. data/spec/lib/rdstation/error/formatter_spec.rb +83 -0
  35. data/spec/lib/rdstation/error_handler/unauthorized_spec.rb +0 -29
  36. data/spec/lib/rdstation/error_handler_spec.rb +162 -26
  37. data/spec/lib/rdstation/events_spec.rb +20 -9
  38. data/spec/lib/rdstation/fields_spec.rb +10 -3
  39. data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
  40. data/spec/lib/rdstation/webhooks_spec.rb +41 -13
  41. data/spec/lib/rdstation_spec.rb +18 -0
  42. metadata +41 -12
  43. data/lib/rdstation/error_handler/default.rb +0 -15
  44. data/lib/rdstation/error_handler/resource_not_found.rb +0 -24
  45. data/spec/lib/rdstation/error_handler/default_spec.rb +0 -14
  46. data/spec/lib/rdstation/error_handler/resource_not_found_spec.rb +0 -54
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './format'
2
4
 
3
5
  module RDStation
@@ -15,6 +17,10 @@ module RDStation
15
17
  return from_flat_hash
16
18
  when RDStation::Error::Format::HASH_OF_ARRAYS
17
19
  return from_hash_of_arrays
20
+ when RDStation::Error::Format::HASH_OF_HASHES
21
+ return from_hash_of_hashes
22
+ when RDStation::Error::Format::HASH_OF_MULTIPLE_TYPES
23
+ return from_hash_of_multiple_types
18
24
  end
19
25
 
20
26
  errors
@@ -24,14 +30,29 @@ module RDStation
24
30
  [errors]
25
31
  end
26
32
 
27
- def from_hash_of_arrays
28
- errors.each_with_object([]) do |errors, array_of_errors|
29
- attribute_name = errors.first
30
- attribute_errors = errors.last
31
- path = { 'path' => "body.#{attribute_name}" }
32
- errors = attribute_errors.map { |error| error.merge(path) }
33
- array_of_errors.push(*errors)
33
+ def from_hash_of_multiple_types
34
+ array_of_errors = []
35
+ errors.each do |attribute_name, errors|
36
+ if errors.is_a? Array
37
+ result = build_error_from_array(attribute_name, errors)
38
+ end
39
+ if errors.is_a? Hash
40
+ result = build_error_from_multilingual_hash(attribute_name, errors)
41
+ end
42
+ array_of_errors.push(*result)
34
43
  end
44
+
45
+ array_of_errors
46
+ end
47
+
48
+ def from_hash_of_hashes
49
+ array_of_errors = []
50
+ errors.each do |attribute_name, errors|
51
+ result = build_error_from_multilingual_hash(attribute_name, errors)
52
+ array_of_errors.push(*result)
53
+ end
54
+
55
+ array_of_errors
35
56
  end
36
57
 
37
58
  def error_format
@@ -41,6 +62,31 @@ module RDStation
41
62
  def errors
42
63
  @errors ||= @error_response['errors']
43
64
  end
65
+
66
+ private
67
+
68
+ def build_error_from_array(attribute_name, attribute_errors)
69
+ path = { 'path' => "body.#{attribute_name}" }
70
+ attribute_errors.map { |error| error.merge(path) }
71
+ end
72
+
73
+ def build_error_from_multilingual_hash(attribute_name, errors_by_language)
74
+ array_of_errors = []
75
+ errors_by_language.each do |language, errors|
76
+ result = build_error_from_array("#{attribute_name}.#{language}", errors)
77
+ array_of_errors.push(*result)
78
+ end
79
+ array_of_errors
80
+ end
81
+
82
+ def from_hash_of_arrays
83
+ errors.each_with_object([]) do |errors, array_of_errors|
84
+ attribute_name = errors.first
85
+ attribute_errors = errors.last
86
+ errors = build_error_from_array(attribute_name, attribute_errors)
87
+ array_of_errors.push(*errors)
88
+ end
89
+ end
44
90
  end
45
91
  end
46
92
  end
@@ -1,37 +1,46 @@
1
1
  require_relative 'error/formatter'
2
- require_relative 'error_handler/conflicting_field'
3
- require_relative 'error_handler/default'
4
- require_relative 'error_handler/expired_access_token'
5
- require_relative 'error_handler/expired_code_grant'
6
- require_relative 'error_handler/invalid_credentials'
7
- require_relative 'error_handler/invalid_event_type'
8
- require_relative 'error_handler/resource_not_found'
2
+ require_relative 'error_handler/bad_request'
9
3
  require_relative 'error_handler/unauthorized'
10
4
 
11
5
  module RDStation
12
6
  class ErrorHandler
13
- ERROR_TYPES = [
14
- ErrorHandler::ConflictingField,
15
- ErrorHandler::ExpiredAccessToken,
16
- ErrorHandler::ExpiredCodeGrant,
17
- ErrorHandler::InvalidCredentials,
18
- ErrorHandler::InvalidEventType,
19
- ErrorHandler::ResourceNotFound,
20
- ErrorHandler::Unauthorized,
21
- ErrorHandler::Default
22
- ].freeze
23
-
24
7
  def initialize(response)
25
8
  @response = response
9
+ @code = response.code
26
10
  end
27
11
 
28
- def raise_errors
29
- error_types.each(&:raise_error)
12
+ def raise_error
13
+ raise error_class, array_of_errors.first if error_class < RDStation::Error
14
+
15
+ error_class.new(array_of_errors).raise_error
16
+ rescue JSON::ParserError => error
17
+ raise error_class, { 'error_message' => response.body }
30
18
  end
31
19
 
32
20
  private
33
21
 
34
- attr_reader :response
22
+ attr_reader :response, :code
23
+
24
+ def error_class
25
+ case code
26
+ when 400 then RDStation::ErrorHandler::BadRequest
27
+ when 401 then RDStation::ErrorHandler::Unauthorized
28
+ when 403 then RDStation::Error::Forbidden
29
+ when 404 then RDStation::Error::NotFound
30
+ when 405 then RDStation::Error::MethodNotAllowed
31
+ when 406 then RDStation::Error::NotAcceptable
32
+ when 409 then RDStation::Error::Conflict
33
+ when 415 then RDStation::Error::UnsupportedMediaType
34
+ when 422 then RDStation::Error::UnprocessableEntity
35
+ when 500 then RDStation::Error::InternalServerError
36
+ when 501 then RDStation::Error::NotImplemented
37
+ when 502 then RDStation::Error::BadGateway
38
+ when 503 then RDStation::Error::ServiceUnavailable
39
+ when 500..599 then RDStation::Error::ServerError
40
+ else
41
+ RDStation::Error::UnknownError
42
+ end
43
+ end
35
44
 
36
45
  def array_of_errors
37
46
  error_formatter.to_array.map do |error|
@@ -39,10 +48,6 @@ module RDStation
39
48
  end
40
49
  end
41
50
 
42
- def error_types
43
- ERROR_TYPES.map { |error_type| error_type.new(array_of_errors) }
44
- end
45
-
46
51
  def response_errors
47
52
  JSON.parse(response.body)
48
53
  end
@@ -55,7 +60,7 @@ module RDStation
55
60
  {
56
61
  'headers' => response.headers,
57
62
  'body' => JSON.parse(response.body),
58
- 'http_status' => response.code
63
+ 'http_status' => response.code,
59
64
  }
60
65
  end
61
66
  end
@@ -0,0 +1,30 @@
1
+ require_relative 'conflicting_field'
2
+ require_relative 'invalid_event_type'
3
+
4
+ module RDStation
5
+ class ErrorHandler
6
+ class BadRequest
7
+ BAD_REQUEST_ERRORS = [
8
+ ErrorHandler::ConflictingField,
9
+ ErrorHandler::InvalidEventType,
10
+ ].freeze
11
+
12
+ def initialize(array_of_errors)
13
+ @array_of_errors = array_of_errors
14
+ end
15
+
16
+ def raise_error
17
+ error_classes.each(&:raise_error)
18
+ raise RDStation::Error::BadRequest, @array_of_errors.first
19
+ end
20
+
21
+ private
22
+
23
+ def error_classes
24
+ BAD_REQUEST_ERRORS.map do |error|
25
+ error.new(@array_of_errors)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,23 +1,31 @@
1
+ require_relative 'expired_access_token'
2
+ require_relative 'expired_code_grant'
3
+ require_relative 'invalid_credentials'
4
+
1
5
  module RDStation
2
6
  class ErrorHandler
3
7
  class Unauthorized
4
- attr_reader :errors
5
-
6
- ERROR_CODE = 'UNAUTHORIZED'.freeze
8
+ UNAUTHORIZED_ERRORS = [
9
+ ErrorHandler::ExpiredAccessToken,
10
+ ErrorHandler::ExpiredCodeGrant,
11
+ ErrorHandler::InvalidCredentials,
12
+ ].freeze
7
13
 
8
- def initialize(errors)
9
- @errors = errors
14
+ def initialize(array_of_errors)
15
+ @array_of_errors = array_of_errors
10
16
  end
11
17
 
12
18
  def raise_error
13
- return if unauthorized_errors.empty?
14
- raise RDStation::Error::Unauthorized, unauthorized_errors.first
19
+ error_classes.each(&:raise_error)
20
+ raise RDStation::Error::Unauthorized, @array_of_errors.first
15
21
  end
16
22
 
17
23
  private
18
24
 
19
- def unauthorized_errors
20
- errors.select { |error| error['error_type'] == ERROR_CODE }
25
+ def error_classes
26
+ UNAUTHORIZED_ERRORS.map do |error|
27
+ error.new(@array_of_errors)
28
+ end
21
29
  end
22
30
  end
23
31
  end
@@ -1,31 +1,19 @@
1
1
  module RDStation
2
2
  class Events
3
3
  include HTTParty
4
+ include ::RDStation::RetryableRequest
4
5
 
5
6
  EVENTS_ENDPOINT = 'https://api.rd.services/platform/events'.freeze
6
7
 
7
- def initialize(auth_token)
8
- @auth_token = auth_token
8
+ def initialize(authorization:)
9
+ @authorization = authorization
9
10
  end
10
11
 
11
12
  def create(payload)
12
- response = self.class.post(EVENTS_ENDPOINT, headers: required_headers, body: payload.to_json)
13
- response_body = JSON.parse(response.body)
14
- return response_body unless errors?(response_body)
15
- RDStation::ErrorHandler.new(response).raise_errors
16
- end
17
-
18
- private
19
-
20
- def errors?(response_body)
21
- response_body.is_a?(Array) || response_body['errors']
22
- end
23
-
24
- def required_headers
25
- {
26
- 'Authorization' => "Bearer #{@auth_token}",
27
- 'Content-Type' => 'application/json'
28
- }
13
+ retryable_request(@authorization) do |authorization|
14
+ response = self.class.post(EVENTS_ENDPOINT, headers: authorization.headers, body: payload.to_json)
15
+ ApiResponse.build(response)
16
+ end
29
17
  end
30
18
  end
31
19
  end
@@ -1,24 +1,48 @@
1
1
  # encoding: utf-8
2
2
  module RDStation
3
- # More info: https://developers.rdstation.com/pt-BR/reference/contacts
3
+ # More info: https://developers.rdstation.com/pt-BR/reference/fields
4
4
  class Fields
5
5
  include HTTParty
6
+ include ::RDStation::RetryableRequest
6
7
 
7
8
  BASE_URL = 'https://api.rd.services/platform/contacts/fields'.freeze
8
9
 
9
- def initialize(auth_token)
10
- @auth_token = auth_token
10
+ def initialize(authorization:)
11
+ @authorization = authorization
11
12
  end
12
13
 
13
14
  def all
14
- response = self.class.get(BASE_URL, headers: required_headers)
15
- ApiResponse.build(response)
15
+ retryable_request(@authorization) do |authorization|
16
+ response = self.class.get(BASE_URL, headers: authorization.headers)
17
+ ApiResponse.build(response)
18
+ end
19
+ end
20
+
21
+ def create(payload)
22
+ retryable_request(@authorization) do |authorization|
23
+ response = self.class.post(BASE_URL, headers: authorization.headers, body: payload.to_json)
24
+ ApiResponse.build(response)
25
+ end
26
+ end
27
+
28
+ def update(uuid, payload)
29
+ retryable_request(@authorization) do |authorization|
30
+ response = self.class.patch(base_url(uuid), headers: authorization.headers, body: payload.to_json)
31
+ ApiResponse.build(response)
32
+ end
33
+ end
34
+
35
+ def delete(uuid)
36
+ retryable_request(@authorization) do |authorization|
37
+ response = self.class.delete(base_url(uuid), headers: authorization.headers)
38
+ ApiResponse.build(response)
39
+ end
16
40
  end
17
41
 
18
42
  private
19
43
 
20
- def required_headers
21
- { "Authorization" => "Bearer #{@auth_token}", "Content-Type" => "application/json" }
44
+ def base_url(path = '')
45
+ "#{BASE_URL}/#{path}"
22
46
  end
23
47
  end
24
48
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RDStation
4
+ module RetryableRequest
5
+ MAX_RETRIES = 1
6
+ def retryable_request(authorization)
7
+ retries = 0
8
+ begin
9
+ yield authorization
10
+ rescue ::RDStation::Error::ExpiredAccessToken => e
11
+ raise if !retry_possible?(authorization) || retries >= MAX_RETRIES
12
+
13
+ retries += 1
14
+ refresh_access_token(authorization)
15
+ retry
16
+ end
17
+ end
18
+
19
+ def retry_possible?(authorization)
20
+ [
21
+ RDStation.configuration&.client_id,
22
+ RDStation.configuration&.client_secret,
23
+ authorization.refresh_token
24
+ ].all?
25
+ end
26
+
27
+ def refresh_access_token(authorization)
28
+ client = RDStation::Authentication.new
29
+ response = client.update_access_token(authorization.refresh_token)
30
+ authorization.access_token = response['access_token']
31
+ authorization.access_token_expires_in = response['expires_in']
32
+ RDStation.configuration&.access_token_refresh_callback&.call(authorization)
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module RDStation
2
- VERSION = '1.2.1'.freeze
2
+ VERSION = '2.3.1'.freeze
3
3
  end
@@ -1,35 +1,47 @@
1
1
  module RDStation
2
2
  class Webhooks
3
3
  include HTTParty
4
+ include ::RDStation::RetryableRequest
4
5
 
5
- def initialize(auth_token)
6
- @auth_token = auth_token
6
+ def initialize(authorization:)
7
+ @authorization = authorization
7
8
  end
8
9
 
9
10
  def all
10
- response = self.class.get(base_url, headers: required_headers)
11
- ApiResponse.build(response)
11
+ retryable_request(@authorization) do |authorization|
12
+ response = self.class.get(base_url, headers: authorization.headers)
13
+ ApiResponse.build(response)
14
+ end
12
15
  end
13
16
 
14
17
  def by_uuid(uuid)
15
- response = self.class.get(base_url(uuid), headers: required_headers)
16
- ApiResponse.build(response)
18
+ retryable_request(@authorization) do |authorization|
19
+ response = self.class.get(base_url(uuid), headers: authorization.headers)
20
+ ApiResponse.build(response)
21
+ end
17
22
  end
18
23
 
19
24
  def create(payload)
20
- response = self.class.post(base_url, headers: required_headers, body: payload.to_json)
21
- ApiResponse.build(response)
25
+ retryable_request(@authorization) do |authorization|
26
+ response = self.class.post(base_url, headers: authorization.headers, body: payload.to_json)
27
+ ApiResponse.build(response)
28
+ end
22
29
  end
23
30
 
24
31
  def update(uuid, payload)
25
- response = self.class.put(base_url(uuid), headers: required_headers, body: payload.to_json)
26
- ApiResponse.build(response)
32
+ retryable_request(@authorization) do |authorization|
33
+ response = self.class.put(base_url(uuid), headers: authorization.headers, body: payload.to_json)
34
+ ApiResponse.build(response)
35
+ end
27
36
  end
28
37
 
29
38
  def delete(uuid)
30
- response = self.class.delete(base_url(uuid), headers: required_headers)
31
- return webhook_deleted_message unless response.body
32
- RDStation::ErrorHandler.new(response).raise_errors
39
+ retryable_request(@authorization) do |authorization|
40
+ response = self.class.delete(base_url(uuid), headers: authorization.headers)
41
+ return webhook_deleted_message unless response.body
42
+
43
+ RDStation::ErrorHandler.new(response).raise_error
44
+ end
33
45
  end
34
46
 
35
47
  private
@@ -41,9 +53,5 @@ module RDStation
41
53
  def base_url(path = '')
42
54
  "https://api.rd.services/integrations/webhooks/#{path}"
43
55
  end
44
-
45
- def required_headers
46
- { 'Authorization' => "Bearer #{@auth_token}", 'Content-Type' => 'application/json' }
47
- end
48
56
  end
49
57
  end