ruby_hubspot_api 0.2.1.1 → 0.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.
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