rdstation-ruby-client 2.0.0 → 2.4.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -4
  3. data/README.md +114 -22
  4. data/lib/rdstation-ruby-client.rb +6 -1
  5. data/lib/rdstation.rb +19 -0
  6. data/lib/rdstation/api_response.rb +1 -2
  7. data/lib/rdstation/authentication.rb +32 -3
  8. data/lib/rdstation/{authorization_header.rb → authorization.rb} +11 -8
  9. data/lib/rdstation/builder/field.rb +70 -0
  10. data/lib/rdstation/client.rb +17 -7
  11. data/lib/rdstation/contacts.rb +22 -13
  12. data/lib/rdstation/error.rb +2 -0
  13. data/lib/rdstation/error/format.rb +29 -3
  14. data/lib/rdstation/error/formatter.rb +69 -8
  15. data/lib/rdstation/error_handler.rb +6 -1
  16. data/lib/rdstation/events.rb +7 -12
  17. data/lib/rdstation/fields.rb +35 -6
  18. data/lib/rdstation/retryable_request.rb +35 -0
  19. data/lib/rdstation/version.rb +1 -1
  20. data/lib/rdstation/webhooks.rb +25 -13
  21. data/rdstation-ruby-client.gemspec +2 -1
  22. data/spec/lib/rdstation/api_response_spec.rb +34 -0
  23. data/spec/lib/rdstation/authentication_spec.rb +164 -0
  24. data/spec/lib/rdstation/{authorization_header_spec.rb → authorization_spec.rb} +3 -3
  25. data/spec/lib/rdstation/builder/field_spec.rb +69 -0
  26. data/spec/lib/rdstation/client_spec.rb +6 -6
  27. data/spec/lib/rdstation/contacts_spec.rb +23 -3
  28. data/spec/lib/rdstation/error/format_spec.rb +63 -0
  29. data/spec/lib/rdstation/error/formatter_spec.rb +113 -0
  30. data/spec/lib/rdstation/error_handler_spec.rb +23 -0
  31. data/spec/lib/rdstation/events_spec.rb +8 -3
  32. data/spec/lib/rdstation/fields_spec.rb +6 -1
  33. data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
  34. data/spec/lib/rdstation/webhooks_spec.rb +26 -1
  35. data/spec/lib/rdstation_spec.rb +18 -0
  36. metadata +36 -11
@@ -1,8 +1,7 @@
1
1
  module RDStation
2
2
  module ApiResponse
3
3
  def self.build(response)
4
- response_body = JSON.parse(response.body)
5
- return response_body if response.code.between?(200, 299)
4
+ return JSON.parse(response.body) if response.code.between?(200, 299)
6
5
 
7
6
  RDStation::ErrorHandler.new(response).raise_error
8
7
  end
@@ -5,10 +5,12 @@ module RDStation
5
5
 
6
6
  AUTH_TOKEN_URL = 'https://api.rd.services/auth/token'.freeze
7
7
  DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
8
+ REVOKE_URL = 'https://api.rd.services/auth/revoke'.freeze
8
9
 
9
- def initialize(client_id, client_secret)
10
- @client_id = client_id
11
- @client_secret = client_secret
10
+ def initialize(client_id = nil, client_secret = nil)
11
+ warn_deprecation if client_id || client_secret
12
+ @client_id = client_id || RDStation.configuration&.client_id
13
+ @client_secret = client_secret || RDStation.configuration&.client_secret
12
14
  end
13
15
 
14
16
  #
@@ -47,8 +49,31 @@ module RDStation
47
49
  ApiResponse.build(response)
48
50
  end
49
51
 
52
+ def self.revoke(access_token:)
53
+ response = self.post(
54
+ REVOKE_URL,
55
+ body: revoke_body(access_token),
56
+ headers: revoke_headers(access_token)
57
+ )
58
+ ApiResponse.build(response)
59
+ end
60
+
50
61
  private
51
62
 
63
+ def self.revoke_body(access_token)
64
+ URI.encode_www_form({
65
+ token: access_token,
66
+ token_type_hint: 'access_token'
67
+ })
68
+ end
69
+
70
+ def self.revoke_headers(access_token)
71
+ {
72
+ "Authorization" => "Bearer #{access_token}",
73
+ "Content-Type" => "application/x-www-form-urlencoded"
74
+ }
75
+ end
76
+
52
77
  def post_to_auth_endpoint(params)
53
78
  default_body = { client_id: @client_id, client_secret: @client_secret }
54
79
  body = default_body.merge(params)
@@ -59,5 +84,9 @@ module RDStation
59
84
  headers: DEFAULT_HEADERS
60
85
  )
61
86
  end
87
+
88
+ def warn_deprecation
89
+ warn "DEPRECATION WARNING: Providing client_id and client_secret directly to RDStation::Authentication.new is deprecated and will be removed in future versions. Use RDStation.configure instead."
90
+ end
62
91
  end
63
92
  end
@@ -1,21 +1,24 @@
1
1
  module RDStation
2
- class AuthorizationHeader
3
-
4
- def initialize(access_token:)
2
+ class Authorization
3
+ attr_reader :refresh_token
4
+ attr_accessor :access_token, :access_token_expires_in
5
+ def initialize(access_token:, refresh_token: nil, access_token_expires_in: nil)
5
6
  @access_token = access_token
7
+ @refresh_token = refresh_token
8
+ @access_token_expires_in = access_token_expires_in
6
9
  validate_access_token access_token
7
10
  end
8
-
9
- def to_h
11
+
12
+ def headers
10
13
  { "Authorization" => "Bearer #{@access_token}", "Content-Type" => "application/json" }
11
14
  end
12
-
15
+
13
16
  private
14
-
17
+
15
18
  def validate_access_token(access_token)
16
19
  access_token_msg = ':access_token is required'
17
20
  raise ArgumentError, access_token_msg unless access_token
18
21
  end
19
-
22
+
20
23
  end
21
24
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RDStation
4
+ class Builder
5
+ # More info: https://developers.rdstation.com/pt-BR/reference/fields#methodPostDetails
6
+ class Field
7
+ DATA_TYPES = %w(STRING INTEGER BOOLEAN STRING[]).freeze
8
+ PRESENTATION_TYPES = %w[TEXT_INPUT TEXT_AREA URL_INPUT PHONE_INPUT
9
+ EMAIL_INPUT CHECK_BOX NUMBER_INPUT COMBO_BOX
10
+ RADIO_BUTTON MULTIPLE_CHOICE].freeze
11
+
12
+ REQUIRED_FIELDS = %w[api_identifier data_type presentation_type label name].freeze
13
+
14
+ def initialize(api_identifier)
15
+ raise 'api_identifier required' unless api_identifier
16
+ unless valid_identifier(api_identifier)
17
+ raise 'api_identifier is not in a valid format, need start with "cf_"'
18
+ end
19
+
20
+ @values = {}
21
+ @values['api_identifier'] = api_identifier
22
+ end
23
+
24
+ def data_type(data_type)
25
+ raise "Not valid data_type - #{DATA_TYPES}" unless DATA_TYPES.include? data_type
26
+
27
+ @values['data_type'] = data_type
28
+ end
29
+
30
+ def presentation_type(presentation_type)
31
+ unless PRESENTATION_TYPES.include? presentation_type
32
+ raise "Not valid presentation_type - #{PRESENTATION_TYPES}"
33
+ end
34
+
35
+ @values['presentation_type'] = presentation_type
36
+ end
37
+
38
+ def label(language, label)
39
+ @values['label'] = { language.to_s => label }
40
+ end
41
+
42
+ def name(language, name)
43
+ @values['name'] = { language.to_s => name }
44
+ end
45
+
46
+ def validation_rules(validation_rules)
47
+ @values['validation_rules'] = validation_rules
48
+ end
49
+
50
+ def valid_options(valid_options)
51
+ @values['valid_options'] = valid_options
52
+ end
53
+
54
+ def build
55
+ empty_fields = REQUIRED_FIELDS.select { |field| @values[field].nil? }
56
+ unless empty_fields.empty?
57
+ raise "Required fields are missing - #{empty_fields}"
58
+ end
59
+
60
+ @values
61
+ end
62
+
63
+ private
64
+
65
+ def valid_identifier(api_identifier)
66
+ api_identifier.start_with? 'cf_'
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,23 +1,33 @@
1
1
  module RDStation
2
2
  class Client
3
- def initialize(access_token:)
4
- @authorization_header = AuthorizationHeader.new(access_token: access_token)
3
+ def initialize(access_token:, refresh_token: nil)
4
+ warn_deprecation unless refresh_token
5
+ @authorization = Authorization.new(
6
+ access_token: access_token,
7
+ refresh_token: refresh_token
8
+ )
5
9
  end
6
-
10
+
7
11
  def contacts
8
- @contacts ||= RDStation::Contacts.new(authorization_header: @authorization_header)
12
+ @contacts ||= RDStation::Contacts.new(authorization: @authorization)
9
13
  end
10
14
 
11
15
  def events
12
- @events ||= RDStation::Events.new(authorization_header: @authorization_header)
16
+ @events ||= RDStation::Events.new(authorization: @authorization)
13
17
  end
14
18
 
15
19
  def fields
16
- @fields ||= RDStation::Fields.new(authorization_header: @authorization_header)
20
+ @fields ||= RDStation::Fields.new(authorization: @authorization)
17
21
  end
18
22
 
19
23
  def webhooks
20
- @webhooks ||= RDStation::Webhooks.new(authorization_header: @authorization_header)
24
+ @webhooks ||= RDStation::Webhooks.new(authorization: @authorization)
25
+ end
26
+
27
+ private
28
+
29
+ def warn_deprecation
30
+ warn "DEPRECATION WARNING: Specifying refresh_token in RDStation::Client.new(access_token: 'at', refresh_token: 'rt') is optional right now, but will be mandatory in future versions. "
21
31
  end
22
32
  end
23
33
  end
@@ -3,9 +3,10 @@ module RDStation
3
3
  # More info: https://developers.rdstation.com/pt-BR/reference/contacts
4
4
  class Contacts
5
5
  include HTTParty
6
-
7
- def initialize(authorization_header:)
8
- @authorization_header = authorization_header
6
+ include ::RDStation::RetryableRequest
7
+
8
+ def initialize(authorization:)
9
+ @authorization = authorization
9
10
  end
10
11
 
11
12
  #
@@ -13,13 +14,17 @@ module RDStation
13
14
  # The unique uuid associated to each RD Station Contact.
14
15
  #
15
16
  def by_uuid(uuid)
16
- response = self.class.get(base_url(uuid), headers: @authorization_header.to_h)
17
- ApiResponse.build(response)
17
+ retryable_request(@authorization) do |authorization|
18
+ response = self.class.get(base_url(uuid), headers: authorization.headers)
19
+ ApiResponse.build(response)
20
+ end
18
21
  end
19
22
 
20
23
  def by_email(email)
21
- response = self.class.get(base_url("email:#{email}"), headers: @authorization_header.to_h)
22
- ApiResponse.build(response)
24
+ retryable_request(@authorization) do |authorization|
25
+ response = self.class.get(base_url("email:#{email}"), headers: authorization.headers)
26
+ ApiResponse.build(response)
27
+ end
23
28
  end
24
29
 
25
30
  # The Contact hash may contain the following parameters:
@@ -34,8 +39,10 @@ module RDStation
34
39
  # :website
35
40
  # :tags
36
41
  def update(uuid, contact_hash)
37
- response = self.class.patch(base_url(uuid), :body => contact_hash.to_json, :headers => @authorization_header.to_h)
38
- ApiResponse.build(response)
42
+ retryable_request(@authorization) do |authorization|
43
+ response = self.class.patch(base_url(uuid), :body => contact_hash.to_json, :headers => authorization.headers)
44
+ ApiResponse.build(response)
45
+ end
39
46
  end
40
47
 
41
48
  #
@@ -47,14 +54,16 @@ module RDStation
47
54
  # Contact data
48
55
  #
49
56
  def upsert(identifier, identifier_value, contact_hash)
50
- path = "#{identifier}:#{identifier_value}"
51
- response = self.class.patch(base_url(path), body: contact_hash.to_json, headers: @authorization_header.to_h)
52
- ApiResponse.build(response)
57
+ retryable_request(@authorization) do |authorization|
58
+ path = "#{identifier}:#{identifier_value}"
59
+ response = self.class.patch(base_url(path), body: contact_hash.to_json, headers: authorization.headers)
60
+ ApiResponse.build(response)
61
+ end
53
62
  end
54
63
 
55
64
  private
56
65
 
57
- def base_url(path = "")
66
+ def base_url(path = '')
58
67
  "https://api.rd.services/platform/contacts/#{path}"
59
68
  end
60
69
  end
@@ -19,11 +19,13 @@ module RDStation
19
19
  class Conflict < Error; end
20
20
  class UnsupportedMediaType < Error; end
21
21
  class UnprocessableEntity < Error; end
22
+ class TooManyRequests < Error; end
22
23
  class InternalServerError < Error; end
23
24
  class NotImplemented < Error; end
24
25
  class BadGateway < Error; end
25
26
  class ServiceUnavailable < Error; end
26
27
  class ServerError < Error; end
28
+ class UnknownError < Error; end
27
29
 
28
30
  # 400 - Bad Request
29
31
  class ConflictingField < BadRequest; end
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RDStation
2
4
  class Error
3
5
  class Format
4
- FLAT_HASH = 'FLAT_HASH'.freeze
5
- HASH_OF_ARRAYS = 'HASH_OF_ARRAYS'.freeze
6
- ARRAY_OF_HASHES = 'ARRAY_OF_HASHES'.freeze
6
+ FLAT_HASH = 'FLAT_HASH'
7
+ HASH_OF_ARRAYS = 'HASH_OF_ARRAYS'
8
+ ARRAY_OF_HASHES = 'ARRAY_OF_HASHES'
9
+ HASH_OF_MULTIPLE_TYPES = 'HASH_OF_MULTIPLE_TYPES'
10
+ HASH_OF_HASHES = 'HASH_OF_HASHES'
11
+ SINGLE_HASH = 'SINGLE_HASH'
7
12
 
8
13
  def initialize(errors)
9
14
  @errors = errors
@@ -11,20 +16,41 @@ module RDStation
11
16
 
12
17
  def format
13
18
  return FLAT_HASH if flat_hash?
19
+ return SINGLE_HASH if single_hash?
14
20
  return HASH_OF_ARRAYS if hash_of_arrays?
21
+ return HASH_OF_HASHES if hash_of_hashes?
22
+ return HASH_OF_MULTIPLE_TYPES if hash_of_multiple_types?
23
+
15
24
  ARRAY_OF_HASHES
16
25
  end
17
26
 
18
27
  private
19
28
 
29
+ def single_hash?
30
+ return unless @errors.is_a?(Hash)
31
+
32
+ @errors.key?('error')
33
+ end
34
+
20
35
  def flat_hash?
21
36
  return unless @errors.is_a?(Hash)
37
+
22
38
  @errors.key?('error_type')
23
39
  end
24
40
 
25
41
  def hash_of_arrays?
26
42
  @errors.is_a?(Hash) && @errors.values.all? { |error| error.is_a? Array }
27
43
  end
44
+
45
+ def hash_of_hashes?
46
+ @errors.is_a?(Hash) && @errors.values.all? { |error| error.is_a? Hash }
47
+ end
48
+
49
+ def hash_of_multiple_types?
50
+ @errors.is_a?(Hash) &&
51
+ @errors.values.any? { |error| error.is_a? Hash } &&
52
+ @errors.values.any? { |error| error.is_a? Array }
53
+ end
28
54
  end
29
55
  end
30
56
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './format'
2
4
 
3
5
  module RDStation
@@ -11,27 +13,61 @@ module RDStation
11
13
  return @error_response unless @error_response.is_a?(Hash)
12
14
 
13
15
  case error_format.format
16
+ when RDStation::Error::Format::SINGLE_HASH
17
+ return from_single_hash
14
18
  when RDStation::Error::Format::FLAT_HASH
15
19
  return from_flat_hash
16
20
  when RDStation::Error::Format::HASH_OF_ARRAYS
17
21
  return from_hash_of_arrays
22
+ when RDStation::Error::Format::HASH_OF_HASHES
23
+ return from_hash_of_hashes
24
+ when RDStation::Error::Format::HASH_OF_MULTIPLE_TYPES
25
+ return from_hash_of_multiple_types
18
26
  end
19
27
 
20
28
  errors
21
29
  end
22
30
 
31
+ def from_single_hash
32
+ error_hash = @error_response.dup
33
+ error_message = error_hash.delete('error')
34
+
35
+ [
36
+ {
37
+ 'error_type' => 'TOO_MANY_REQUESTS',
38
+ 'error_message' => error_message,
39
+ 'details' => error_hash
40
+ }
41
+ ]
42
+ end
43
+
23
44
  def from_flat_hash
24
45
  [errors]
25
46
  end
26
47
 
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)
48
+ def from_hash_of_multiple_types
49
+ array_of_errors = []
50
+ errors.each do |attribute_name, errors|
51
+ if errors.is_a? Array
52
+ result = build_error_from_array(attribute_name, errors)
53
+ end
54
+ if errors.is_a? Hash
55
+ result = build_error_from_multilingual_hash(attribute_name, errors)
56
+ end
57
+ array_of_errors.push(*result)
34
58
  end
59
+
60
+ array_of_errors
61
+ end
62
+
63
+ def from_hash_of_hashes
64
+ array_of_errors = []
65
+ errors.each do |attribute_name, errors|
66
+ result = build_error_from_multilingual_hash(attribute_name, errors)
67
+ array_of_errors.push(*result)
68
+ end
69
+
70
+ array_of_errors
35
71
  end
36
72
 
37
73
  def error_format
@@ -39,7 +75,32 @@ module RDStation
39
75
  end
40
76
 
41
77
  def errors
42
- @errors ||= @error_response['errors']
78
+ @errors ||= @error_response.fetch('errors', @error_response)
79
+ end
80
+
81
+ private
82
+
83
+ def build_error_from_array(attribute_name, attribute_errors)
84
+ path = { 'path' => "body.#{attribute_name}" }
85
+ attribute_errors.map { |error| error.merge(path) }
86
+ end
87
+
88
+ def build_error_from_multilingual_hash(attribute_name, errors_by_language)
89
+ array_of_errors = []
90
+ errors_by_language.each do |language, errors|
91
+ result = build_error_from_array("#{attribute_name}.#{language}", errors)
92
+ array_of_errors.push(*result)
93
+ end
94
+ array_of_errors
95
+ end
96
+
97
+ def from_hash_of_arrays
98
+ errors.each_with_object([]) do |errors, array_of_errors|
99
+ attribute_name = errors.first
100
+ attribute_errors = errors.last
101
+ errors = build_error_from_array(attribute_name, attribute_errors)
102
+ array_of_errors.push(*errors)
103
+ end
43
104
  end
44
105
  end
45
106
  end