ruby_hubspot_api 0.1.2.1 → 0.2.1.pre.1
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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +47 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +36 -1
- data/Gemfile.lock +25 -20
- data/README.md +316 -24
- data/lib/hubspot/api_client.rb +42 -8
- data/lib/hubspot/batch.rb +252 -0
- data/lib/hubspot/config.rb +7 -6
- data/lib/hubspot/paged_batch.rb +56 -0
- data/lib/hubspot/paged_collection.rb +3 -18
- data/lib/hubspot/property.rb +4 -0
- data/lib/hubspot/resource.rb +36 -7
- data/lib/hubspot/version.rb +1 -1
- data/lib/hubspot.rb +15 -0
- data/lib/ruby_hubspot_api.rb +3 -0
- data/lib/support/patches.rb +38 -0
- data/ruby_hubspot_api.gemspec +5 -3
- metadata +46 -14
- data/.ruby-version +0 -1
data/lib/hubspot/api_client.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# HubSpot API Client
|
4
|
+
# Handles all HTTP interactions with the HubSpot API. It manages GET, POST, PATCH, and DELETE requests.
|
3
5
|
module Hubspot
|
4
6
|
# All interations with the Hubspot API happen here...
|
5
7
|
class ApiClient
|
8
|
+
MAX_RETRIES = 3
|
9
|
+
RETRY_WAIT_TIME = 1 # seconds
|
10
|
+
|
6
11
|
include HTTParty
|
7
12
|
base_uri 'https://api.hubapi.com'
|
8
13
|
|
@@ -32,7 +37,7 @@ module Hubspot
|
|
32
37
|
ensure_configuration!
|
33
38
|
start_time = Time.now
|
34
39
|
response = super(url, options)
|
35
|
-
log_request(:post, url, response, start_time)
|
40
|
+
log_request(:post, url, response, start_time, options)
|
36
41
|
response
|
37
42
|
end
|
38
43
|
|
@@ -40,7 +45,7 @@ module Hubspot
|
|
40
45
|
ensure_configuration!
|
41
46
|
start_time = Time.now
|
42
47
|
response = super(url, options)
|
43
|
-
log_request(:patch, url, response, start_time)
|
48
|
+
log_request(:patch, url, response, start_time, options)
|
44
49
|
response
|
45
50
|
end
|
46
51
|
|
@@ -52,23 +57,52 @@ module Hubspot
|
|
52
57
|
response
|
53
58
|
end
|
54
59
|
|
55
|
-
def log_request(http_method, url, response, start_time)
|
60
|
+
def log_request(http_method, url, response, start_time, extra = nil)
|
56
61
|
d = Time.now - start_time
|
57
62
|
Hubspot.logger.info("#{http_method.to_s.upcase} #{url} took #{d.round(2)}s with status #{response.code}")
|
58
|
-
|
63
|
+
return unless Hubspot.logger.debug?
|
64
|
+
|
65
|
+
Hubspot.logger.debug("Request body: #{extra}") if extra
|
66
|
+
Hubspot.logger.debug("Response body: #{response.body}")
|
59
67
|
end
|
60
68
|
|
61
|
-
def handle_response(response)
|
62
|
-
|
69
|
+
def handle_response(response, retries = 0)
|
70
|
+
case response.code
|
71
|
+
when 200..299
|
63
72
|
response.parsed_response
|
73
|
+
when 429
|
74
|
+
handle_rate_limit(response, retries)
|
64
75
|
else
|
65
|
-
|
66
|
-
raise Hubspot.error_from_response(response)
|
76
|
+
log_and_raise_error(response)
|
67
77
|
end
|
68
78
|
end
|
69
79
|
|
70
80
|
private
|
71
81
|
|
82
|
+
def handle_rate_limit(response, retries)
|
83
|
+
if retries < MAX_RETRIES
|
84
|
+
retry_after = response.headers['Retry-After']&.to_i || RETRY_WAIT_TIME
|
85
|
+
Hubspot.logger.warn("Rate limit hit. Retrying in #{retry_after} seconds...")
|
86
|
+
sleep(retry_after)
|
87
|
+
retry_request(response.request, retries + 1)
|
88
|
+
else
|
89
|
+
Hubspot.logger.error('Exceeded maximum retries for rate-limited request.')
|
90
|
+
raise Hubspot.error_from_response(response)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def retry_request(request, retries)
|
95
|
+
# Re-issues the original request using the retry logic
|
96
|
+
http_method = request.http_method::METHOD.downcase # Use the METHOD constant to get the method string
|
97
|
+
response = HTTParty.send(http_method, request.uri, request.options)
|
98
|
+
handle_response(response, retries)
|
99
|
+
end
|
100
|
+
|
101
|
+
def log_and_raise_error(response)
|
102
|
+
Hubspot.logger.error("API Error: #{response.code} - #{response.body}")
|
103
|
+
raise Hubspot.error_from_response(response)
|
104
|
+
end
|
105
|
+
|
72
106
|
def ensure_configuration!
|
73
107
|
raise NotConfiguredError, 'Hubspot API not configured' unless Hubspot.configured?
|
74
108
|
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Hubspot
|
6
|
+
# exactly the same as a parsed_response but with the status code preserved
|
7
|
+
class BatchResponse < SimpleDelegator
|
8
|
+
attr_reader :status_code
|
9
|
+
|
10
|
+
def initialize(status_code, parsed_response)
|
11
|
+
@status_code = status_code
|
12
|
+
super(parsed_response) # Delegate to the parsed response object
|
13
|
+
end
|
14
|
+
|
15
|
+
# Check if all responses were successful (status 200)
|
16
|
+
def all_successful?
|
17
|
+
@status_code == 200
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if some responses succeeded and some failed (status 207)
|
21
|
+
def partial_success?
|
22
|
+
@status_code == 207
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Class to handle batch updates of resources
|
27
|
+
# rubocop:disable Metrics/ClassLength
|
28
|
+
class Batch < ApiClient
|
29
|
+
attr_accessor :id_property, :resources, :responses
|
30
|
+
|
31
|
+
CONTACT_LIMIT = 10
|
32
|
+
DEFAULT_LIMIT = 100
|
33
|
+
|
34
|
+
# rubocop:disable Lint/MissingSuper
|
35
|
+
def initialize(resources = [], id_property: 'id')
|
36
|
+
@resources = []
|
37
|
+
@id_property = id_property # Set id_property for the batch (default: 'id')
|
38
|
+
@responses = [] # Store multiple BatchResponse objects here
|
39
|
+
resources.each { |resource| add_resource(resource) }
|
40
|
+
end
|
41
|
+
# rubocop:enable Lint/MissingSuper
|
42
|
+
|
43
|
+
# batch create from the resources
|
44
|
+
def create
|
45
|
+
save(action: 'create')
|
46
|
+
end
|
47
|
+
|
48
|
+
def update
|
49
|
+
# validate_update_conditions
|
50
|
+
save(action: 'update')
|
51
|
+
end
|
52
|
+
|
53
|
+
# Upsert method that calls save with upsert action
|
54
|
+
def upsert
|
55
|
+
validate_upsert_conditions
|
56
|
+
save(action: 'upsert')
|
57
|
+
end
|
58
|
+
|
59
|
+
# Archive method
|
60
|
+
def archive
|
61
|
+
save(action: 'archive')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if all responses were successful
|
65
|
+
def all_successful?
|
66
|
+
@responses.all?(&:all_successful?)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if some responses were successful and others failed
|
70
|
+
def partial_success?
|
71
|
+
@responses.any?(&:partial_success?) && @responses.none?(&:all_successful?)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Check if any responses failed
|
75
|
+
def any_failed?
|
76
|
+
@responses.any? { |response| !response.all_successful? && !response.partial_success? }
|
77
|
+
end
|
78
|
+
|
79
|
+
def add_resource(resource)
|
80
|
+
if @resources.any? && @resources.first.resource_name != resource.resource_name
|
81
|
+
raise ArgumentError, 'All resources in a batch must be of the same type'
|
82
|
+
end
|
83
|
+
|
84
|
+
@resources << resource
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# rubocop:disable Metrics/MethodLength
|
90
|
+
def save(action: 'update')
|
91
|
+
@action = action
|
92
|
+
resource_type = check_single_resource_type
|
93
|
+
inputs = gather_inputs
|
94
|
+
|
95
|
+
return false if inputs.empty? # Guard clause
|
96
|
+
|
97
|
+
# Perform the batch updates in chunks based on the resource type's limit
|
98
|
+
batch_limit = batch_size_limit(resource_type)
|
99
|
+
inputs.each_slice(batch_limit) do |batch_inputs|
|
100
|
+
response = batch_request(resource_type, batch_inputs, action)
|
101
|
+
@responses << response
|
102
|
+
end
|
103
|
+
|
104
|
+
process_responses unless @action == 'archive'
|
105
|
+
|
106
|
+
# Check if any responses failed
|
107
|
+
!any_failed?
|
108
|
+
end
|
109
|
+
# rubocop:enable Metrics/MethodLength
|
110
|
+
|
111
|
+
def check_single_resource_type
|
112
|
+
raise 'Batch is empty' if @resources.empty?
|
113
|
+
|
114
|
+
@resources.first.resource_name
|
115
|
+
end
|
116
|
+
|
117
|
+
# Gather all the changes, ensuring each resource has an id and changes
|
118
|
+
def gather_inputs
|
119
|
+
return gather_archive_inputs if @action == 'archive'
|
120
|
+
|
121
|
+
@resources.map do |resource|
|
122
|
+
next if resource.changes.empty?
|
123
|
+
|
124
|
+
{
|
125
|
+
id: resource.public_send(@id_property), # Dynamically get the ID based on the batch's id_property
|
126
|
+
idProperty: determine_id_property, # Use the helper method to decide whether to include idProperty
|
127
|
+
properties: resource.changes # Gather the changes for the resource
|
128
|
+
}.compact # Removes nil keys
|
129
|
+
end.compact # Removes nil entries
|
130
|
+
end
|
131
|
+
|
132
|
+
# Gather inputs for the archive operation
|
133
|
+
def gather_archive_inputs
|
134
|
+
@resources.map do |resource|
|
135
|
+
{
|
136
|
+
id: resource.public_send(@id_property), # Use the ID or the custom property
|
137
|
+
idProperty: determine_id_property # Include idProperty if it's not "id"
|
138
|
+
}.compact
|
139
|
+
end.compact
|
140
|
+
end
|
141
|
+
|
142
|
+
# Only include idProperty if it's not "id"
|
143
|
+
def determine_id_property
|
144
|
+
@id_property == 'id' ? nil : @id_property
|
145
|
+
end
|
146
|
+
|
147
|
+
# Perform batch request based on the provided action (upsert, update, create, or archive)
|
148
|
+
def batch_request(type, inputs, action)
|
149
|
+
response = self.class.post("/crm/v3/objects/#{type}/batch/#{action}", body: { inputs: inputs }.to_json)
|
150
|
+
BatchResponse.new(response.code, handle_response(response))
|
151
|
+
end
|
152
|
+
|
153
|
+
# Validation for upsert conditions
|
154
|
+
def validate_upsert_conditions
|
155
|
+
raise ArgumentError, "id_property cannot be 'id' for upsert" if @id_property == 'id'
|
156
|
+
|
157
|
+
# check if there are any resources without a value from the id_property
|
158
|
+
return unless @resources.any? { |resource| resource.public_send(id_property).blank? }
|
159
|
+
|
160
|
+
raise ArgumentError, "All resources must have a non-blank value for #{@id_property} to perform upsert"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Return the appropriate batch size limit for the resource type
|
164
|
+
def batch_size_limit(resource_type)
|
165
|
+
resource_type == 'contacts' ? CONTACT_LIMIT : DEFAULT_LIMIT
|
166
|
+
end
|
167
|
+
|
168
|
+
# Process responses from the batch API call
|
169
|
+
def process_responses
|
170
|
+
@responses.each do |response|
|
171
|
+
next unless response['results']
|
172
|
+
|
173
|
+
process_results(response['results'])
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Process each result and update the resource accordingly
|
178
|
+
def process_results(results)
|
179
|
+
results.each do |result|
|
180
|
+
resource = find_resource_from_result(result)
|
181
|
+
next unless resource
|
182
|
+
|
183
|
+
# Set the ID on the resource directly
|
184
|
+
resource.id = result['id'].to_i if result['id']
|
185
|
+
|
186
|
+
# Update the resource properties
|
187
|
+
update_resource_properties(resource, result['properties'])
|
188
|
+
|
189
|
+
# Update metadata like updatedAt
|
190
|
+
update_metadata(resource, result['updatedAt'])
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def find_resource_from_result(result)
|
195
|
+
case @action
|
196
|
+
when 'update', 'upsert'
|
197
|
+
find_resource_from_id(result['id'].to_i)
|
198
|
+
|
199
|
+
#
|
200
|
+
# when specifying idProperty in the upsert request
|
201
|
+
# the Hubspot API returns the id value (aka the hs_object_id)
|
202
|
+
# instead of the value of the <idProperty> field, so the value of the field
|
203
|
+
# is only stored in results['properties'][@id_property] if it changed!
|
204
|
+
# so this condition is redundant but left here in case Hubspot updates the response
|
205
|
+
#
|
206
|
+
# when 'upsert'
|
207
|
+
# resource_id = result.dig('properties', @id_property) || result['id']
|
208
|
+
# find_resource_from_id_property(resource_id)
|
209
|
+
#
|
210
|
+
when 'create'
|
211
|
+
# For create, check if the resource's changes are entirely contained in the result's properties
|
212
|
+
@resources.reject(&:persisted?).find do |resource|
|
213
|
+
resource.changes.any? && resource.changes.all? { |key, value| result['properties'][key.to_s] == value }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def find_resource_from_id(resource_id)
|
219
|
+
@resources.find { |r| r.id == resource_id }
|
220
|
+
end
|
221
|
+
|
222
|
+
# def find_resource_from_id_property(resource_id)
|
223
|
+
# @resources.find { |r| r.public_send(@id_property) == resource_id }
|
224
|
+
# end
|
225
|
+
|
226
|
+
def update_resource_properties(resource, properties)
|
227
|
+
properties.each do |key, value|
|
228
|
+
if resource.changes[key]
|
229
|
+
resource.properties[key] = value
|
230
|
+
resource.changes.delete(key)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def update_metadata(resource, updated_at)
|
236
|
+
resource.metadata['updatedAt'] = updated_at if updated_at
|
237
|
+
end
|
238
|
+
|
239
|
+
class << self
|
240
|
+
def read(object_class, object_ids = [], id_property: 'id')
|
241
|
+
raise ArgumentError, 'Must be a valid Hubspot resource class' unless object_class < Hubspot::Resource
|
242
|
+
|
243
|
+
# fetch all the matching resources with paging handled
|
244
|
+
resources = object_class.batch_read(object_ids, id_property: id_property).all
|
245
|
+
|
246
|
+
# return instance of Hubspot::Batch with the resources set
|
247
|
+
new(resources, id_property: id_property)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
# rubocop:enable Metrics/ClassLength
|
252
|
+
end
|
data/lib/hubspot/config.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
module Hubspot
|
4
4
|
# To hold Hubspot configuration
|
5
5
|
class Config
|
6
|
-
attr_accessor :access_token, :portal_id, :client_secret, :logger, :log_level
|
6
|
+
attr_accessor :access_token, :portal_id, :client_secret, :logger, :log_level,
|
7
|
+
:timeout, :open_timeout, :read_timeout, :write_timeout
|
7
8
|
|
8
9
|
def initialize
|
9
10
|
@access_token = nil
|
@@ -14,6 +15,11 @@ module Hubspot
|
|
14
15
|
apply_log_level
|
15
16
|
end
|
16
17
|
|
18
|
+
# Apply the log level to the logger
|
19
|
+
def apply_log_level
|
20
|
+
@logger.level = @log_level
|
21
|
+
end
|
22
|
+
|
17
23
|
private
|
18
24
|
|
19
25
|
# Initialize the default logger
|
@@ -42,11 +48,6 @@ module Hubspot
|
|
42
48
|
end
|
43
49
|
# rubocop:enable Metrics/MethodLength
|
44
50
|
|
45
|
-
# Apply the log level to the logger
|
46
|
-
def apply_log_level
|
47
|
-
@logger.level = @log_level
|
48
|
-
end
|
49
|
-
|
50
51
|
# Set the default log level based on environment
|
51
52
|
def default_log_level
|
52
53
|
if defined?(Rails) && Rails.env.test?
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './api_client'
|
4
|
+
require_relative './exceptions'
|
5
|
+
|
6
|
+
module Hubspot
|
7
|
+
# Enumerable class for handling paged data from the API
|
8
|
+
class PagedBatch < ApiClient
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
MAX_LIMIT = 100 # HubSpot max items per page
|
12
|
+
|
13
|
+
# rubocop:disable Lint/MissingSuper
|
14
|
+
def initialize(url:, params: {}, resource_class: nil, object_ids: [])
|
15
|
+
@url = url
|
16
|
+
@params = params
|
17
|
+
@resource_class = resource_class
|
18
|
+
@object_ids = object_ids
|
19
|
+
end
|
20
|
+
# rubocop:enable Lint/MissingSuper
|
21
|
+
|
22
|
+
def each_page
|
23
|
+
@object_ids.each_slice(MAX_LIMIT) do |ids|
|
24
|
+
response = fetch_page(ids)
|
25
|
+
results = response['results'] || []
|
26
|
+
mapped_results = @resource_class ? results.map { |result| @resource_class.new(result) } : results
|
27
|
+
yield mapped_results unless mapped_results.empty?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def all
|
32
|
+
results = []
|
33
|
+
each_page do |page|
|
34
|
+
results.concat(page)
|
35
|
+
end
|
36
|
+
results
|
37
|
+
end
|
38
|
+
|
39
|
+
def each(&block)
|
40
|
+
each_page do |page|
|
41
|
+
page.each(&block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def fetch_page(object_ids)
|
48
|
+
params_with_ids = @params.dup
|
49
|
+
params_with_ids[:inputs] = object_ids.map { |id| { id: id } }
|
50
|
+
|
51
|
+
response = self.class.post(@url, body: params_with_ids.to_json)
|
52
|
+
|
53
|
+
handle_response(response)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -8,9 +8,6 @@ module Hubspot
|
|
8
8
|
class PagedCollection < ApiClient
|
9
9
|
include Enumerable
|
10
10
|
|
11
|
-
RATE_LIMIT_STATUS = 429
|
12
|
-
MAX_RETRIES = 3
|
13
|
-
RETRY_WAIT_TIME = 3
|
14
11
|
MAX_LIMIT = 100 # HubSpot max items per page
|
15
12
|
|
16
13
|
# rubocop:disable Lint/MissingSuper
|
@@ -68,18 +65,14 @@ module Hubspot
|
|
68
65
|
|
69
66
|
private
|
70
67
|
|
71
|
-
def fetch_page(offset
|
72
|
-
params_with_offset =
|
68
|
+
def fetch_page(offset)
|
69
|
+
params_with_offset = @params.dup
|
73
70
|
params_with_offset.merge!(after: offset) if offset
|
74
71
|
|
75
72
|
# Handle different HTTP methods
|
76
73
|
response = fetch_response_by_method(params_with_offset)
|
77
74
|
|
78
|
-
|
79
|
-
handle_rate_limit(response, offset, attempt, params_override)
|
80
|
-
else
|
81
|
-
handle_response(response)
|
82
|
-
end
|
75
|
+
handle_response(response)
|
83
76
|
end
|
84
77
|
|
85
78
|
def fetch_response_by_method(params = {})
|
@@ -90,13 +83,5 @@ module Hubspot
|
|
90
83
|
self.class.send(@method, @url, body: params.to_json)
|
91
84
|
end
|
92
85
|
end
|
93
|
-
|
94
|
-
def handle_rate_limit(response, offset, attempt, params_override)
|
95
|
-
raise Hubspot.error_from_response(response) if attempt > MAX_RETRIES
|
96
|
-
|
97
|
-
retry_after = response.headers['Retry-After']&.to_i || RETRY_WAIT_TIME
|
98
|
-
sleep(retry_after)
|
99
|
-
fetch_page(offset, attempt + 1, params_override)
|
100
|
-
end
|
101
86
|
end
|
102
87
|
end
|
data/lib/hubspot/property.rb
CHANGED
data/lib/hubspot/resource.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative './api_client'
|
4
|
+
require_relative './paged_collection'
|
5
|
+
require_relative './paged_batch'
|
4
6
|
|
5
7
|
module Hubspot
|
6
8
|
# rubocop:disable Metrics/ClassLength
|
@@ -53,6 +55,21 @@ module Hubspot
|
|
53
55
|
)
|
54
56
|
end
|
55
57
|
|
58
|
+
def batch_read(object_ids = [], id_property: 'id')
|
59
|
+
params = id_property == 'id' ? {} : { idProperty: id_property }
|
60
|
+
|
61
|
+
PagedBatch.new(
|
62
|
+
url: "/crm/v3/objects/#{resource_name}/batch/read",
|
63
|
+
params: params,
|
64
|
+
object_ids: object_ids,
|
65
|
+
resource_class: self
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def batch_read_all(object_ids = [], id_property: 'id')
|
70
|
+
Hubspot::Batch.read(self, object_ids, id_property: id_property)
|
71
|
+
end
|
72
|
+
|
56
73
|
# Get the complete list of fields (properties) for the object
|
57
74
|
def properties
|
58
75
|
@properties ||= begin
|
@@ -65,6 +82,14 @@ module Hubspot
|
|
65
82
|
properties.reject { |property| property['hubspotDefined'] }
|
66
83
|
end
|
67
84
|
|
85
|
+
def updatable_properties
|
86
|
+
properties.reject(&:read_only?)
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_only_properties
|
90
|
+
properties.select(&:read_only?)
|
91
|
+
end
|
92
|
+
|
68
93
|
def property(property_name)
|
69
94
|
properties.detect { |prop| prop.name == property_name }
|
70
95
|
end
|
@@ -111,8 +136,6 @@ module Hubspot
|
|
111
136
|
|
112
137
|
# rubocop:enable Metrics/MethodLength
|
113
138
|
|
114
|
-
private
|
115
|
-
|
116
139
|
# Define the resource name based on the class
|
117
140
|
def resource_name
|
118
141
|
name = self.name.split('::').last.downcase
|
@@ -123,6 +146,8 @@ module Hubspot
|
|
123
146
|
end
|
124
147
|
end
|
125
148
|
|
149
|
+
private
|
150
|
+
|
126
151
|
# Instantiate a single resource object from the response
|
127
152
|
def instantiate_from_response(response)
|
128
153
|
data = handle_response(response)
|
@@ -161,10 +186,10 @@ module Hubspot
|
|
161
186
|
|
162
187
|
# rubocop:disable Ling/MissingSuper
|
163
188
|
def initialize(data = {})
|
189
|
+
data.transform_keys!(&:to_s)
|
164
190
|
@id = extract_id(data)
|
165
191
|
@properties = {}
|
166
192
|
@metadata = {}
|
167
|
-
|
168
193
|
if @id
|
169
194
|
initialize_from_api(data)
|
170
195
|
else
|
@@ -173,6 +198,10 @@ module Hubspot
|
|
173
198
|
end
|
174
199
|
# rubocop:enable Ling/MissingSuper
|
175
200
|
|
201
|
+
def changes?
|
202
|
+
!@changes.empty?
|
203
|
+
end
|
204
|
+
|
176
205
|
# Instance methods for update (or save)
|
177
206
|
def save
|
178
207
|
if persisted?
|
@@ -207,6 +236,10 @@ module Hubspot
|
|
207
236
|
end
|
208
237
|
alias archive delete
|
209
238
|
|
239
|
+
def resource_name
|
240
|
+
self.class.resource_name
|
241
|
+
end
|
242
|
+
|
210
243
|
# rubocop:disable Metrics/MethodLength
|
211
244
|
# Handle dynamic getter and setter methods with method_missing
|
212
245
|
def method_missing(method, *args)
|
@@ -232,19 +265,15 @@ module Hubspot
|
|
232
265
|
end
|
233
266
|
|
234
267
|
# Fallback if the method or attribute is not found
|
235
|
-
# :nocov:
|
236
268
|
super
|
237
|
-
# :nocov:
|
238
269
|
end
|
239
270
|
# rubocop:enable Metrics/MethodLength
|
240
271
|
|
241
272
|
# Ensure respond_to_missing? is properly overridden
|
242
|
-
# :nocov:
|
243
273
|
def respond_to_missing?(method_name, include_private = false)
|
244
274
|
property_name = method_name.to_s.chomp('=')
|
245
275
|
@properties.key?(property_name) || @changes.key?(property_name) || super
|
246
276
|
end
|
247
|
-
# :nocov:
|
248
277
|
|
249
278
|
private
|
250
279
|
|
data/lib/hubspot/version.rb
CHANGED
data/lib/hubspot.rb
CHANGED
@@ -19,6 +19,8 @@ module Hubspot
|
|
19
19
|
def configure
|
20
20
|
yield(config) if block_given?
|
21
21
|
set_client_headers if config.access_token
|
22
|
+
set_request_timeouts
|
23
|
+
config.apply_log_level
|
22
24
|
end
|
23
25
|
|
24
26
|
def configured?
|
@@ -31,5 +33,18 @@ module Hubspot
|
|
31
33
|
def set_client_headers
|
32
34
|
Hubspot::ApiClient.headers 'Authorization' => "Bearer #{config.access_token}"
|
33
35
|
end
|
36
|
+
|
37
|
+
def set_request_timeouts
|
38
|
+
config.timeout && Hubspot::ApiClient.default_timeout(config.timeout)
|
39
|
+
timeouts = %i[open_timeout read_timeout]
|
40
|
+
timeouts << :write_timeout if RUBY_VERSION >= '2.6'
|
41
|
+
|
42
|
+
timeouts.each do |t|
|
43
|
+
timeout = config.send(t)
|
44
|
+
next unless timeout
|
45
|
+
|
46
|
+
Hubspot::ApiClient.send(t, timeout)
|
47
|
+
end
|
48
|
+
end
|
34
49
|
end
|
35
50
|
end
|
data/lib/ruby_hubspot_api.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# simplest monkey patch, honest ;)
|
4
|
+
class Object
|
5
|
+
# Only define blank? if it's not already defined
|
6
|
+
unless method_defined?(:blank?)
|
7
|
+
def blank?
|
8
|
+
respond_to?(:empty?) ? empty? : !self
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# At some point this will seem like a bad idea ;)
|
14
|
+
|
15
|
+
# :nocov:
|
16
|
+
if RUBY_VERSION < '2.5.0'
|
17
|
+
class Hash
|
18
|
+
# Non-mutating version (returns a new hash with transformed keys)
|
19
|
+
def transform_keys
|
20
|
+
return enum_for(:transform_keys) unless block_given?
|
21
|
+
result = {}
|
22
|
+
each_key do |key|
|
23
|
+
result[yield(key)] = self[key]
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
# Mutating version (modifies the hash in place)
|
29
|
+
def transform_keys!
|
30
|
+
return enum_for(:transform_keys!) unless block_given?
|
31
|
+
keys.each do |key|
|
32
|
+
self[yield(key)] = delete(key)
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# :nocov:end
|