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
@@ -4,7 +4,10 @@ require 'httparty'
4
4
  require 'rdstation/api_response'
5
5
 
6
6
  # API requests
7
+ require 'rdstation'
8
+ require 'rdstation/retryable_request'
7
9
  require 'rdstation/authentication'
10
+ require 'rdstation/authorization'
8
11
  require 'rdstation/client'
9
12
  require 'rdstation/contacts'
10
13
  require 'rdstation/events'
@@ -14,3 +17,6 @@ require 'rdstation/webhooks'
14
17
  # Error handling
15
18
  require 'rdstation/error'
16
19
  require 'rdstation/error_handler'
20
+
21
+ # Builders
22
+ require 'rdstation/builder/field'
@@ -0,0 +1,19 @@
1
+ module RDStation
2
+ class << self
3
+ attr_accessor :configuration
4
+
5
+ def configure
6
+ self.configuration ||= Configuration.new
7
+ yield(configuration)
8
+ end
9
+ end
10
+
11
+ class Configuration
12
+ attr_accessor :client_id, :client_secret
13
+ attr_reader :access_token_refresh_callback
14
+
15
+ def on_access_token_refresh(&block)
16
+ @access_token_refresh_callback = block
17
+ end
18
+ end
19
+ end
@@ -1,9 +1,9 @@
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 unless response_body['errors']
6
- RDStation::ErrorHandler.new(response).raise_errors
4
+ return JSON.parse(response.body) if response.code.between?(200, 299)
5
+
6
+ RDStation::ErrorHandler.new(response).raise_error
7
7
  end
8
8
  end
9
9
  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
@@ -0,0 +1,24 @@
1
+ module RDStation
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)
6
+ @access_token = access_token
7
+ @refresh_token = refresh_token
8
+ @access_token_expires_in = access_token_expires_in
9
+ validate_access_token access_token
10
+ end
11
+
12
+ def headers
13
+ { "Authorization" => "Bearer #{@access_token}", "Content-Type" => "application/json" }
14
+ end
15
+
16
+ private
17
+
18
+ def validate_access_token(access_token)
19
+ access_token_msg = ':access_token is required'
20
+ raise ArgumentError, access_token_msg unless access_token
21
+ end
22
+
23
+ end
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,86 +1,33 @@
1
- # encoding: utf-8
2
1
  module RDStation
3
- #
4
- # Mais informações em http://ajuda.rdstation.com.br/hc/pt-br/articles/204526429-Guia-de-integra%C3%A7%C3%B5es-com-o-RD-Station
5
- #
6
2
  class Client
7
- include HTTParty
8
-
9
- def initialize(rdstation_token, auth_token, identifier="integração")
10
- @identificador = identifier
11
- @rdstation_token = rdstation_token
12
- @auth_token = auth_token
13
- end
14
-
15
- #
16
- # A hash do Lead pode conter os seguintes parâmetros:
17
- # (obrigatório) :email
18
- # :identificador
19
- # :nome
20
- # :empresa
21
- # :cargo
22
- # :telefone
23
- # :celular
24
- # :website
25
- # :twitter
26
- # :c_utmz
27
- # :created_at
28
- # :tags
29
- #
30
- # Caso algum parâmetro não seja identificado como campo padrão ou como
31
- # campo personalizado, este parâmetro desconhecido será gravado nos
32
- # "Detalhes do Lead".
33
- #
34
- def create_lead(lead_hash)
35
- lead_hash = rdstation_token_hash.merge(lead_hash)
36
- lead_hash = lead_hash.merge(identifier_hash) unless lead_hash.has_key?(:identificador)
37
- post_with_body("/conversions", {:body => lead_hash})
38
- end
39
- alias_method :update_lead_info, :create_lead
40
-
41
- #
42
- # param lead:
43
- # id ou email do Lead a ser alterado
44
- #
45
- # param lead_hash:
46
- # Hash contendo:
47
- # :lifecycle_stage
48
- # 0 - Lead; 1 - Lead Qualificado; 2 - Cliente
49
- # :opportunity
50
- # true ou false
51
- #
52
- def change_lead(lead, lead_hash)
53
- lead_hash = auth_token_hash.merge({:lead => lead_hash})
54
- put_with_body("/leads/#{lead}", :body => lead_hash.to_json, :headers => {'Content-Type' => 'application/json'})
55
- end
56
-
57
- def change_lead_status(lead_hash)
58
- post_with_body("/services/#{@auth_token}/generic", :body => lead_hash )
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
+ )
59
9
  end
60
10
 
61
- private
62
- def base_url
63
- "https://www.rdstation.com.br/api/1.2"
11
+ def contacts
12
+ @contacts ||= RDStation::Contacts.new(authorization: @authorization)
64
13
  end
65
14
 
66
- def rdstation_token_hash
67
- { :token_rdstation => @rdstation_token }
15
+ def events
16
+ @events ||= RDStation::Events.new(authorization: @authorization)
68
17
  end
69
18
 
70
- def auth_token_hash
71
- { :auth_token => @auth_token }
19
+ def fields
20
+ @fields ||= RDStation::Fields.new(authorization: @authorization)
72
21
  end
73
22
 
74
- def identifier_hash
75
- { :identificador => @identificador }
23
+ def webhooks
24
+ @webhooks ||= RDStation::Webhooks.new(authorization: @authorization)
76
25
  end
77
26
 
78
- def post_with_body(path, opts)
79
- self.class.post("#{base_url}#{path}", opts)
80
- end
27
+ private
81
28
 
82
- def put_with_body(path, opts)
83
- self.class.put("#{base_url}#{path}", opts)
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. "
84
31
  end
85
32
  end
86
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
+ include ::RDStation::RetryableRequest
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
  #
@@ -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: required_headers)
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: required_headers)
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 => required_headers)
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,19 +54,17 @@ 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: required_headers)
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
-
61
- def required_headers
62
- { "Authorization" => "Bearer #{@auth_token}", "Content-Type" => "application/json" }
63
- end
64
69
  end
65
70
  end
@@ -1,5 +1,4 @@
1
1
  module RDStation
2
-
3
2
  class Error < StandardError
4
3
  attr_reader :details, :http_status, :headers, :body
5
4
 
@@ -8,23 +7,31 @@ module RDStation
8
7
  message = details['error_message']
9
8
  raise ArgumentError, 'The details hash must contain an error message' unless message
10
9
 
11
- # Those three arguments are kept only for compatibility reasons.
12
- # They aren't needed since we can get them directly in the details hash.
13
- # Consider removing them when update the major version.
14
- @http_status = details['http_status']
15
- @headers = details['headers']
16
- @body = details['body']
17
-
18
10
  super(message)
19
11
  end
20
12
 
21
- class ConflictingField < Error; end
22
- class Default < Error; end
23
- class ExpiredAccessToken < Error; end
24
- class ExpiredCodeGrant < Error; end
25
- class InvalidCredentials < Error; end
26
- class InvalidEventType < Error; end
27
- class ResourceNotFound < Error; end
13
+ class BadRequest < Error; end
28
14
  class Unauthorized < Error; end
15
+ class Forbidden < Error; end
16
+ class NotFound < Error; end
17
+ class MethodNotAllowed < Error; end
18
+ class NotAcceptable < Error; end
19
+ class Conflict < Error; end
20
+ class UnsupportedMediaType < Error; end
21
+ class UnprocessableEntity < Error; end
22
+ class InternalServerError < Error; end
23
+ class NotImplemented < Error; end
24
+ class BadGateway < Error; end
25
+ class ServiceUnavailable < Error; end
26
+ class ServerError < Error; end
27
+
28
+ # 400 - Bad Request
29
+ class ConflictingField < BadRequest; end
30
+ class InvalidEventType < BadRequest; end
31
+
32
+ # 401 - Unauthorized
33
+ class ExpiredAccessToken < Unauthorized; end
34
+ class ExpiredCodeGrant < Unauthorized; end
35
+ class InvalidCredentials < Unauthorized; end
29
36
  end
30
37
  end
@@ -1,9 +1,13 @@
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'
7
11
 
8
12
  def initialize(errors)
9
13
  @errors = errors
@@ -12,6 +16,9 @@ module RDStation
12
16
  def format
13
17
  return FLAT_HASH if flat_hash?
14
18
  return HASH_OF_ARRAYS if hash_of_arrays?
19
+ return HASH_OF_HASHES if hash_of_hashes?
20
+ return HASH_OF_MULTIPLE_TYPES if hash_of_multiple_types?
21
+
15
22
  ARRAY_OF_HASHES
16
23
  end
17
24
 
@@ -19,12 +26,23 @@ module RDStation
19
26
 
20
27
  def flat_hash?
21
28
  return unless @errors.is_a?(Hash)
29
+
22
30
  @errors.key?('error_type')
23
31
  end
24
32
 
25
33
  def hash_of_arrays?
26
34
  @errors.is_a?(Hash) && @errors.values.all? { |error| error.is_a? Array }
27
35
  end
36
+
37
+ def hash_of_hashes?
38
+ @errors.is_a?(Hash) && @errors.values.all? { |error| error.is_a? Hash }
39
+ end
40
+
41
+ def hash_of_multiple_types?
42
+ @errors.is_a?(Hash) &&
43
+ @errors.values.any? { |error| error.is_a? Hash } &&
44
+ @errors.values.any? { |error| error.is_a? Array }
45
+ end
28
46
  end
29
47
  end
30
48
  end