rdstation-ruby-client 1.2.0 → 2.3.0

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