Skip to content

Commit 945a871

Browse files
authored
Refactoring resource interface into Hubspot::Resource (#176)
* WIP: Refactoring resource interface into Hubspot::Resource * Add support for paged collections * Add recently created and modified to company * Add support for fetching and adding contacts to a company * Support removing contacts from a company * Add batch update to companies * Replace Company with the resource implementation * Update Contact2 with new methods * Add FactoryBot for generating fixtures * Rework new method and add tests for base resource class * Lots of updates and tests for Company and Contact * Fix delegate_missing_to to work with older activesupport versions * Add Contact search support * Add merge support for Contact * Replace old Contact with new Contact resource * Update methods to return success * Update the README with the new resource API
1 parent 8daebdb commit 945a871

79 files changed

Lines changed: 8156 additions & 3133 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ tmp
5252

5353
# Ignore local environment variables
5454
/.env
55+
56+
# Byebug history
57+
.byebug_history

README.md

Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,49 +108,159 @@ At this time, OAuth tokens are configured globally rather than on a per-connecti
108108

109109
## Usage
110110

111-
Here's what you can do for now:
111+
Classes have been created that map to Hubspot resource types and attempt to abstract away as much of the API specific details as possible. These classes generally follow the [ActiveRecord](https://en.wikipedia.org/wiki/Active_record_pattern) pattern and general Ruby conventions. Anyone familiar with [Ruby On Rails](https://rubyonrails.org/) should find this API closely maps with familiar concepts.
112112

113-
### Create a contact
113+
114+
### Creating a new resource
114115

115116
```ruby
116-
Hubspot::Contact.create!("email@address.com", {firstname: "First", lastname: "Last"})
117+
irb(main):001:0> company = Hubspot::Company.new(name: "My Company LLC.")
118+
=> #<Hubspot::Company:0x000055b9219cc068 @changes={"name"=>"My Company LLC."}, @properties={}, @id=nil, @persisted=false, @deleted=false>
119+
120+
irb(main):002:0> company.persisted?
121+
=> false
122+
123+
irb(main):003:0> company.save
124+
=> true
125+
126+
irb(main):004:0> company.persisted?
127+
=> true
117128
```
118129

119-
#### In batches
130+
```ruby
131+
irb(main):001:0> company = Hubspot::Company.create(name: "Second Financial LLC.")
132+
=> #<Hubspot::Company:0x0000557ea7119fb0 @changes={}, @properties={"hs_lastmodifieddate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceId"=>nil, "versions"=>[{"name"=>"hs_lastmodifieddate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "name"=>{"value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"name", "value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "createdate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"createdate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}, {"name"=>"createdate", "value"=>"1552234087467", "timestamp"=>1552234087467, "sourceId"=>"API", "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}}, @id=1726317857, @persisted=true, @deleted=false, @metadata={"portalId"=>62515, "companyId"=>1726317857, "isDeleted"=>false, "additionalDomains"=>[], "stateChanges"=>[], "mergeAudits"=>[]}>
133+
134+
irb(main):002:0> company.persisted?
135+
=> true
136+
```
137+
138+
139+
### Find an existing resource
140+
141+
**Note:** Hubspot uses a combination of different names for the "ID" property of a resource based on what type of resource it is (eg. vid for Contact). This library attempts to abstract that away and generalizes an `id` property for all resources
120142

121143
```ruby
122-
Hubspot::Contact.create_or_update!([{email: 'smith@example.com', firstname: 'First', lastname: 'Last'}])
144+
irb(main):001:0> company = Hubspot::Company.find(1726317857)
145+
=> #<Hubspot::Company:0x0000562e4988c9a8 @changes={}, @properties={"hs_lastmodifieddate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceId"=>nil, "versions"=>[{"name"=>"hs_lastmodifieddate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "name"=>{"value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"name", "value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "createdate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"createdate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}}, @id=1726317857, @persisted=true, @deleted=false, @metadata={"portalId"=>62515, "companyId"=>1726317857, "isDeleted"=>false, "additionalDomains"=>[], "stateChanges"=>[], "mergeAudits"=>[]}>
146+
147+
irb(main):002:0> company = Hubspot::Company.find(1)
148+
Traceback (most recent call last):
149+
6: from /home/chris/projects/hubspot-ruby/bin/console:20:in `<main>'
150+
5: from (irb):2
151+
4: from /home/chris/projects/hubspot-ruby/lib/hubspot/resource.rb:17:in `find'
152+
3: from /home/chris/projects/hubspot-ruby/lib/hubspot/resource.rb:81:in `reload'
153+
2: from /home/chris/projects/hubspot-ruby/lib/hubspot/connection.rb:10:in `get_json'
154+
1: from /home/chris/projects/hubspot-ruby/lib/hubspot/connection.rb:52:in `handle_response'
155+
Hubspot::RequestError (Response body: {"status":"error","message":"resource not found","correlationId":"7c8ba50e-16a4-4a52-a304-ff249175a8f1","requestId":"b4898274bf8992924082b4a460b90cbe"})
123156
```
124157
125-
### Find a contact
126158
127-
These methods will return a `Hubspot::Contact` object if successful, `nil` otherwise:
159+
### Updating resource properties
160+
161+
```ruby
162+
irb(main):001:0> company = Hubspot::Company.find(1726317857)
163+
=> #<Hubspot::Company:0x0000563b9f3ee230 @changes={}, @properties={"hs_lastmodifieddate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceId"=>nil, "versions"=>[{"name"=>"hs_lastmodifieddate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"CALCULATED", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "name"=>{"value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"name", "value"=>"Second Financial LLC.", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}, "createdate"=>{"value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceId"=>nil, "versions"=>[{"name"=>"createdate", "value"=>"1552234087467", "timestamp"=>1552234087467, "source"=>"API", "sourceVid"=>[], "requestId"=>"fd45773b-30d0-4d9d-b3b8-a85e01534e46"}]}}, @id=1726317857, @persisted=true, @deleted=false, @metadata={"portalId"=>62515, "companyId"=>1726317857, "isDeleted"=>false, "additionalDomains"=>[], "stateChanges"=>[], "mergeAudits"=>[]}>
164+
165+
irb(main):002:0> company.name
166+
=> "Second Financial LLC."
167+
168+
irb(main):003:0> company.name = "Third Financial LLC."
169+
=> "Third Financial LLC."
170+
171+
irb(main):004:0> company.changed?
172+
=> true
173+
174+
irb(main):005:0> company.changes
175+
=> {:name=>"Third Financial LLC."}
176+
177+
irb(main):006:0> company.save
178+
=> true
179+
180+
irb(main):007:0> company.changed?
181+
=> false
182+
183+
irb(main):008:0> company.changes
184+
=> {}
185+
```
186+
187+
**Note:** Unlike ActiveRecord in Rails, in some cases not all properties of a resource are known. If these properties are not returned by the API then they will not have a getter method defined for them until they've been set first. This may change in the future to improve the user experience and different methods are being tested.
128188

129189
```ruby
130-
Hubspot::Contact.find_by_email("email@address.com")
131-
Hubspot::Contact.find_by_id(12345) # Pass the contact VID
190+
irb(main):001:0> company = Hubspot::Company.new
191+
=> #<Hubspot::Company:0x0000561d0a8bdff8 @changes={}, @properties={}, @id=nil, @persisted=false, @deleted=false>
192+
193+
irb(main):002:0> company.name
194+
Traceback (most recent call last):
195+
3: from /home/chris/projects/hubspot-ruby/bin/console:20:in `<main>'
196+
2: from (irb):2
197+
1: from /home/chris/projects/hubspot-ruby/lib/hubspot/resource.rb:215:in `method_missing'
198+
NoMethodError (undefined method `name' for #<Hubspot::Company:0x0000561d0a8bdff8>)
199+
200+
irb(main):003:0> company.name = "Foobar"
201+
=> "Foobar"
202+
203+
irb(main):004:0> company.name
204+
=> "Foobar"
132205
```
133206

134-
### Update a contact
207+
### Collections
135208

136-
Given an instance of `Hubspot::Contact`, update its attributes with:
209+
To make working with API endpoints that return multiple resources easier, the returned instances will be wrapped in a collection instance. Just like in Rails, the collection instance provides helper methods for limiting the number of returned resources, paging through the results, and handles passing the options each time a new API call is made. The collection exposes all Ruby Array methods so you can use things like `size()`, `first()`, `last()`, and `map()`.
137210

138211
```ruby
139-
contact.update!({firstname: "First", lastname: "Last"})
212+
irb(main):001:0> contacts = Hubspot::Contact.all
213+
=> #<Hubspot::PagedCollection:0x000055ba3c2b55d8 @limit_param="limit", @limit=25, @offset_param="offset", @offset=nil, @options={}, @fetch_proc=#<Proc:0x000055ba3c2b5538@/home/chris/projects/hubspot-ruby/lib/hubspot/contact.rb:18>, @resources=[...snip...], @next_offset=9242374, @has_more=true>
214+
215+
irb(main):002:0> contacts.more?
216+
=> true
217+
218+
irb(main):003:0> contacts.next_offset
219+
=> 9242374
220+
221+
irb(main):004:0> contacts.size
222+
=> 25
223+
224+
irb(main):005:0> contacts.first
225+
=> #<Hubspot::Contact:0x000055ba3c29bac0 @changes={}, @properties={"firstname"=>{"value"=>"My Street X 1551971239 => My Street X 1551971267 => My Street X 1551971279"}, "lastmodifieddate"=>{"value"=>"1551971286841"}, "company"=>{"value"=>"MadKudu"}, "lastname"=>{"value"=>"Test0830181615"}}, @id=9153674, @persisted=true, @deleted=false, @metadata={"addedAt"=>1535664601481, "vid"=>9153674, "canonical-vid"=>9153674, "merged-vids"=>[], "portal-id"=>62515, "is-contact"=>true, "profile-token"=>"AO_T-mPNHk6O7jh8u8D2IlrhZn7GO91w-weZrC93_UaJvdB0U4o6Uc_PkPJ3DOpf15sUplrxMzG9weiTTpPI05Nr04zxnaNYBVcWHOlMbVlJ2Avq1KGoCBVbIoQucOy_YmCBIfOXRtcc", "profile-url"=>"https://app.hubspot.com/contacts/62515/contact/9153674", "form-submissions"=>[], "identity-profiles"=>[{"vid"=>9153674, "saved-at-timestamp"=>1535664601272, "deleted-changed-timestamp"=>0, "identities"=>[{"type"=>"EMAIL", "value"=>"test.0830181615@test.com", "timestamp"=>1535664601151, "is-primary"=>true}, {"type"=>"LEAD_GUID", "value"=>"01a107c4-3872-44e0-ab2e-47061507ffa1", "timestamp"=>1535664601259}]}], "merge-audits"=>[]}>
226+
227+
irb(main):006:0> contacts.next_page
228+
=> #<Hubspot::PagedCollection:0x000055ba3c2b55d8 @limit_param="limit", @limit=25, @offset_param="offset", @offset=9242374, @options={}, @fetch_proc=#<Proc:0x000055ba3c2b5538@/home/chris/projects/hubspot-ruby/lib/hubspot/contact.rb:18>, @resources=[...snip...], @next_offset=9324874, @has_more=true>
140229
```
141230

142-
#### In batches
231+
For Hubspot resources that support batch updates for updating multiple resources, the collection provides an `update_all()` method:
143232

144233
```ruby
145-
Hubspot::Contact.create_or_update!([{vid: '12345', firstname: 'First', lastname: 'Last'}])
234+
irb(main):001:0> companies = Hubspot::Company.all(limit: 5)
235+
=> #<Hubspot::PagedCollection:0x000055d5314fe0c8 @limit_param="limit", @limit=5, @offset_param="offset", @offset=nil, @options={}, @fetch_proc=#<Proc:0x000055d5314fe028@/home/chris/projects/hubspot-ruby/lib/hubspot/company.rb:21>, @resources=[...snip...], @next_offset=116011506, @has_more=true>
236+
237+
irb(main):002:0> companies.size
238+
=> 5
239+
240+
irb(main):003:0> companies.update_all(lifecyclestage: "opportunity")
241+
=> true
242+
243+
irb(main):004:0> companies.refresh
244+
=> #<Hubspot::PagedCollection:0x000055d5314fe0c8 @limit_param="limit", @limit=5, @offset_param="offset", @offset=nil, @options={}, @fetch_proc=#<Proc:0x000055d5314fe028@/home/chris/projects/hubspot-ruby/lib/hubspot/company.rb:21>, @resources=[...snip...], @next_offset=116011506, @has_more=true>
146245
```
147246

148-
### Create a deal
247+
### Deleting a resource
149248

150249
```ruby
151-
Hubspot::Deal.create!(nil, [company.vid], [contact.vid], pipeline: 'default', dealstage: 'initial_contact')
250+
irb(main):001:0> contact = Hubspot::Contact.find(9324874)
251+
=> #<Hubspot::Contact:0x000055a87c87aee0 ...snip... >
252+
253+
irb(main):002:0> contact.delete
254+
=> true
152255
```
153256

257+
## Resource types
258+
259+
**Note:** These are the currently defined classes the support the new resource API. This list will continue to grow as we update other classes. All existing classes will be updated prior to releasing v1.0.
260+
261+
* Contact -> Hubspot::Contact
262+
* Company -> Hubspot::Company
263+
154264
## Contributing to hubspot-ruby
155265

156266
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.

bin/console

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env ruby
2+
3+
require "bundler/setup"
4+
require "hubspot-ruby"
5+
6+
# Include Byebug for debugging
7+
require "byebug"
8+
9+
# You can add fixtures and/or initialization code here to make experimenting
10+
# with your gem easier. You can also use a different console, if you like.
11+
12+
# (If you use this, don't forget to add pry to your Gemfile!)
13+
# require "pry"
14+
# Pry.start
15+
16+
# Configure with the demo key by default
17+
Hubspot.configure(hapikey: 'demo')
18+
19+
require "irb"
20+
IRB.start(__FILE__)

gemfiles/activesupport_4.2.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file was generated by Appraisal
22

3-
source "http://rubygems.org"
3+
source "https://rubygems.org"
44

55
gem "activesupport", "~> 4.2.2"
66

gemfiles/activesupport_5.0.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file was generated by Appraisal
22

3-
source "http://rubygems.org"
3+
source "https://rubygems.org"
44

55
gem "activesupport", "~> 5.0.0"
66

gemfiles/activesupport_5.1.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file was generated by Appraisal
22

3-
source "http://rubygems.org"
3+
source "https://rubygems.org"
44

55
gem "activesupport", "~> 5.1.0"
66

gemfiles/activesupport_5.2.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file was generated by Appraisal
22

3-
source "http://rubygems.org"
3+
source "https://rubygems.org"
44

55
gem "activesupport", "~> 5.2.2"
66

hubspot-ruby.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ Gem::Specification.new do |s|
3434
s.add_development_dependency("awesome_print")
3535
s.add_development_dependency("timecop")
3636
s.add_development_dependency("guard-rspec")
37+
s.add_development_dependency("byebug")
38+
s.add_development_dependency("faker")
39+
s.add_development_dependency("factory_bot")
3740
end
3841

lib/hubspot-ruby.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
require 'active_support/core_ext'
33
require 'httparty'
44
require 'hubspot/exceptions'
5+
require 'hubspot/resource'
6+
require 'hubspot/collection'
7+
require 'hubspot/paged_collection'
58
require 'hubspot/properties'
69
require 'hubspot/company'
710
require 'hubspot/company_properties'

lib/hubspot/collection.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class Hubspot::Collection
2+
def initialize(opts = {}, &block)
3+
@options = opts
4+
@fetch_proc = block
5+
fetch
6+
end
7+
8+
def refresh
9+
fetch
10+
self
11+
end
12+
13+
def resources
14+
@resources
15+
end
16+
17+
def update_all(opts = {})
18+
return true if empty?
19+
20+
# This assumes that all resources are the same type
21+
resource_class = resources.first.class
22+
unless resource_class.respond_to?(:batch_update)
23+
raise "#{resource_class} does not support bulk update"
24+
end
25+
26+
resource_class.batch_update(resources, opts)
27+
end
28+
29+
protected
30+
def fetch
31+
@resources = @fetch_proc.call(@options)
32+
end
33+
34+
def respond_to_missing?(name, include_private = false)
35+
@resources.respond_to?(name, include_private)
36+
end
37+
38+
def method_missing(method, *args, &block)
39+
@resources.public_send(method, *args, &block)
40+
end
41+
end

0 commit comments

Comments
 (0)