ruby_hubspot_api 0.2.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae46636189d9ad859e0e342eed9b752ccbd64c06
4
- data.tar.gz: 3cf0de6c387b00810af776e6b4ff89bcdcc00230
3
+ metadata.gz: 6052dfd71294e988924beeab1b89662673ceaf93
4
+ data.tar.gz: 4b2e295931295d639e473a9b139df03ec7df1554
5
5
  SHA512:
6
- metadata.gz: ebdaa4d4f2bb6f1f7b6c9fede064598daf78edcf2204322819d2a13e7dbd504ff1792a7afd75037a4eae24171cd9b52d962dc6e2eb7439fe5bb9a84ad6733a22
7
- data.tar.gz: fa2dd031fa494b3bd8c68880d68aa19603035bbf32395a7e3650eefa8906507d6e0790020e032002612d59c516c5eb97b371b69f2f1ff563a4819006aa5d6180
6
+ metadata.gz: caee0d8350205b6bcf45bb4ed4b5421f3fdaacaff87acfcb52c4845858bbafb664f70acbca1a564a34a99a166fd1072a838702d4bf4044b076cfd0b77e87f6a7
7
+ data.tar.gz: 28cbb5b1ea5ebc076d7177e8f4ee35f450bbe7fbb33224a31c55f0801fc566b705cf9b65faf95453b91154675139fb33cfce557ea74e06afd001a09f710190bf
@@ -41,7 +41,7 @@ jobs:
41
41
  env:
42
42
  CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
43
43
 
44
- - name: Upload coverage to Codacy
45
- run: bash <(curl -Ls https://coverage.codacy.com/get.sh)
46
- env:
47
- CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
44
+ # - name: Upload coverage to Codacy
45
+ # run: bash <(curl -Ls https://coverage.codacy.com/get.sh)
46
+ # env:
47
+ # CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
data/.gitignore CHANGED
@@ -1,7 +1,7 @@
1
1
  /.bundle/
2
2
  /coverage/
3
3
  /tmp/
4
-
4
+ *.gem
5
5
  .env
6
6
  # rspec failure tracking
7
7
  .rspec_status
data/.rubocop.yml CHANGED
@@ -1,6 +1,19 @@
1
- # .rubocop.yml
1
+ Documentation:
2
+ Enabled: false
3
+
4
+ Metrics/LineLength:
5
+ Max: 100
6
+ Exclude:
7
+ - spec/**/*.rb
8
+
9
+ # Metrics/MethodLength:
10
+ # Max: 20
11
+
12
+ # Metrics/ClassLength:
13
+ # Max: 200
2
14
 
3
- # Ignore Metrics/BlockLength for the spec directory
4
15
  Metrics/BlockLength:
5
- Exclude:
6
- - spec/**/*.rb
16
+ # Max: 50
17
+ # Ignore Metrics/BlockLength for the spec directory
18
+ Exclude:
19
+ - spec/**/*.rb
data/CHANGELOG.md CHANGED
@@ -1,4 +1,61 @@
1
- ## v.0.2.0
1
+
2
+ ## v0.3.0
3
+
4
+ - Clarify usage
5
+ - More usage clarification
6
+ - Create ruby.yml
7
+ - fix the github workflow
8
+ - fix the github workflow properly
9
+ - add plaatforms to Gemfile.lock
10
+ - add ruby 2.5 and reduce log output
11
+ - try calling rspec directly
12
+ - Use ERB in VCR tests so as to be independent of the env vars
13
+ - ignore ruby version file
14
+ - test on ruby 3.0 too
15
+ - determine if a Hubspot property is read_only (or by negation updatable)
16
+ - adding codecov
17
+ - Using earlier bundler
18
+ - Adding lcov format
19
+ - Add read-only properties to resource class
20
+ - Add property check to the contact spec
21
+ - Only apply lcov formatter if running on github
22
+ - try to upload the coverage results to codacy too
23
+ - Adding Codacy badge
24
+ - Ensure we always apply the right log_level
25
+ - Tidy up documentation of resource
26
+ - Yep. Back ported to 2.4
27
+ - adjust some rubocop settings
28
+ - Tidy up somer doc comments
29
+ - Add some handling of required properties
30
+ - Update user model to force specific properties to be retrieved
31
+ - Improve logic of resource matching
32
+ - Adds :sparkle: attributes method to a resource
33
+ - Tidy comments
34
+ - Update the hierarchy to allow more flexibility
35
+ - Fix find resources
36
+ - Clear up processing results
37
+ - Update batch spec
38
+ - Bump the version
39
+ - Adds validation for resource matcher
40
+ - Dynamically add a method to batch to allow "resources" to be referred to as the resource_name
41
+ - Ignore gem files
42
+ - Drop cadacy for now
43
+ - Update batch spec to check resource_matcher works
44
+ - Ensure properties are passed as named argument
45
+ - No cov for inspect
46
+ - Allow ERB in json fixtures
47
+ - Contact find_by_token spec
48
+ - find_by_token method - uses v1 API
49
+ - Tests the method missing setter for resource
50
+ - Sanitize web mock output
51
+ - Finish specs
52
+ - check the env vars before sanitising
53
+ - make erb explicitly determined by the file exension (.json.erb)
54
+ - use safe navigation for extracting id
55
+ - bump version
56
+ - update gem version in lock file
57
+
58
+ ## v0.2.0
2
59
 
3
60
  - Get the development dependencies right!
4
61
  - Bump the version again
@@ -21,11 +78,23 @@
21
78
  - Simplify mocked responses in batch spec
22
79
  - Adds PagedBatch as pager for batch/read request
23
80
  - Update the Readme to add Batch operations
24
- - #5 batch_updating
81
+ - bump version
25
82
 
26
83
  ## v0.1.2
27
84
 
28
- - initial setup
85
+ - Fix the Readme
86
+ - Sure the search param is values where passing an array
87
+ - update changelog and Gemfile.lock
88
+ - bump version
89
+
90
+ ## v0.1.1
91
+
92
+ - adds the version numbers to the gemspec
93
+ - Fix dependencies
94
+ - bump version
95
+
96
+ ## v0.1.0
97
+
29
98
  - Setup the configuration block
30
99
  - Adds spec for config
31
100
  - Set the auth headers when access_token configured
@@ -67,23 +136,4 @@
67
136
  - Flatten the properties array into a comma separated list
68
137
  - Improve the intialiser
69
138
  - Update the changeling and link in gem spec
70
- - adds the version numbers to the gemspec
71
- - Fix dependencies
72
- - bump version
73
- - Fix the Readme
74
- - Sure the search param is values where passing an array
75
- - update changelog and Gemfile.lock
76
- - bump version
77
-
78
- ## v0.1.1
79
-
80
- - Fix the Readme
81
- - Sure the search param is values where passing an array
82
- - update changelog and Gemfile.lock
83
- - bump version
84
139
 
85
- ## v0.1.0
86
-
87
- - adds the version numbers to the gemspec
88
- - Fix dependencies
89
- - bump version
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_hubspot_api (0.2.1)
4
+ ruby_hubspot_api (0.3.0)
5
5
  httparty (>= 0.1, < 1.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # HubSpot API Client
4
- # Handles all HTTP interactions with the HubSpot API. It manages GET, POST, PATCH, and DELETE requests.
4
+ # Handles all HTTP interactions with the HubSpot API.
5
+ # It manages GET, POST, PATCH, and DELETE requests.
5
6
  module Hubspot
6
7
  # All interations with the Hubspot API happen here...
7
8
  class ApiClient
@@ -59,10 +60,14 @@ module Hubspot
59
60
 
60
61
  def log_request(http_method, url, response, start_time, extra = nil)
61
62
  d = Time.now - start_time
62
- Hubspot.logger.info("#{http_method.to_s.upcase} #{url} took #{d.round(2)}s with status #{response.code}")
63
+
64
+ Hubspot.logger.info(
65
+ "#{http_method.to_s.upcase} #{url} took #{d.round(2)}s with status #{response.code}"
66
+ )
67
+
63
68
  return unless Hubspot.logger.debug?
64
69
 
65
- Hubspot.logger.debug("Request body: #{extra}") if extra
70
+ Hubspot.logger.debug("Request body: #{extra[:body]}") if extra
66
71
  Hubspot.logger.debug("Response body: #{response.body}")
67
72
  end
68
73
 
@@ -93,7 +98,7 @@ module Hubspot
93
98
 
94
99
  def retry_request(request, retries)
95
100
  # 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
101
+ http_method = request.http_method::METHOD.downcase
97
102
  response = HTTParty.send(http_method, request.uri, request.options)
98
103
  handle_response(response, retries)
99
104
  end
data/lib/hubspot/batch.rb CHANGED
@@ -31,8 +31,20 @@ module Hubspot
31
31
  CONTACT_LIMIT = 10
32
32
  DEFAULT_LIMIT = 100
33
33
 
34
+ # :nocov:
35
+ def inspect
36
+ "#<#{self.class.name} " \
37
+ "@resource_count=#{@resources.size}, " \
38
+ "@id_property=#{@id_property.inspect}, " \
39
+ "@resource_type=#{@resources.first&.resource_name}, " \
40
+ "@responses_count=#{@responses.size}>"
41
+ end
42
+ # :nocov:
43
+
34
44
  # rubocop:disable Lint/MissingSuper
35
- def initialize(resources = [], id_property: 'id')
45
+ def initialize(resources = [], id_property: 'id', resource_matcher: nil)
46
+ validate_resource_matcher(resource_matcher)
47
+
36
48
  @resources = []
37
49
  @id_property = id_property # Set id_property for the batch (default: 'id')
38
50
  @responses = [] # Store multiple BatchResponse objects here
@@ -51,11 +63,23 @@ module Hubspot
51
63
  end
52
64
 
53
65
  # Upsert method that calls save with upsert action
54
- def upsert
66
+ def upsert(resource_matcher: nil)
67
+ validate_resource_matcher(resource_matcher)
68
+
55
69
  validate_upsert_conditions
56
70
  save(action: 'upsert')
57
71
  end
58
72
 
73
+ def validate_resource_matcher(resource_matcher)
74
+ return if resource_matcher.blank?
75
+
76
+ unless resource_matcher.is_a?(Proc) && resource_matcher.arity == 2
77
+ raise ArgumentError, 'resource_matcher must be a proc that accepts exactly 2 arguments'
78
+ end
79
+
80
+ @resource_matcher = resource_matcher
81
+ end
82
+
59
83
  # Archive method
60
84
  def archive
61
85
  save(action: 'archive')
@@ -77,15 +101,29 @@ module Hubspot
77
101
  end
78
102
 
79
103
  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'
104
+ if @resources.any?
105
+ if @resources.first.resource_name != resource.resource_name
106
+ raise ArgumentError, 'All resources in a batch must be of the same type'
107
+ end
108
+ else
109
+ add_resource_method(resource.resource_name)
82
110
  end
83
111
 
84
112
  @resources << resource
85
113
  end
86
114
 
115
+ def any_changes?
116
+ @resources.any?(&:changes?)
117
+ end
118
+
87
119
  private
88
120
 
121
+ def add_resource_method(resource_name)
122
+ self.class.class_eval do
123
+ alias_method resource_name.to_sym, :resources
124
+ end
125
+ end
126
+
89
127
  # rubocop:disable Metrics/MethodLength
90
128
  def save(action: 'update')
91
129
  @action = action
@@ -122,9 +160,12 @@ module Hubspot
122
160
  next if resource.changes.empty?
123
161
 
124
162
  {
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
163
+ # Dynamically get the ID based on the batch's id_property
164
+ id: resource.public_send(@id_property),
165
+ # Use the helper method to decide whether to include idProperty
166
+ idProperty: determine_id_property,
167
+ # Gather the changes for the resource
168
+ properties: resource.changes
128
169
  }.compact # Removes nil keys
129
170
  end.compact # Removes nil entries
130
171
  end
@@ -146,7 +187,8 @@ module Hubspot
146
187
 
147
188
  # Perform batch request based on the provided action (upsert, update, create, or archive)
148
189
  def batch_request(type, inputs, action)
149
- response = self.class.post("/crm/v3/objects/#{type}/batch/#{action}", body: { inputs: inputs }.to_json)
190
+ response = self.class.post("/crm/v3/objects/#{type}/batch/#{action}",
191
+ body: { inputs: inputs }.to_json)
150
192
  BatchResponse.new(response.code, handle_response(response))
151
193
  end
152
194
 
@@ -157,7 +199,8 @@ module Hubspot
157
199
  # check if there are any resources without a value from the id_property
158
200
  return unless @resources.any? { |resource| resource.public_send(id_property).blank? }
159
201
 
160
- raise ArgumentError, "All resources must have a non-blank value for #{@id_property} to perform upsert"
202
+ raise ArgumentError,
203
+ "All resources must have a non-blank value for #{@id_property} to perform upsert"
161
204
  end
162
205
 
163
206
  # Return the appropriate batch size limit for the resource type
@@ -167,6 +210,8 @@ module Hubspot
167
210
 
168
211
  # Process responses from the batch API call
169
212
  def process_responses
213
+ # TODO: issue a warning if the id_property is email and the action is upsert*
214
+ # people may have more than one email address abd Hubspot views that as one record
170
215
  @responses.each do |response|
171
216
  next unless response['results']
172
217
 
@@ -192,36 +237,56 @@ module Hubspot
192
237
  end
193
238
 
194
239
  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
240
+ action_method = method_for_action
241
+ send(action_method, result) if action_method
242
+ end
243
+
244
+ def method_for_action
245
+ {
246
+ 'create' => :find_resource_for_created_result,
247
+ 'update' => :find_resource_from_updated_result,
248
+ 'upsert' => :find_resource_from_upserted_result
249
+ }[@action]
250
+ end
251
+
252
+ def find_resource_for_created_result(result)
253
+ properties = result['properties']
254
+
255
+ @resources.reject(&:persisted?).find do |resource|
256
+ next unless resource.changes.any?
257
+
258
+ resource.changes.all? { |key, value| properties[key.to_s] == value }
215
259
  end
216
260
  end
217
261
 
262
+ def find_resource_from_updated_result(result)
263
+ resource_id = id_property == 'id' ? result['id'].to_i : result.dig('properties', id_property)
264
+ find_resource_from_id(resource_id)
265
+ end
266
+
218
267
  def find_resource_from_id(resource_id)
219
- @resources.find { |r| r.id == resource_id }
268
+ return find_resource_from_id_property(resource_id) unless @id_property == 'id'
269
+
270
+ @resources.find { |resource| resource.id == resource_id }
271
+ end
272
+
273
+ def find_resource_from_id_property(resource_id)
274
+ @resources.find do |resource|
275
+ resource.respond_to?(@id_property) && resource.public_send(@id_property) == resource_id
276
+ end
220
277
  end
221
278
 
222
- # def find_resource_from_id_property(resource_id)
223
- # @resources.find { |r| r.public_send(@id_property) == resource_id }
224
- # end
279
+ def find_resource_from_upserted_result(result)
280
+ # if this was inserted then match on all the fields
281
+ return find_resource_for_created_result(result['properties']) if result['new']
282
+
283
+ # call the custom resource matcher if specified
284
+ if @resource_matcher
285
+ @resources.find { |resource| @resource_matcher.call(resource, result) }
286
+ else
287
+ find_resource_from_updated_result(result)
288
+ end
289
+ end
225
290
 
226
291
  def update_resource_properties(resource, properties)
227
292
  properties.each do |key, value|
@@ -237,11 +302,15 @@ module Hubspot
237
302
  end
238
303
 
239
304
  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
305
+ def read(object_class, object_ids = [], properties: [], id_property: 'id')
306
+ unless object_class < Hubspot::Resource
307
+ raise ArgumentError, 'Must be a valid Hubspot resource class'
308
+ end
242
309
 
243
310
  # fetch all the matching resources with paging handled
244
- resources = object_class.batch_read(object_ids, id_property: id_property).all
311
+ resources = object_class.batch_read(object_ids,
312
+ properties: properties,
313
+ id_property: id_property).all
245
314
 
246
315
  # return instance of Hubspot::Batch with the resources set
247
316
  new(resources, id_property: id_property)
@@ -2,5 +2,68 @@
2
2
 
3
3
  module Hubspot
4
4
  class Contact < Resource
5
+ # def required_properties
6
+ # %w[email firstname lastname]
7
+ # end
8
+
9
+ private
10
+
11
+ def metadata_field?(key)
12
+ METADATA_FIELDS.include?(key) || key.start_with?('hs_')
13
+ end
14
+
15
+ class << self
16
+ # Finds a contact by the hubspotutk cookie
17
+ #
18
+ # token - the hubspot tracking token (stored from the hubspotutk cookie value)
19
+ # properties: - Optional list of properties to return.
20
+ # Note: If properties are specified 2 calls to the api will be made because
21
+ # at this time you can only search by the token using the v1 api
22
+ # from which we
23
+ #
24
+ # Example:
25
+ # properties = %w[firstname lastname email last_contacted]
26
+ # contact = Hubspot::Contact.find_by_token(hubspotutk_cookie_value, properties)
27
+ #
28
+ # Returns An instance of the resource.
29
+ def find_by_token(token, properties: [])
30
+ all_properties = build_property_list(properties)
31
+ query_props = all_properties.map { |prop| "property=#{prop}" }
32
+ query_string = query_props.concat(['propertyMode=value_only']).join('&')
33
+
34
+ # Make the original API request, manually appending the query string
35
+ response = get("/contacts/v1/contact/utk/#{token}/profile?#{query_string}")
36
+
37
+ # Only modify the response if it's successful (status 200 OK)
38
+ if response.success?
39
+ # Convert the v1 response body (parsed_response) to a v3 structure
40
+ v3_response_hash = convert_v1_response(response.parsed_response, all_properties)
41
+
42
+ # Modify the existing response object by updating its `parsed_response`
43
+ response.instance_variable_set(:@parsed_response, v3_response_hash)
44
+ end
45
+
46
+ # Pass the (potentially modified) HTTParty response to the next step
47
+ instantiate_from_response(response)
48
+ end
49
+
50
+ private
51
+
52
+ def convert_v1_response(v1_response, property_list)
53
+ # Extract the `vid` as `id`
54
+ v3_response = {
55
+ 'id' => v1_response['vid']
56
+ }
57
+
58
+ properties = property_list.each_with_object({}) do |property, hash|
59
+ hash[property] = v1_response.dig('properties', property, 'value')
60
+ end
61
+
62
+ # Build the v3 structure
63
+ v3_response['properties'] = properties
64
+
65
+ v3_response
66
+ end
67
+ end
5
68
  end
6
69
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hubspot
4
+ class Form < Resource
5
+ METADATA_FIELDS = %w[createdAt updatedAt archived].freeze
6
+
7
+ # :nocov:
8
+ def inspect
9
+ "#<#{self.class.name} " \
10
+ "@name=#{name}, " \
11
+ "@fieldGroups=#{respond_to?('fieldGroups') ? fieldGroups.size : '-'}>"
12
+ end
13
+ # :nocov:
14
+
15
+ class << self
16
+ def api_root
17
+ '/marketing/v3'
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # dont convert (from string)
24
+ def extract_id(id)
25
+ id
26
+ end
27
+ end
28
+ end
@@ -10,6 +10,16 @@ module Hubspot
10
10
 
11
11
  MAX_LIMIT = 100 # HubSpot max items per page
12
12
 
13
+ # :nocov:
14
+ def inspect
15
+ "#<#{self.class.name} " \
16
+ "@url=#{@url.inspect}, " \
17
+ "@params=#{@params.inspect}, " \
18
+ "@resource_class=#{@resource_class.inspect}, " \
19
+ "@object_ids_count=#{@object_ids.size}>"
20
+ end
21
+ # :nocov:
22
+
13
23
  # rubocop:disable Lint/MissingSuper
14
24
  def initialize(url:, params: {}, resource_class: nil, object_ids: [])
15
25
  @url = url
@@ -22,8 +32,7 @@ module Hubspot
22
32
  def each_page
23
33
  @object_ids.each_slice(MAX_LIMIT) do |ids|
24
34
  response = fetch_page(ids)
25
- results = response['results'] || []
26
- mapped_results = @resource_class ? results.map { |result| @resource_class.new(result) } : results
35
+ mapped_results = process_results(response)
27
36
  yield mapped_results unless mapped_results.empty?
28
37
  end
29
38
  end
@@ -45,12 +54,19 @@ module Hubspot
45
54
  private
46
55
 
47
56
  def fetch_page(object_ids)
48
- params_with_ids = @params.dup
57
+ params_with_ids = @params.dup || {}
49
58
  params_with_ids[:inputs] = object_ids.map { |id| { id: id } }
50
59
 
51
60
  response = self.class.post(@url, body: params_with_ids.to_json)
52
61
 
53
62
  handle_response(response)
54
63
  end
64
+
65
+ def process_results(response)
66
+ results = response['results'] || []
67
+ return results unless @resource_class
68
+
69
+ results.map { |result| @resource_class.new(result) }
70
+ end
55
71
  end
56
72
  end
@@ -10,21 +10,18 @@ module Hubspot
10
10
 
11
11
  MAX_LIMIT = 100 # HubSpot max items per page
12
12
 
13
- # rubocop:disable Lint/MissingSuper
14
13
  def initialize(url:, params: {}, resource_class: nil, method: :get)
15
14
  @url = url
16
15
  @params = params
17
16
  @resource_class = resource_class
18
17
  @method = method.to_sym
19
18
  end
20
- # rubocop:enable Lint/MissingSuper
21
19
 
22
20
  def each_page
23
21
  offset = nil
24
22
  loop do
25
23
  response = fetch_page(offset)
26
- results = response['results'] || []
27
- mapped_results = @resource_class ? results.map { |result| @resource_class.new(result) } : results
24
+ mapped_results = process_results(response)
28
25
  yield mapped_results unless mapped_results.empty?
29
26
  offset = response.dig('paging', 'next', 'after')
30
27
  break unless offset
@@ -83,5 +80,12 @@ module Hubspot
83
80
  self.class.send(@method, @url, body: params.to_json)
84
81
  end
85
82
  end
83
+
84
+ def process_results(response)
85
+ results = response['results'] || []
86
+ return results unless @resource_class
87
+
88
+ results.map { |result| @resource_class.new(result) }
89
+ end
86
90
  end
87
91
  end
@@ -6,71 +6,190 @@ require_relative './paged_batch'
6
6
 
7
7
  module Hubspot
8
8
  # rubocop:disable Metrics/ClassLength
9
- # Hubspot::Resource class
9
+
10
+ # HubSpot Resource Base Class
11
+ # This class provides common functionality for interacting with
12
+ # HubSpot API resources such as Contacts, Companies, etc
13
+ #
14
+ # It supports common operations like finding, creating, updating,
15
+ # and deleting resources, as well as batch operations.
16
+ #
17
+ # This class is meant to be inherited by specific resources
18
+ # like `Hubspot::Contact`.
19
+ #
20
+ # Example Usage:
21
+ # Hubspot::Contact.find(1)
22
+ # contact.name # 'Luke'
23
+ #
24
+ # company = Hubspot::Company.create(name: "Acme Corp")
25
+ # company.id.nil? # false
26
+ #
10
27
  class Resource < ApiClient
11
28
  METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze
12
29
 
13
- # Allow read/write access to properties and metadata
14
- attr_accessor :id, :properties, :changes, :metadata
30
+ # Allow read/write access to id, properties, changes and metadata
31
+
32
+ # the id of the object in hubspot
33
+ attr_accessor :id
34
+
35
+ # the properties as if read from the api
36
+ attr_accessor :properties
37
+
38
+ # track any changes made to properties before saving etc
39
+ attr_accessor :changes
40
+
41
+ # any other data sent from the api about the resource
42
+ attr_accessor :metadata
15
43
 
16
44
  class << self
17
45
  # Find a resource by ID and return an instance of the class
18
- def find(id)
19
- response = get("/crm/v3/objects/#{resource_name}/#{id}")
46
+ #
47
+ # id - [Integer] The ID (or hs_object_id) of the resource to fetch.
48
+ # properties - an array of property names to fetch in the result
49
+ #
50
+ # Example:
51
+ # contact = Hubspot::Contact.find(1)
52
+ # contact = Hubspot::Contact.find(1, properties: %w[email firstname lastname custom_field])
53
+ #
54
+ # Returns An instance of the resource.
55
+ def find(id, properties: nil)
56
+ all_properties = build_property_list(properties)
57
+ if all_properties.is_a?(Array) && !all_properties.empty?
58
+ params = { query: { properties: all_properties } }
59
+ end
60
+ response = get("#{api_root}/#{resource_name}/#{id}", params || {})
20
61
  instantiate_from_response(response)
21
62
  end
22
63
 
64
+ # Finds a resource by a given property and value.
65
+ #
66
+ # property - The property to search by (e.g., "email").
67
+ # value - The value of the property to match.
68
+ # properties - Optional list of properties to return.
69
+ #
70
+ # Example:
71
+ # properties = %w[firstname lastname email last_contacted]
72
+ # contact = Hubspot::Contact.find_by("email", "john@example.com", properties)
73
+ #
74
+ # Returns An instance of the resource.
23
75
  def find_by(property, value, properties = nil)
24
76
  params = { idProperty: property }
25
- params[:properties] = properties if properties.is_a?(Array)
26
- response = get("/crm/v3/objects/#{resource_name}/#{value}", query: params)
77
+
78
+ all_properties = build_property_list(properties)
79
+ params[:properties] = all_properties unless all_properties.empty?
80
+
81
+ response = get("#{api_root}/#{resource_name}/#{value}", query: params)
27
82
  instantiate_from_response(response)
28
83
  end
29
84
 
30
- # Create a new resource
85
+ # Creates a new resource with the given parameters.
86
+ #
87
+ # params - The properties to create the resource with.
88
+ #
89
+ # Example:
90
+ # contact = Hubspot::Contact.create(name: "John Doe", email: "john@example.com")
91
+ #
92
+ # Returns [Resource] The newly created resource.
31
93
  def create(params)
32
- response = post("/crm/v3/objects/#{resource_name}", body: { properties: params }.to_json)
94
+ response = post("#{api_root}/#{resource_name}", body: { properties: params }.to_json)
33
95
  instantiate_from_response(response)
34
96
  end
35
97
 
98
+ # Updates an existing resource by ID.
99
+ #
100
+ # id - The ID of the resource to update.
101
+ # params - The properties to update.
102
+ #
103
+ # Example:
104
+ # contact.update(1, name: "Jane Doe")
105
+ #
106
+ # Returns True if the update was successful
36
107
  def update(id, params)
37
- response = patch("/crm/v3/objects/#{resource_name}/#{id}", body: { properties: params }.to_json)
38
- raise Hubspot.error_from_response(response) unless response.success?
108
+ response = patch("#{api_root}/#{resource_name}/#{id}",
109
+ body: { properties: params }.to_json)
110
+ handle_response(response)
39
111
 
40
112
  true
41
113
  end
42
114
 
115
+ # Deletes a resource by ID.
116
+ #
117
+ # id - The ID of the resource to delete.
118
+ #
119
+ # Example:
120
+ # Hubspot::Contact.archive(1)
121
+ #
122
+ # Returns True if the deletion was successful
43
123
  def archive(id)
44
- response = delete("/crm/v3/objects/#{resource_name}/#{id}")
45
- raise Hubspot.error_from_response(response) unless response.success?
124
+ response = delete("#{api_root}/#{resource_name}/#{id}")
125
+ handle_response(response)
46
126
 
47
127
  true
48
128
  end
49
129
 
130
+ # Lists all resources with optional filters and pagination.
131
+ #
132
+ # params - Optional parameters to filter or paginate the results.
133
+ #
134
+ # Example:
135
+ # contacts = Hubspot::Contact.list(limit: 100)
136
+ #
137
+ # Returns [PagedCollection] A collection of resources.
50
138
  def list(params = {})
139
+ all_properties = build_property_list(params[:properties])
140
+
141
+ if all_properties.is_a?(Array) && !all_properties.empty?
142
+ params[:properties] = all_properties.join(',')
143
+ end
144
+
51
145
  PagedCollection.new(
52
- url: "/crm/v3/objects/#{resource_name}",
146
+ url: list_page_uri,
53
147
  params: params,
54
148
  resource_class: self
55
149
  )
56
150
  end
57
151
 
58
- def batch_read(object_ids = [], id_property: 'id')
59
- params = id_property == 'id' ? {} : { idProperty: id_property }
152
+ # Performs a batch read operation to retrieve multiple resources by their IDs.
153
+ #
154
+ # object_ids - A list of resource IDs to fetch.
155
+ #
156
+ # id_property - The property to use for identifying resources (default: 'id').
157
+ #
158
+ #
159
+ # Example:
160
+ # Hubspot::Contact.batch_read([1, 2, 3])
161
+ #
162
+ # Returns [PagedBatch] A paged batch of resources
163
+ def batch_read(object_ids = [], properties: [], id_property: 'id')
164
+ params = {}
165
+ params[:idProperty] = id_property unless id_property == 'id'
166
+ params[:properties] = properties unless properties.blank?
60
167
 
61
168
  PagedBatch.new(
62
- url: "/crm/v3/objects/#{resource_name}/batch/read",
63
- params: params,
169
+ url: "#{api_root}/#{resource_name}/batch/read",
170
+ params: params.empty? ? nil : params,
64
171
  object_ids: object_ids,
65
172
  resource_class: self
66
173
  )
67
174
  end
68
175
 
69
- def batch_read_all(object_ids = [], id_property: 'id')
70
- Hubspot::Batch.read(self, object_ids, id_property: id_property)
176
+ # Performs a batch read operation to retrieve multiple resources by their IDs
177
+ # until there are none left
178
+ #
179
+ # object_ids - A list of resource IDs to fetch. [Array<Integer>]
180
+ # id_property - The property to use for identifying resources (default: 'id').
181
+ #
182
+ # Example:
183
+ # Hubspot::Contact.batch_read_all(hubspot_contact_ids)
184
+ #
185
+ # Returns [Hubspot::Batch] A batch of resources that can be operated on further
186
+ def batch_read_all(object_ids = [], properties: [], id_property: 'id')
187
+ Hubspot::Batch.read(self, object_ids, properties: properties, id_property: id_property)
71
188
  end
72
189
 
73
- # Get the complete list of fields (properties) for the object
190
+ # Retrieve the complete list of properties for this resource class
191
+ #
192
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
74
193
  def properties
75
194
  @properties ||= begin
76
195
  response = get("/crm/v3/properties/#{resource_name}")
@@ -78,18 +197,36 @@ module Hubspot
78
197
  end
79
198
  end
80
199
 
200
+ # Retrieve the complete list of user defined properties for this resource class
201
+ #
202
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
81
203
  def custom_properties
82
204
  properties.reject { |property| property['hubspotDefined'] }
83
205
  end
84
206
 
207
+ # Retrieve the complete list of updatable properties for this resource class
208
+ #
209
+ # Returns [Array<Hubspot::Property>] An array of updateable hubspot properties
85
210
  def updatable_properties
86
211
  properties.reject(&:read_only?)
87
212
  end
88
213
 
214
+ # Retrieve the complete list of read-only properties for this resource class
215
+ #
216
+ # Returns [Array<Hubspot::Property>] An array of read-only hubspot properties
89
217
  def read_only_properties
90
- properties.select(&:read_only?)
218
+ properties.select(&:read_only)
91
219
  end
92
220
 
221
+ # Retrieve information about a specific property
222
+ #
223
+ # Example:
224
+ # property = Hubspot::Contact.property('industry_sector')
225
+ # values_for_select = property.options.each_with_object({}) do |prop, hash|
226
+ # hash[prop['value']] = prop['label']
227
+ # end
228
+ #
229
+ # Returns [Hubspot::Property] A hubspot property
93
230
  def property(property_name)
94
231
  properties.detect { |prop| prop.name == property_name }
95
232
  end
@@ -106,6 +243,48 @@ module Hubspot
106
243
  }.freeze
107
244
 
108
245
  # rubocop:disable Metrics/MethodLength
246
+
247
+ # Search for resources using a flexible query format and optional properties.
248
+ #
249
+ # This method allows searching for resources by passing a query in the form of a string
250
+ # (for full-text search) or a hash with special suffixes on the keys to
251
+ # define different comparison operators.
252
+ #
253
+ # You can also specify which properties to return and the number of results per page.
254
+ #
255
+ # Available suffixes for query keys (when using a hash):
256
+ # - `_contains`: Matches values that contain the given string.
257
+ # - `_gt`: Greater than comparison.
258
+ # - `_lt`: Less than comparison.
259
+ # - `_gte`: Greater than or equal to comparison.
260
+ # - `_lte`: Less than or equal to comparison.
261
+ # - `_neq`: Not equal to comparison.
262
+ # - `_in`: Matches any of the values in the given array.
263
+ #
264
+ # If no suffix is provided, the default comparison is equality (`EQ`).
265
+ #
266
+ # query - [String, Hash] The query for searching. This can be either:
267
+ # - A String: for full-text search.
268
+ # - A Hash: where each key represents a property and may have suffixes for the comparison
269
+ # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`).
270
+ # properties - An optional array of property names to return in the search results.
271
+ # If not specified or empty, HubSpot will return the default set of properties.
272
+ # page_size - The number of results to return per page
273
+ # (default is 10 for contacts and 100 for everything else).
274
+ #
275
+ # Example Usage:
276
+ # # Full-text search for 'example.org':
277
+ # props = %w[email firstname lastname]
278
+ # contacts = Hubspot::Contact.search(query: "example.org", properties: props, page_size: 50)
279
+ #
280
+ # # Search for contacts whose email contains 'example.org' and are older than 30:
281
+ # contacts = Hubspot::Contact.search(
282
+ # query: { email_contains: 'example.org', age_gt: 30 },
283
+ # properties: ["email", "firstname", "lastname"],
284
+ # page_size: 50
285
+ # )
286
+ #
287
+ # Returns [PagedCollection] A paged collection of results that can be iterated over.
109
288
  def search(query:, properties: [], page_size: 100)
110
289
  search_body = {}
111
290
 
@@ -127,7 +306,7 @@ module Hubspot
127
306
 
128
307
  # Perform the search and return a PagedCollection
129
308
  PagedCollection.new(
130
- url: "/crm/v3/objects/#{resource_name}/search",
309
+ url: "#{api_root}/#{resource_name}/search",
131
310
  params: search_body,
132
311
  resource_class: self,
133
312
  method: :post
@@ -136,6 +315,10 @@ module Hubspot
136
315
 
137
316
  # rubocop:enable Metrics/MethodLength
138
317
 
318
+ # The root of the api call. Mostly this will be "crm"
319
+ # but you can override this to account for a different
320
+ # object hierarchy
321
+
139
322
  # Define the resource name based on the class
140
323
  def resource_name
141
324
  name = self.name.split('::').last.downcase
@@ -146,8 +329,22 @@ module Hubspot
146
329
  end
147
330
  end
148
331
 
332
+ # List of properties that will always be retrieved
333
+ # should be overridden in specific resource class
334
+ def required_properties
335
+ []
336
+ end
337
+
149
338
  private
150
339
 
340
+ def api_root
341
+ '/crm/v3/objects'
342
+ end
343
+
344
+ def list_page_uri
345
+ "#{api_root}/#{resource_name}"
346
+ end
347
+
151
348
  # Instantiate a single resource object from the response
152
349
  def instantiate_from_response(response)
153
350
  data = handle_response(response)
@@ -182,12 +379,44 @@ module Hubspot
182
379
  # Default to 'EQ' operator if no suffix is found
183
380
  { propertyName: key.to_s, operator: 'EQ' }
184
381
  end
382
+
383
+ # Internal make a list of properties to request from the API
384
+ # will be merged with any required_properties defined on the class
385
+ def build_property_list(properties)
386
+ properties = [] unless properties.is_a?(Array)
387
+ raise 'Must be an array' unless required_properties.is_a?(Array)
388
+
389
+ properties.concat(required_properties).uniq
390
+ end
185
391
  end
186
392
 
187
- # rubocop:disable Ling/MissingSuper
393
+ # rubocop:disable Lint/MissingSuper
394
+
395
+ # Public: Initialize a resouce
396
+ #
397
+ # data - [2D Hash, nested Hash] data to initialise:
398
+ # - The response from the api will be of the form:
399
+ # { id: <hs_object_id>, properties: { "email": "john@example.org" ... }, ... }
400
+ #
401
+ # - A Simple 2D Hash, key value pairs in the form:
402
+ # { email: 'john@example.org', firstname: 'John', lastname: 'Smith' }
403
+ #
404
+ # - A structured hash consisting of { id: <hs_object_id>, properties: {}, ... }
405
+ # This is the same structure as per the API, and can be rebuilt if you store the id
406
+ # of the object against your own data
407
+ #
408
+ # Example:
409
+ # attrs = { firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org' }
410
+ # contact = Hubspot::Contact.new(attrs)
411
+ # contact.persisted? # false
412
+ # contact.save # creates the record in Hubspot
413
+ # contact.persisted? # true
414
+ # puts "Contact saved with hubspot id #{contact.id}"
415
+ #
416
+ # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
188
417
  def initialize(data = {})
189
418
  data.transform_keys!(&:to_s)
190
- @id = extract_id(data)
419
+ @id = extract_id(data.delete('id'))
191
420
  @properties = {}
192
421
  @metadata = {}
193
422
  if @id
@@ -196,13 +425,22 @@ module Hubspot
196
425
  initialize_new_object(data)
197
426
  end
198
427
  end
199
- # rubocop:enable Ling/MissingSuper
428
+ # rubocop:enable Lint/MissingSuper
200
429
 
430
+ # Determine the state of the object
431
+ #
432
+ # Returns Boolean
201
433
  def changes?
202
434
  !@changes.empty?
203
435
  end
204
436
 
205
- # Instance methods for update (or save)
437
+ # Create or Update the resource.
438
+ # If the resource was already persisted (e.g. it was retrieved from the API)
439
+ # it will be updated using values from @changes
440
+ #
441
+ # If the resource is new (no id) it will be created
442
+ #
443
+ # Returns Boolean
206
444
  def save
207
445
  if persisted?
208
446
  self.class.update(@id, @changes).tap do |result|
@@ -216,21 +454,57 @@ module Hubspot
216
454
  end
217
455
  end
218
456
 
457
+ # If the resource exists in Hubspot
458
+ #
459
+ # Returns Boolean
219
460
  def persisted?
220
461
  @id ? true : false
221
462
  end
222
463
 
223
- # Update the resource
224
- def update(params)
464
+ # Public - Update the resource and persist to the api
465
+ #
466
+ # attributes - hash of properties to update in key value pairs
467
+ #
468
+ # Example:
469
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
470
+ # contact.update(status: 'gold customer', last_contacted_at: Time.now.utc.iso8601)
471
+ #
472
+ # Returns Boolean
473
+ def update(attributes)
225
474
  raise 'Not able to update as not persisted' unless persisted?
226
475
 
227
- params.each do |key, value|
228
- send("#{key}=", value) # This will trigger the @changes tracking via method_missing
229
- end
476
+ update_attributes(attributes)
230
477
 
231
478
  save
232
479
  end
233
480
 
481
+ # Public - Update resource attributes
482
+ #
483
+ # Does not persist to the api but processes each attribute correctly
484
+ #
485
+ # Example:
486
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
487
+ # contact.changes? # false
488
+ # contact.update_attributes(education: 'Graduate', university: 'Life')
489
+ # contact.education # Graduate
490
+ # contact.changes? # true
491
+ # contact.changes # { "education" => "Graduate", "university" => "Life" }
492
+ #
493
+ # Returns Hash of changes
494
+ def update_attributes(attributes)
495
+ raise ArgumentError, 'must be a hash' unless attributes.is_a?(Hash)
496
+
497
+ attributes.each do |key, value|
498
+ send("#{key}=", value) # This will trigger the @changes tracking via method_missing
499
+ end
500
+ end
501
+
502
+ # Archive the object in Hubspot
503
+ #
504
+ # Example:
505
+ # company = Hubspot::Company.find(hubspot_company_id)
506
+ # company.delete
507
+ #
234
508
  def delete
235
509
  self.class.archive(id)
236
510
  end
@@ -241,7 +515,11 @@ module Hubspot
241
515
  end
242
516
 
243
517
  # rubocop:disable Metrics/MethodLength
244
- # Handle dynamic getter and setter methods with method_missing
518
+
519
+ # getter: Check the properties and changes hashes to see if the method
520
+ # being called is a key, and return the corresponding value
521
+ # setter: If the method ends in "=" persist the value in the changes hash
522
+ # (when it is different from the corresponding value in properties if set)
245
523
  def method_missing(method, *args)
246
524
  method_name = method.to_s
247
525
 
@@ -249,15 +527,8 @@ module Hubspot
249
527
  if method_name.end_with?('=')
250
528
  attribute = method_name.chomp('=')
251
529
  new_value = args.first
252
-
253
- # Track changes only if the value has actually changed
254
- if @properties[attribute] != new_value
255
- @changes[attribute] = new_value
256
- else
257
- @changes.delete(attribute) # Remove from changes if it reverts to the original value
258
- end
259
-
260
- return new_value
530
+ add_accessors attribute
531
+ return send("#{attribute}=", new_value)
261
532
  # Handle getters
262
533
  else
263
534
  return @changes[method_name] if @changes.key?(method_name)
@@ -267,35 +538,77 @@ module Hubspot
267
538
  # Fallback if the method or attribute is not found
268
539
  super
269
540
  end
541
+
270
542
  # rubocop:enable Metrics/MethodLength
271
543
 
272
- # Ensure respond_to_missing? is properly overridden
544
+ # Ensure respond_to_missing? handles existing keys in the properties anc changes hashes
273
545
  def respond_to_missing?(method_name, include_private = false)
274
546
  property_name = method_name.to_s.chomp('=')
275
547
  @properties.key?(property_name) || @changes.key?(property_name) || super
276
548
  end
277
549
 
550
+ # Initialize from API response, separating metadata from properties
551
+ def initialize_from_api(data)
552
+ @changes = data.delete('changes')&.transform_keys!(&:to_s) || {}
553
+
554
+ if data['properties']
555
+ @metadata = data.reject { |key, _v| key == 'properties' }
556
+ handle_properties(data['properties'])
557
+ else
558
+ handle_properties(data)
559
+ end
560
+ end
561
+
278
562
  private
279
563
 
280
564
  # Extract ID from data and convert to integer
281
- def extract_id(data)
282
- data['id'] ? data['id'].to_i : nil
565
+ def extract_id(id)
566
+ id&.to_i
283
567
  end
284
568
 
285
- # Initialize from API response, separating metadata from properties
286
- def initialize_from_api(data)
287
- @metadata = extract_metadata(data)
288
- properties_data = data['properties'] || {}
569
+ def handle_properties(properties_data)
570
+ properties_data.each do |attribute, value|
571
+ if metadata_field?(attribute)
572
+ @metadata[attribute.to_s] = value
573
+ else
574
+ add_accessors attribute.to_s
575
+ @properties[attribute.to_s] = value
576
+ end
577
+ end
578
+ end
579
+
580
+ def add_accessors(attribute)
581
+ add_accessors_setter(attribute)
582
+ add_accessors_getter(attribute)
583
+ end
289
584
 
290
- properties_data.each do |key, value|
291
- if METADATA_FIELDS.include?(key)
292
- @metadata[key] = value
585
+ def add_accessors_setter(attribute)
586
+ # Define the setter method
587
+ define_singleton_method("#{attribute}=") do |new_value|
588
+ # Track changes only if the value has actually changed
589
+ if @properties[attribute] != new_value
590
+ @changes[attribute] = new_value
293
591
  else
294
- @properties[key] = value
592
+ @changes.delete(attribute) # Remove from changes if it reverts to the original value
295
593
  end
594
+
595
+ new_value
596
+ end
597
+ end
598
+
599
+ def add_accessors_getter(attribute)
600
+ # Define the getter method
601
+ define_singleton_method(attribute) do
602
+ # Return from changes if available, else return from properties
603
+ return @changes[attribute] if @changes.key?(attribute)
604
+
605
+ @properties[attribute] if @properties.key?(attribute)
296
606
  end
607
+ end
297
608
 
298
- @changes = {}
609
+ # allows overwriting in other resource classes
610
+ def metadata_field?(key)
611
+ METADATA_FIELDS.include?(key)
299
612
  end
300
613
 
301
614
  # Initialize a new object (no API response)
@@ -305,11 +618,6 @@ module Hubspot
305
618
  @metadata = {}
306
619
  end
307
620
 
308
- # Extract metadata from data, excluding properties
309
- def extract_metadata(data)
310
- data.reject { |key, _| key == 'properties' }
311
- end
312
-
313
621
  # Create a new resource
314
622
  def create_new
315
623
  created_resource = self.class.create(@changes)
data/lib/hubspot/user.rb CHANGED
@@ -1,8 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
+ # ORM for hubspot users
5
+ #
6
+ # Hubspot users consist mostly of read_only attributes (you can add custom properties).
7
+ # As such we extend this class to ensure that we retrieve useful data back from the API
8
+ # and provide helper methods to resolve hubspot fields e.g. user.email calls user.hs_email etc
4
9
  class User < Resource
10
+ class << self
11
+ def required_properties
12
+ %w[hs_email hs_given_name hs_family_name]
13
+ end
14
+ end
15
+
16
+ def first_name
17
+ hs_given_name
18
+ end
19
+ alias firstname first_name
20
+
21
+ def last_name
22
+ hs_family_name
23
+ end
24
+ alias lastname last_name
25
+
26
+ def email
27
+ hs_email
28
+ end
5
29
  end
6
30
 
7
- Owner = User
31
+ Owner = User
8
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
- VERSION = '0.2.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -12,13 +12,18 @@ require_relative 'hubspot/config'
12
12
  require_relative 'hubspot/exceptions'
13
13
  require_relative 'hubspot/api_client'
14
14
 
15
- # load base class then modules
15
+ # load base class then models
16
16
  require_relative 'hubspot/resource'
17
17
  require_relative 'hubspot/property'
18
+
19
+ # load CRM models
18
20
  require_relative 'hubspot/contact'
19
21
  require_relative 'hubspot/company'
20
22
  require_relative 'hubspot/user'
21
23
 
24
+ # load marketing models
25
+ require_relative 'hubspot/form'
26
+
22
27
  # Load other components
23
28
  require_relative 'hubspot/batch'
24
29
  require_relative 'hubspot/paged_collection'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_hubspot_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Brook
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-01 00:00:00.000000000 Z
11
+ date: 2024-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -223,6 +223,7 @@ files:
223
223
  - lib/hubspot/config.rb
224
224
  - lib/hubspot/contact.rb
225
225
  - lib/hubspot/exceptions.rb
226
+ - lib/hubspot/form.rb
226
227
  - lib/hubspot/paged_batch.rb
227
228
  - lib/hubspot/paged_collection.rb
228
229
  - lib/hubspot/property.rb
@@ -254,7 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
255
  version: '0'
255
256
  requirements: []
256
257
  rubyforge_project:
257
- rubygems_version: 2.6.14.4
258
+ rubygems_version: 2.6.14
258
259
  signing_key:
259
260
  specification_version: 4
260
261
  summary: ruby_hubspot_api is an ORM-like wrapper for the Hubspot API