ruby_hubspot_api 0.2.2 → 0.3.1

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
- SHA256:
3
- metadata.gz: 52c9ee03d417a29792dbd370399982a03662cb621c0ab3e5ec0a2abd7de89ac5
4
- data.tar.gz: aae2926993d4b6dcf78b8e728c031631225f667c882f0b6d0710601ff62c0772
2
+ SHA1:
3
+ metadata.gz: 62b690c7e426369cd5979c4f6b5b0cec8b5a7620
4
+ data.tar.gz: 26047bb4aa34d3a91bf6c1aff80b6e3775220f4f
5
5
  SHA512:
6
- metadata.gz: 6d2c9d82565c619a189bfc0780c914bb1b10cf769eb94b66e1e702fa5268fedadf7e3b9a188f6a1b0caa67c1b2d432e8f6401ffb890ca01efc768abc16ae3d3a
7
- data.tar.gz: e66a9d5daa8373529a3a2ee07c474d288d94327fc40d83ed08e050e1dbb83c8f89217c95083035396f212ed78b97348f1a9437889d2719cd9dbf49d4624cbce3
6
+ metadata.gz: a6512174b13fa2ef07740690864c79a6c3593c908c1fed37170ef35bf08ca7ae5ceb9a3e53fe12d53079eb4e4bb47a58801430103b96c9a7b78dfd67eaaabaa5
7
+ data.tar.gz: 6ea719cb62b217fdbbdb2009d64215369608ba8d3423b59245472fb64fcd957174c5cc28a0cbcbd1f5eaf904260d19e1989da528df913292c139b5f4d893110e
@@ -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/CHANGELOG.md CHANGED
@@ -1,89 +1,139 @@
1
- ## v.0.2.0
2
1
 
3
- - Get the development dependencies right!
4
- - Bump the version again
5
- - describe find_by method
6
- - update lock
7
- - batch :sparkle: upsert spec
8
- - Borrowing Object#blank? method cos it actually really helps...
9
- - batch implemntation
10
- - logger.debug the post body and response body
11
- - Adds a changes? Method on resource
12
- - Adds instance method resource_name on resource
13
- - Ensure keys are stringified
14
- - Add all end points to the batch spec
15
- - Adds create and archive methods to batches
16
- - Tidy up resource code
17
- - Cover the previously nocov'd code
18
- - Add api client logging spec
19
- - add configurable timeouts to requests
20
- - Move rate limit handling to the client
21
- - Simplify mocked responses in batch spec
22
- - Adds PagedBatch as pager for batch/read request
2
+ ## v0.3.0
3
+
4
+ - update gem version in lock file
5
+ - bump version
6
+ - use safe navigation for extracting id
7
+ - make erb explicitly determined by the file exension (.json.erb)
8
+ - check the env vars before sanitising
9
+ - Finish specs
10
+ - Sanitize web mock output
11
+ - Tests the method missing setter for resource
12
+ - find_by_token method - uses v1 API
13
+ - Contact find_by_token spec
14
+ - Allow ERB in json fixtures
15
+ - No cov for inspect
16
+ - Ensure properties are passed as named argument
17
+ - Update batch spec to check resource_matcher works
18
+ - Drop cadacy for now
19
+ - Ignore gem files
20
+ - Dynamically add a method to batch to allow "resources" to be referred to as the resource_name
21
+ - Adds validation for resource matcher
22
+ - Bump the version
23
+ - Update batch spec
24
+ - Clear up processing results
25
+ - Fix find resources
26
+ - Update the hierarchy to allow more flexibility
27
+ - Tidy comments
28
+ - Adds :sparkle: attributes method to a resource
29
+ - Improve logic of resource matching
30
+ - Update user model to force specific properties to be retrieved
31
+ - Add some handling of required properties
32
+ - Tidy up somer doc comments
33
+ - adjust some rubocop settings
34
+ - Yep. Back ported to 2.4
35
+ - Tidy up documentation of resource
36
+ - Ensure we always apply the right log_level
37
+ - Adding Codacy badge
38
+ - try to upload the coverage results to codacy too
39
+ - Only apply lcov formatter if running on github
40
+ - Add property check to the contact spec
41
+ - Add read-only properties to resource class
42
+ - Adding lcov format
43
+ - Using earlier bundler
44
+ - adding codecov
45
+ - determine if a Hubspot property is read_only (or by negation updatable)
46
+ - test on ruby 3.0 too
47
+ - ignore ruby version file
48
+ - Use ERB in VCR tests so as to be independent of the env vars
49
+ - try calling rspec directly
50
+ - add ruby 2.5 and reduce log output
51
+ - add plaatforms to Gemfile.lock
52
+ - fix the github workflow properly
53
+ - fix the github workflow
54
+ - Create ruby.yml
55
+ - More usage clarification
56
+ - Clarify usage
57
+
58
+ ## v0.2.0
59
+
60
+ - bump version
23
61
  - Update the Readme to add Batch operations
24
- - #5 batch_updating
62
+ - Adds PagedBatch as pager for batch/read request
63
+ - Simplify mocked responses in batch spec
64
+ - Move rate limit handling to the client
65
+ - add configurable timeouts to requests
66
+ - Add api client logging spec
67
+ - Cover the previously nocov'd code
68
+ - Tidy up resource code
69
+ - Adds create and archive methods to batches
70
+ - Add all end points to the batch spec
71
+ - Ensure keys are stringified
72
+ - Adds instance method resource_name on resource
73
+ - Adds a changes? Method on resource
74
+ - logger.debug the post body and response body
75
+ - batch implemntation
76
+ - Borrowing Object#blank? method cos it actually really helps...
77
+ - batch :sparkle: upsert spec
78
+ - update lock
79
+ - describe find_by method
80
+ - Bump the version again
81
+ - Get the development dependencies right!
25
82
 
26
83
  ## v0.1.2
27
84
 
28
- - initial setup
29
- - Setup the configuration block
30
- - Adds spec for config
31
- - Set the auth headers when access_token configured
32
- - Version spec
33
- - don't test for client id
34
- - Load api client and add exception handler
35
- - Initial bases class Resource for api crud
36
- - Contact class with spec
37
- - Cassettes for contact spec
38
- - company model and spec with cassettes
39
- - Adds PagedCollection for paged results (list / search)
40
- - Adds Hubspot configuration to tests
41
- - Update required files
42
- - Adds list method to return PagedCollection
43
- - Add interface for search
44
- - VCR configuration
45
- - Contact/search cassette
46
- - Sample ENV file for developers
47
- - console with configuration if env vars set
48
- - Readme file
49
- - MIT license
50
- - allow connections if vcr_record_mode is on
51
- - Fix rubocop config
52
- - Update Readme
53
- - Log api requests and add interface to set logging
54
- - Update exception handing logic and add more exception classes
55
- - Test all parts of the config code
56
- - Adds user model (aliased to Owner) and specs
57
- - Add a configured? method to Hubspot module
58
- - Adds a dummy Hubspot::Property class
59
- - Update Paged request handling based on method
60
- - Adds a find_by mechanism for resources
61
- - Use the Hubspot::Property class to return properties for a given resource object
62
- - Update the specs for 100% coverage
63
- - Update the sample .env file
64
- - Reorder and clarify Readme
65
- - When we use limit(1) we should only return the object not an array
66
- - Test that we only get the properties we ask for or the defaults
67
- - Flatten the properties array into a comma separated list
68
- - Improve the intialiser
69
- - Update the changeling and link in gem spec
70
- - adds the version numbers to the gemspec
71
- - Fix dependencies
72
85
  - bump version
73
- - Fix the Readme
74
- - Sure the search param is values where passing an array
75
86
  - update changelog and Gemfile.lock
76
- - bump version
87
+ - Sure the search param is values where passing an array
88
+ - Fix the Readme
77
89
 
78
90
  ## v0.1.1
79
91
 
80
- - Fix the Readme
81
- - Sure the search param is values where passing an array
82
- - update changelog and Gemfile.lock
83
92
  - bump version
93
+ - Fix dependencies
94
+ - adds the version numbers to the gemspec
84
95
 
85
96
  ## v0.1.0
86
97
 
87
- - adds the version numbers to the gemspec
88
- - Fix dependencies
89
- - bump version
98
+ - Update the changeling and link in gem spec
99
+ - Improve the intialiser
100
+ - Flatten the properties array into a comma separated list
101
+ - Test that we only get the properties we ask for or the defaults
102
+ - When we use limit(1) we should only return the object not an array
103
+ - Reorder and clarify Readme
104
+ - Update the sample .env file
105
+ - Update the specs for 100% coverage
106
+ - Use the Hubspot::Property class to return properties for a given resource object
107
+ - Adds a find_by mechanism for resources
108
+ - Update Paged request handling based on method
109
+ - Adds a dummy Hubspot::Property class
110
+ - Add a configured? method to Hubspot module
111
+ - Adds user model (aliased to Owner) and specs
112
+ - Test all parts of the config code
113
+ - Update exception handing logic and add more exception classes
114
+ - Log api requests and add interface to set logging
115
+ - Update Readme
116
+ - Fix rubocop config
117
+ - allow connections if vcr_record_mode is on
118
+ - MIT license
119
+ - Readme file
120
+ - console with configuration if env vars set
121
+ - Sample ENV file for developers
122
+ - Contact/search cassette
123
+ - VCR configuration
124
+ - Add interface for search
125
+ - Adds list method to return PagedCollection
126
+ - Update required files
127
+ - Adds Hubspot configuration to tests
128
+ - Adds PagedCollection for paged results (list / search)
129
+ - company model and spec with cassettes
130
+ - Cassettes for contact spec
131
+ - Contact class with spec
132
+ - Initial bases class Resource for api crud
133
+ - Load api client and add exception handler
134
+ - don't test for client id
135
+ - Version spec
136
+ - Set the auth headers when access_token configured
137
+ - Adds spec for config
138
+ - Setup the configuration block
139
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_hubspot_api (0.2.2)
4
+ ruby_hubspot_api (0.3.1)
5
5
  httparty (>= 0.1, < 1.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -120,7 +120,7 @@ puts "Contact: #{contact.firstname} #{contact.lastname}"
120
120
 
121
121
  ### Updating an Existing Object
122
122
 
123
- To update an existing object, you can either modify the object and call `save`, or use the `update` method specifying the properties you want to update. You can test whether or not the object will need to upload changes to the api by using the changes? method
123
+ To update an existing object, you can either modify the object and call `save`, or use the `update` method specifying the properties you want to update. You can test whether or not the object will need to upload changes to the api by using the changes? method. If you don't want to check for changes? you can use the method `save!` on the resource which will raise a Hubspot::NothingToDoError if there are no changes.
124
124
 
125
125
  Example using `save`:
126
126
 
@@ -136,6 +136,22 @@ contact.save # true
136
136
  contact.changes? # false
137
137
  ```
138
138
 
139
+ Example using `save!`:
140
+
141
+ ```ruby
142
+ contact = Hubspot::Contact.find(1)
143
+
144
+ MyDecorator.new(contact).apply_changes!
145
+
146
+ begin
147
+ contact.save!
148
+ rescue Hubspot::NothingToDoError = _
149
+ puts "Nothing changed, cancelled api call"
150
+ rescue Hubspot::RequestError => e
151
+ puts "API Error: #{e.message}"
152
+ end
153
+ ```
154
+
139
155
  Example using `update`:
140
156
 
141
157
  ```ruby
@@ -274,6 +290,26 @@ end
274
290
  - **lte**: Less than or equal to.
275
291
  - **IN**: Matches any of the values in an array.
276
292
 
293
+ #### Searching for empty values (NOT_HAS_PROPERTY)
294
+
295
+ Any empty value in your search will be matched using the correect filter in Hubspot
296
+
297
+ ```ruby
298
+ # Search for companies with no value for a given field
299
+ companies = Hubspot::Company.search(query: { client_category: nil }, properties: %w[name number_of_employees])
300
+ # Request body: {"filterGroups":[{"filters":[{"propertyName":"client_category","operator":"NOT_HAS_PROPERTY"}]}]
301
+
302
+ puts "Searching for uncategorised customers"
303
+ puts ""
304
+
305
+ companies.each do |company|
306
+ category = company_category_by_size(company.number_of_employees)
307
+ company.update(client_category: category)
308
+
309
+ puts " Found: #{company.name} (size #{company.number_of_employees}) - filed under #{category}"
310
+ end
311
+ ```
312
+
277
313
  #### Specifying Properties in Search
278
314
 
279
315
  When performing a search, you can also specify which properties to return.
data/lib/hubspot/batch.rb CHANGED
@@ -31,6 +31,7 @@ module Hubspot
31
31
  CONTACT_LIMIT = 10
32
32
  DEFAULT_LIMIT = 100
33
33
 
34
+ # :nocov:
34
35
  def inspect
35
36
  "#<#{self.class.name} " \
36
37
  "@resource_count=#{@resources.size}, " \
@@ -38,16 +39,11 @@ module Hubspot
38
39
  "@resource_type=#{@resources.first&.resource_name}, " \
39
40
  "@responses_count=#{@responses.size}>"
40
41
  end
42
+ # :nocov:
41
43
 
42
44
  # rubocop:disable Lint/MissingSuper
43
45
  def initialize(resources = [], id_property: 'id', resource_matcher: nil)
44
- if resource_matcher
45
- unless resource_matcher.is_a?(Proc) && resource_matcher.arity == 2
46
- raise ArgumentError, 'resource_matcher must be a proc that accepts exactly 2 arguments'
47
- end
48
-
49
- @resource_matcher = resource_matcher
50
- end
46
+ validate_resource_matcher(resource_matcher)
51
47
 
52
48
  @resources = []
53
49
  @id_property = id_property # Set id_property for the batch (default: 'id')
@@ -67,11 +63,23 @@ module Hubspot
67
63
  end
68
64
 
69
65
  # Upsert method that calls save with upsert action
70
- def upsert
66
+ def upsert(resource_matcher: nil)
67
+ validate_resource_matcher(resource_matcher)
68
+
71
69
  validate_upsert_conditions
72
70
  save(action: 'upsert')
73
71
  end
74
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
+
75
83
  # Archive method
76
84
  def archive
77
85
  save(action: 'archive')
@@ -93,15 +101,29 @@ module Hubspot
93
101
  end
94
102
 
95
103
  def add_resource(resource)
96
- if @resources.any? && @resources.first.resource_name != resource.resource_name
97
- 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)
98
110
  end
99
111
 
100
112
  @resources << resource
101
113
  end
102
114
 
115
+ def any_changes?
116
+ @resources.any?(&:changes?)
117
+ end
118
+
103
119
  private
104
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
+
105
127
  # rubocop:disable Metrics/MethodLength
106
128
  def save(action: 'update')
107
129
  @action = action
@@ -280,13 +302,15 @@ module Hubspot
280
302
  end
281
303
 
282
304
  class << self
283
- def read(object_class, object_ids = [], id_property: 'id')
305
+ def read(object_class, object_ids = [], properties: [], id_property: 'id')
284
306
  unless object_class < Hubspot::Resource
285
307
  raise ArgumentError, 'Must be a valid Hubspot resource class'
286
308
  end
287
309
 
288
310
  # fetch all the matching resources with paging handled
289
- 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
290
314
 
291
315
  # return instance of Hubspot::Batch with the resources set
292
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
@@ -19,6 +19,7 @@ module Hubspot
19
19
  class RateLimitExceededError < RequestError; end
20
20
  class NotConfiguredError < StandardError; end
21
21
  class ArgumentError < StandardError; end
22
+ class NothingToDoError < StandardError; end
22
23
 
23
24
  class << self
24
25
  def error_from_response(response)
data/lib/hubspot/form.rb CHANGED
@@ -4,11 +4,13 @@ module Hubspot
4
4
  class Form < Resource
5
5
  METADATA_FIELDS = %w[createdAt updatedAt archived].freeze
6
6
 
7
+ # :nocov:
7
8
  def inspect
8
9
  "#<#{self.class.name} " \
9
10
  "@name=#{name}, " \
10
11
  "@fieldGroups=#{respond_to?('fieldGroups') ? fieldGroups.size : '-'}>"
11
12
  end
13
+ # :nocov:
12
14
 
13
15
  class << self
14
16
  def api_root
@@ -18,9 +20,9 @@ module Hubspot
18
20
 
19
21
  private
20
22
 
21
- # Extract ID from data and leave as a string
22
- def extract_id(data)
23
- data.delete('id')
23
+ # dont convert (from string)
24
+ def extract_id(id)
25
+ id
24
26
  end
25
27
  end
26
28
  end
@@ -10,7 +10,7 @@ module Hubspot
10
10
 
11
11
  MAX_LIMIT = 100 # HubSpot max items per page
12
12
 
13
- # customised inspect
13
+ # :nocov:
14
14
  def inspect
15
15
  "#<#{self.class.name} " \
16
16
  "@url=#{@url.inspect}, " \
@@ -18,6 +18,7 @@ module Hubspot
18
18
  "@resource_class=#{@resource_class.inspect}, " \
19
19
  "@object_ids_count=#{@object_ids.size}>"
20
20
  end
21
+ # :nocov:
21
22
 
22
23
  # rubocop:disable Lint/MissingSuper
23
24
  def initialize(url:, params: {}, resource_class: nil, object_ids: [])
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require_relative './api_client'
4
5
  require_relative './exceptions'
5
6
 
@@ -45,12 +45,14 @@ module Hubspot
45
45
  # Find a resource by ID and return an instance of the class
46
46
  #
47
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
48
49
  #
49
50
  # Example:
50
51
  # contact = Hubspot::Contact.find(1)
52
+ # contact = Hubspot::Contact.find(1, properties: %w[email firstname lastname custom_field])
51
53
  #
52
54
  # Returns An instance of the resource.
53
- def find(id, properties = nil)
55
+ def find(id, properties: nil)
54
56
  all_properties = build_property_list(properties)
55
57
  if all_properties.is_a?(Array) && !all_properties.empty?
56
58
  params = { query: { properties: all_properties } }
@@ -181,8 +183,8 @@ module Hubspot
181
183
  # Hubspot::Contact.batch_read_all(hubspot_contact_ids)
182
184
  #
183
185
  # Returns [Hubspot::Batch] A batch of resources that can be operated on further
184
- def batch_read_all(object_ids = [], id_property: 'id')
185
- Hubspot::Batch.read(self, object_ids, id_property: id_property)
186
+ def batch_read_all(object_ids = [], properties: [], id_property: 'id')
187
+ Hubspot::Batch.read(self, object_ids, properties: properties, id_property: id_property)
186
188
  end
187
189
 
188
190
  # Retrieve the complete list of properties for this resource class
@@ -261,6 +263,8 @@ module Hubspot
261
263
  #
262
264
  # If no suffix is provided, the default comparison is equality (`EQ`).
263
265
  #
266
+ # If no value is provided, or is empty the NOT_HAS_PROPERTY operator will be used
267
+ #
264
268
  # query - [String, Hash] The query for searching. This can be either:
265
269
  # - A String: for full-text search.
266
270
  # - A Hash: where each key represents a property and may have suffixes for the comparison
@@ -354,9 +358,9 @@ module Hubspot
354
358
  filter_groups = [{ filters: [] }]
355
359
 
356
360
  filters.each do |key, value|
357
- filter = extract_property_and_operator(key)
361
+ filter = extract_property_and_operator(key, value)
358
362
  value_key = value.is_a?(Array) ? :values : :value
359
- filter[value_key] = value
363
+ filter[value_key] = value unless value.blank?
360
364
  filter_groups.first[:filters] << filter
361
365
  end
362
366
 
@@ -364,7 +368,9 @@ module Hubspot
364
368
  end
365
369
 
366
370
  # Extract property name and operator from the key
367
- def extract_property_and_operator(key)
371
+ def extract_property_and_operator(key, value)
372
+ return { propertyName: key.to_s, operator: 'NOT_HAS_PROPERTY' } if value.blank?
373
+
368
374
  OPERATOR_MAP.each do |suffix, hubspot_operator|
369
375
  if key.to_s.end_with?(suffix)
370
376
  return {
@@ -392,8 +398,13 @@ module Hubspot
392
398
 
393
399
  # Public: Initialize a resouce
394
400
  #
395
- # data - [2D Hash, nested Hash] data to initialise the resourse This can be either:
396
- # - A Simple 2D Hash, key value pairs of property => value (for the create option)
401
+ # data - [2D Hash, nested Hash] data to initialise:
402
+ # - The response from the api will be of the form:
403
+ # { id: <hs_object_id>, properties: { "email": "john@example.org" ... }, ... }
404
+ #
405
+ # - A Simple 2D Hash, key value pairs in the form:
406
+ # { email: 'john@example.org', firstname: 'John', lastname: 'Smith' }
407
+ #
397
408
  # - A structured hash consisting of { id: <hs_object_id>, properties: {}, ... }
398
409
  # This is the same structure as per the API, and can be rebuilt if you store the id
399
410
  # of the object against your own data
@@ -409,7 +420,7 @@ module Hubspot
409
420
  # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
410
421
  def initialize(data = {})
411
422
  data.transform_keys!(&:to_s)
412
- @id = extract_id(data)
423
+ @id = extract_id(data.delete('id'))
413
424
  @properties = {}
414
425
  @metadata = {}
415
426
  if @id
@@ -447,6 +458,12 @@ module Hubspot
447
458
  end
448
459
  end
449
460
 
461
+ def save!
462
+ raise NothingToDoError, 'Nothing to save' unless changes?
463
+
464
+ save
465
+ end
466
+
450
467
  # If the resource exists in Hubspot
451
468
  #
452
469
  # Returns Boolean
@@ -520,15 +537,8 @@ module Hubspot
520
537
  if method_name.end_with?('=')
521
538
  attribute = method_name.chomp('=')
522
539
  new_value = args.first
523
-
524
- # Track changes only if the value has actually changed
525
- if @properties[attribute] != new_value
526
- @changes[attribute] = new_value
527
- else
528
- @changes.delete(attribute) # Remove from changes if it reverts to the original value
529
- end
530
-
531
- return new_value
540
+ add_accessors attribute
541
+ return send("#{attribute}=", new_value)
532
542
  # Handle getters
533
543
  else
534
544
  return @changes[method_name] if @changes.key?(method_name)
@@ -547,35 +557,70 @@ module Hubspot
547
557
  @properties.key?(property_name) || @changes.key?(property_name) || super
548
558
  end
549
559
 
550
- private
551
-
552
- # Extract ID from data and convert to integer
553
- def extract_id(data)
554
- data['id'] ? data['id'].to_i : nil
555
- end
556
-
557
560
  # Initialize from API response, separating metadata from properties
558
561
  def initialize_from_api(data)
562
+ @changes = data.delete('changes')&.transform_keys!(&:to_s) || {}
563
+
559
564
  if data['properties']
560
565
  @metadata = data.reject { |key, _v| key == 'properties' }
561
566
  handle_properties(data['properties'])
562
567
  else
563
568
  handle_properties(data)
564
569
  end
570
+ end
565
571
 
566
- @changes = {}
572
+ private
573
+
574
+ # Extract ID from data and convert to integer
575
+ def extract_id(id)
576
+ id&.to_i
567
577
  end
568
578
 
569
579
  def handle_properties(properties_data)
570
- properties_data.each do |key, value|
571
- if METADATA_FIELDS.include?(key)
572
- @metadata[key] = value
580
+ properties_data.each do |attribute, value|
581
+ if metadata_field?(attribute)
582
+ @metadata[attribute.to_s] = value
583
+ else
584
+ add_accessors attribute.to_s
585
+ @properties[attribute.to_s] = value
586
+ end
587
+ end
588
+ end
589
+
590
+ def add_accessors(attribute)
591
+ add_accessors_setter(attribute)
592
+ add_accessors_getter(attribute)
593
+ end
594
+
595
+ def add_accessors_setter(attribute)
596
+ # Define the setter method
597
+ define_singleton_method("#{attribute}=") do |new_value|
598
+ # Track changes only if the value has actually changed
599
+ if @properties[attribute] != new_value
600
+ @changes[attribute] = new_value
573
601
  else
574
- @properties[key] = value
602
+ @changes.delete(attribute) # Remove from changes if it reverts to the original value
575
603
  end
604
+
605
+ new_value
606
+ end
607
+ end
608
+
609
+ def add_accessors_getter(attribute)
610
+ # Define the getter method
611
+ define_singleton_method(attribute) do
612
+ # Return from changes if available, else return from properties
613
+ return @changes[attribute] if @changes.key?(attribute)
614
+
615
+ @properties[attribute] if @properties.key?(attribute)
576
616
  end
577
617
  end
578
618
 
619
+ # allows overwriting in other resource classes
620
+ def metadata_field?(key)
621
+ METADATA_FIELDS.include?(key)
622
+ end
623
+
579
624
  # Initialize a new object (no API response)
580
625
  def initialize_new_object(data)
581
626
  @properties = {}
@@ -583,15 +628,12 @@ module Hubspot
583
628
  @metadata = {}
584
629
  end
585
630
 
586
- # Extract metadata from data, excluding properties
587
- def extract_metadata(data)
588
- data.reject { |key, _| key == 'properties' }
589
- end
590
-
591
631
  # Create a new resource
592
632
  def create_new
593
633
  created_resource = self.class.create(@changes)
594
634
  @id = created_resource.id
635
+ @properties.merge!(@changes)
636
+ @changes = {}
595
637
  @id ? true : false
596
638
  end
597
639
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.1'
5
5
  end
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.2
4
+ version: 0.3.1
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-03 00:00:00.000000000 Z
11
+ date: 2024-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -254,7 +254,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
254
  - !ruby/object:Gem::Version
255
255
  version: '0'
256
256
  requirements: []
257
- rubygems_version: 3.2.3
257
+ rubyforge_project:
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