rdstation-ruby-client 2.1.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +121 -1
- data/README.md +106 -22
- data/Rakefile +4 -0
- data/lib/rdstation-ruby-client.rb +6 -1
- data/lib/rdstation.rb +19 -0
- data/lib/rdstation/api_response.rb +1 -2
- data/lib/rdstation/authentication.rb +8 -3
- data/lib/rdstation/{authorization_header.rb → authorization.rb} +11 -8
- data/lib/rdstation/builder/field.rb +70 -0
- data/lib/rdstation/client.rb +17 -7
- data/lib/rdstation/contacts.rb +22 -13
- data/lib/rdstation/error.rb +3 -0
- data/lib/rdstation/error/format.rb +29 -3
- data/lib/rdstation/error/formatter.rb +69 -8
- data/lib/rdstation/error_handler.rb +6 -1
- data/lib/rdstation/error_handler/invalid_refresh_token.rb +24 -0
- data/lib/rdstation/error_handler/unauthorized.rb +2 -0
- data/lib/rdstation/events.rb +7 -12
- data/lib/rdstation/fields.rb +35 -6
- data/lib/rdstation/retryable_request.rb +35 -0
- data/lib/rdstation/version.rb +1 -1
- data/lib/rdstation/webhooks.rb +25 -13
- data/rdstation-ruby-client.gemspec +2 -1
- data/spec/lib/rdstation/api_response_spec.rb +34 -0
- data/spec/lib/rdstation/authentication_spec.rb +105 -2
- data/spec/lib/rdstation/{authorization_header_spec.rb → authorization_spec.rb} +3 -3
- data/spec/lib/rdstation/builder/field_spec.rb +69 -0
- data/spec/lib/rdstation/client_spec.rb +6 -6
- data/spec/lib/rdstation/contacts_spec.rb +23 -3
- data/spec/lib/rdstation/error/format_spec.rb +63 -0
- data/spec/lib/rdstation/error/formatter_spec.rb +113 -0
- data/spec/lib/rdstation/error_handler/invalid_refresh_token_spec.rb +53 -0
- data/spec/lib/rdstation/error_handler_spec.rb +23 -0
- data/spec/lib/rdstation/events_spec.rb +8 -3
- data/spec/lib/rdstation/fields_spec.rb +6 -1
- data/spec/lib/rdstation/retryable_request_spec.rb +142 -0
- data/spec/lib/rdstation/webhooks_spec.rb +26 -1
- data/spec/lib/rdstation_spec.rb +18 -0
- metadata +36 -8
@@ -1,8 +1,7 @@
|
|
1
1
|
module RDStation
|
2
2
|
module ApiResponse
|
3
3
|
def self.build(response)
|
4
|
-
|
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
|
@@ -7,9 +7,10 @@ module RDStation
|
|
7
7
|
DEFAULT_HEADERS = { 'Content-Type' => 'application/json' }.freeze
|
8
8
|
REVOKE_URL = 'https://api.rd.services/auth/revoke'.freeze
|
9
9
|
|
10
|
-
def initialize(client_id, client_secret)
|
11
|
-
|
12
|
-
@
|
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
|
13
14
|
end
|
14
15
|
|
15
16
|
#
|
@@ -83,5 +84,9 @@ module RDStation
|
|
83
84
|
headers: DEFAULT_HEADERS
|
84
85
|
)
|
85
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
|
86
91
|
end
|
87
92
|
end
|
@@ -1,21 +1,24 @@
|
|
1
1
|
module RDStation
|
2
|
-
class
|
3
|
-
|
4
|
-
|
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
|
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
|
data/lib/rdstation/client.rb
CHANGED
@@ -1,23 +1,33 @@
|
|
1
1
|
module RDStation
|
2
2
|
class Client
|
3
|
-
def initialize(access_token:)
|
4
|
-
|
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(
|
12
|
+
@contacts ||= RDStation::Contacts.new(authorization: @authorization)
|
9
13
|
end
|
10
14
|
|
11
15
|
def events
|
12
|
-
@events ||= RDStation::Events.new(
|
16
|
+
@events ||= RDStation::Events.new(authorization: @authorization)
|
13
17
|
end
|
14
18
|
|
15
19
|
def fields
|
16
|
-
@fields ||= RDStation::Fields.new(
|
20
|
+
@fields ||= RDStation::Fields.new(authorization: @authorization)
|
17
21
|
end
|
18
22
|
|
19
23
|
def webhooks
|
20
|
-
@webhooks ||= RDStation::Webhooks.new(
|
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
|
data/lib/rdstation/contacts.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
data/lib/rdstation/error.rb
CHANGED
@@ -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
|
@@ -33,5 +35,6 @@ module RDStation
|
|
33
35
|
class ExpiredAccessToken < Unauthorized; end
|
34
36
|
class ExpiredCodeGrant < Unauthorized; end
|
35
37
|
class InvalidCredentials < Unauthorized; end
|
38
|
+
class InvalidRefreshToken < Unauthorized; end
|
36
39
|
end
|
37
40
|
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'
|
5
|
-
HASH_OF_ARRAYS = 'HASH_OF_ARRAYS'
|
6
|
-
ARRAY_OF_HASHES = 'ARRAY_OF_HASHES'
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
@@ -13,6 +13,8 @@ module RDStation
|
|
13
13
|
raise error_class, array_of_errors.first if error_class < RDStation::Error
|
14
14
|
|
15
15
|
error_class.new(array_of_errors).raise_error
|
16
|
+
rescue JSON::ParserError => error
|
17
|
+
raise error_class, { 'error_message' => response.body }
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
@@ -30,11 +32,14 @@ module RDStation
|
|
30
32
|
when 409 then RDStation::Error::Conflict
|
31
33
|
when 415 then RDStation::Error::UnsupportedMediaType
|
32
34
|
when 422 then RDStation::Error::UnprocessableEntity
|
35
|
+
when 429 then RDStation::Error::TooManyRequests
|
33
36
|
when 500 then RDStation::Error::InternalServerError
|
34
37
|
when 501 then RDStation::Error::NotImplemented
|
35
38
|
when 502 then RDStation::Error::BadGateway
|
36
39
|
when 503 then RDStation::Error::ServiceUnavailable
|
37
40
|
when 500..599 then RDStation::Error::ServerError
|
41
|
+
else
|
42
|
+
RDStation::Error::UnknownError
|
38
43
|
end
|
39
44
|
end
|
40
45
|
|
@@ -53,7 +58,7 @@ module RDStation
|
|
53
58
|
end
|
54
59
|
|
55
60
|
def additional_error_attributes
|
56
|
-
{
|
61
|
+
attrs = {
|
57
62
|
'headers' => response.headers,
|
58
63
|
'body' => JSON.parse(response.body),
|
59
64
|
'http_status' => response.code,
|