rdstation-ruby-client 1.2.0 → 2.3.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.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/rdsm-ruby-client-issue-template.md +49 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +163 -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 -70
  13. data/lib/rdstation/contacts.rb +21 -16
  14. data/lib/rdstation/error.rb +22 -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 +29 -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 +153 -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 +40 -13
  43. data/Gemfile.lock +0 -59
  44. data/lib/rdstation/error_handler/default.rb +0 -15
  45. data/lib/rdstation/error_handler/resource_not_found.rb +0 -24
  46. data/spec/lib/rdstation/error_handler/default_spec.rb +0 -14
  47. 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,44 @@
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
+ end
41
+ end
35
42
 
36
43
  def array_of_errors
37
44
  error_formatter.to_array.map do |error|
@@ -39,10 +46,6 @@ module RDStation
39
46
  end
40
47
  end
41
48
 
42
- def error_types
43
- ERROR_TYPES.map { |error_type| error_type.new(array_of_errors) }
44
- end
45
-
46
49
  def response_errors
47
50
  JSON.parse(response.body)
48
51
  end
@@ -55,7 +58,7 @@ module RDStation
55
58
  {
56
59
  'headers' => response.headers,
57
60
  'body' => JSON.parse(response.body),
58
- 'http_status' => response.code
61
+ 'http_status' => response.code,
59
62
  }
60
63
  end
61
64
  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.0'.freeze
2
+ VERSION = '2.3.0'.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