rdstation-ruby-client 2.0.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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