hubspot-api-ruby 0.8.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +9 -0
  5. data/LICENSE.txt +18 -0
  6. data/README.md +295 -0
  7. data/RELEASING.md +6 -0
  8. data/Rakefile +32 -0
  9. data/hubspot-api-ruby.gemspec +42 -0
  10. data/lib/hubspot-api-ruby.rb +39 -0
  11. data/lib/hubspot/blog.rb +98 -0
  12. data/lib/hubspot/collection.rb +41 -0
  13. data/lib/hubspot/company.rb +160 -0
  14. data/lib/hubspot/company_properties.rb +59 -0
  15. data/lib/hubspot/config.rb +63 -0
  16. data/lib/hubspot/connection.rb +152 -0
  17. data/lib/hubspot/contact.rb +110 -0
  18. data/lib/hubspot/contact_list.rb +129 -0
  19. data/lib/hubspot/contact_properties.rb +59 -0
  20. data/lib/hubspot/deal.rb +173 -0
  21. data/lib/hubspot/deal_pipeline.rb +58 -0
  22. data/lib/hubspot/deal_properties.rb +59 -0
  23. data/lib/hubspot/deprecator.rb +7 -0
  24. data/lib/hubspot/engagement.rb +222 -0
  25. data/lib/hubspot/event.rb +21 -0
  26. data/lib/hubspot/exceptions.rb +18 -0
  27. data/lib/hubspot/file.rb +38 -0
  28. data/lib/hubspot/form.rb +95 -0
  29. data/lib/hubspot/oauth.rb +50 -0
  30. data/lib/hubspot/owner.rb +57 -0
  31. data/lib/hubspot/paged_collection.rb +35 -0
  32. data/lib/hubspot/properties.rb +123 -0
  33. data/lib/hubspot/railtie.rb +10 -0
  34. data/lib/hubspot/resource.rb +270 -0
  35. data/lib/hubspot/subscription.rb +37 -0
  36. data/lib/hubspot/topic.rb +37 -0
  37. data/lib/hubspot/utils.rb +127 -0
  38. data/lib/tasks/hubspot.rake +53 -0
  39. data/spec/factories/companies.rb +9 -0
  40. data/spec/factories/contacts.rb +10 -0
  41. data/spec/lib/hubspot-ruby_spec.rb +12 -0
  42. data/spec/lib/hubspot/blog_spec.rb +150 -0
  43. data/spec/lib/hubspot/company_properties_spec.rb +410 -0
  44. data/spec/lib/hubspot/company_spec.rb +340 -0
  45. data/spec/lib/hubspot/config_spec.rb +87 -0
  46. data/spec/lib/hubspot/connection_spec.rb +214 -0
  47. data/spec/lib/hubspot/contact_list_spec.rb +301 -0
  48. data/spec/lib/hubspot/contact_properties_spec.rb +245 -0
  49. data/spec/lib/hubspot/contact_spec.rb +223 -0
  50. data/spec/lib/hubspot/deal_pipeline_spec.rb +85 -0
  51. data/spec/lib/hubspot/deal_properties_spec.rb +262 -0
  52. data/spec/lib/hubspot/deal_spec.rb +185 -0
  53. data/spec/lib/hubspot/deprecator_spec.rb +15 -0
  54. data/spec/lib/hubspot/engagement_spec.rb +177 -0
  55. data/spec/lib/hubspot/event_spec.rb +33 -0
  56. data/spec/lib/hubspot/file_spec.rb +38 -0
  57. data/spec/lib/hubspot/form_spec.rb +189 -0
  58. data/spec/lib/hubspot/owner_spec.rb +56 -0
  59. data/spec/lib/hubspot/properties_spec.rb +45 -0
  60. data/spec/lib/hubspot/resource_spec.rb +54 -0
  61. data/spec/lib/hubspot/topic_spec.rb +23 -0
  62. data/spec/lib/hubspot/utils_spec.rb +164 -0
  63. data/spec/lib/tasks/hubspot_spec.rb +119 -0
  64. data/spec/shared_examples/saveable_resource.rb +45 -0
  65. data/spec/shared_examples/updateable_resource.rb +87 -0
  66. data/spec/spec_helper.rb +44 -0
  67. data/spec/support/capture_output.rb +21 -0
  68. data/spec/support/cassette_helper.rb +19 -0
  69. data/spec/support/hubspot_api_helpers.rb +13 -0
  70. data/spec/support/rake.rb +46 -0
  71. data/spec/support/tests_helper.rb +17 -0
  72. data/spec/support/vcr.rb +16 -0
  73. metadata +369 -0
@@ -0,0 +1,50 @@
1
+ require 'httparty'
2
+
3
+ module Hubspot
4
+ class OAuth < Connection
5
+ include HTTParty
6
+
7
+ DEFAULT_OAUTH_HEADERS = {"Content-Type" => "application/x-www-form-urlencoded;charset=utf-8"}
8
+
9
+ class << self
10
+ def refresh(token, params={}, options={})
11
+ oauth_post(token_url, { grant_type: "refresh_token", refresh_token: token }.merge(params),
12
+ options)
13
+ end
14
+
15
+ def create(code, params={}, options={})
16
+ oauth_post(token_url, { grant_type: "authorization_code", code: code }.merge(params),
17
+ options)
18
+ end
19
+
20
+ def authorize_url(scopes, params={})
21
+ client_id = params[:client_id] || Hubspot::Config.client_id
22
+ redirect_uri = params[:redirect_uri] || Hubspot::Config.redirect_uri
23
+ scopes = Array.wrap(scopes)
24
+
25
+ "https://app.hubspot.com/oauth/authorize?client_id=#{client_id}&scope=#{scopes.join("%20")}&redirect_uri=#{redirect_uri}"
26
+ end
27
+
28
+ def token_url
29
+ token_url = Hubspot::Config.base_url + "/oauth/v1/token"
30
+ end
31
+
32
+ def oauth_post(url, params, options={})
33
+ no_parse = options[:no_parse] || false
34
+
35
+ body = {
36
+ client_id: Hubspot::Config.client_id,
37
+ client_secret: Hubspot::Config.client_secret,
38
+ redirect_uri: Hubspot::Config.redirect_uri,
39
+ }.merge(params)
40
+
41
+ response = post(url, body: body, headers: DEFAULT_OAUTH_HEADERS)
42
+ log_request_and_response url, response, body
43
+
44
+ raise(Hubspot::RequestError.new(response)) unless response.success?
45
+
46
+ no_parse ? response : response.parsed_response
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ module Hubspot
2
+ #
3
+ # HubSpot Owners API
4
+ #
5
+ # {http://developers.hubspot.com/docs/methods/owners/get_owners}
6
+ #
7
+ # TODO: Create an Owner
8
+ # TODO: Update an Owner
9
+ # TODO: Delete an Owner
10
+ class Owner
11
+ GET_OWNER_PATH = '/owners/v2/owners/:owner_id' # GET
12
+ GET_OWNERS_PATH = '/owners/v2/owners' # GET
13
+ CREATE_OWNER_PATH = '/owners/v2/owners' # POST
14
+ UPDATE_OWNER_PATH = '/owners/v2/owners/:owner_id' # PUT
15
+ DELETE_OWNER_PATH = '/owners/v2/owners/:owner_id' # DELETE
16
+
17
+
18
+ attr_reader :properties, :owner_id, :email
19
+
20
+ def initialize(property_hash)
21
+ @properties = property_hash
22
+ @owner_id = @properties['ownerId']
23
+ @email = @properties['email']
24
+ end
25
+
26
+ def [](property)
27
+ @properties[property]
28
+ end
29
+
30
+ class << self
31
+ def all(include_inactive=false)
32
+ path = GET_OWNERS_PATH
33
+ params = { includeInactive: include_inactive }
34
+ response = Hubspot::Connection.get_json(path, params)
35
+ response.map { |r| new(r) }
36
+ end
37
+
38
+ def find(id, include_inactive=false)
39
+ path = GET_OWNER_PATH
40
+ response = Hubspot::Connection.get_json(path, owner_id: id,
41
+ include_inactive: include_inactive)
42
+ new(response)
43
+ end
44
+
45
+ def find_by_email(email, include_inactive=false)
46
+ path = GET_OWNERS_PATH
47
+ params = { email: email, includeInactive: include_inactive }
48
+ response = Hubspot::Connection.get_json(path, params)
49
+ response.blank? ? nil : new(response.first)
50
+ end
51
+
52
+ def find_by_emails(emails, include_inactive=false)
53
+ emails.map { |email| find_by_email(email, include_inactive) }.reject(&:blank?)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ class Hubspot::PagedCollection < Hubspot::Collection
2
+ attr_accessor :offset, :limit
3
+
4
+ def initialize(opts = {}, &block)
5
+ @limit_param = opts.delete(:limit_param) || "limit"
6
+ @limit = opts.delete(:limit) || 25
7
+ @offset_param = opts.delete(:offset_param) || "offset"
8
+ @offset = opts.delete(:offset)
9
+
10
+ super(opts, &block)
11
+ end
12
+
13
+ def more?
14
+ @has_more
15
+ end
16
+
17
+ def next_offset
18
+ @next_offset
19
+ end
20
+
21
+ def next_page?
22
+ @has_more
23
+ end
24
+
25
+ def next_page
26
+ @offset = next_offset
27
+ fetch
28
+ self
29
+ end
30
+
31
+ protected
32
+ def fetch
33
+ @resources, @next_offset, @has_more = @fetch_proc.call(@options, @offset, @limit)
34
+ end
35
+ end
@@ -0,0 +1,123 @@
1
+ module Hubspot
2
+ class Properties
3
+
4
+ PROPERTY_SPECS = {
5
+ group_field_names: %w(name displayName displayOrder properties),
6
+ field_names: %w(name groupName description fieldType formField type displayOrder label options showCurrencySymbol),
7
+ valid_field_types: %w(textarea select text date file number radio checkbox booleancheckbox),
8
+ valid_types: %w(string number bool date datetime enumeration),
9
+ options: %w(description value label hidden displayOrder)
10
+ }
11
+ DEFAULT_PROPERTY = 'email'
12
+
13
+ class << self
14
+ # TODO: properties can be set as configuration
15
+ # TODO: find the way how to set a list of Properties + merge same property key if present from opts
16
+ def add_default_parameters(opts={})
17
+ if opts.keys.map(&:to_s).include? 'property'
18
+ opts
19
+ else
20
+ opts.merge(property: DEFAULT_PROPERTY)
21
+ end
22
+ end
23
+
24
+ def all(path, opts={}, filter={})
25
+ response = Hubspot::Connection.get_json(path, opts)
26
+ filter_results(response, :groupName, filter[:include], filter[:exclude])
27
+ end
28
+
29
+ def groups(path, opts={}, filter={})
30
+ response = Hubspot::Connection.get_json(path, opts)
31
+ filter_results(response, :name, filter[:include], filter[:exclude])
32
+ end
33
+
34
+ def create!(path, params={})
35
+ post_data = valid_property_params(params)
36
+ return nil if post_data.blank?
37
+ Hubspot::Connection.post_json(path, params: {}, body: post_data)
38
+ end
39
+
40
+ def update!(path, property_name, params={})
41
+ post_data = valid_property_params(params)
42
+ return nil if post_data.blank?
43
+ Hubspot::Connection.put_json(path, params: { property_name: property_name }, body: post_data)
44
+ end
45
+
46
+ def delete!(path, property_name)
47
+ response = Hubspot::Connection.delete_json(path, property_name: property_name)
48
+ response.parsed_response
49
+ end
50
+
51
+ def create_group!(path, params={})
52
+ post_data = valid_group_params(params)
53
+ return nil if post_data.blank?
54
+ Hubspot::Connection.post_json(path, params: {}, body: post_data)
55
+ end
56
+
57
+ def update_group!(path, group_name, params={})
58
+ post_data = valid_group_params(params)
59
+ return nil if post_data.blank?
60
+ Hubspot::Connection.put_json(path, params: { group_name: group_name }, body: post_data)
61
+ end
62
+
63
+ def delete_group!(path, group_name)
64
+ response = Hubspot::Connection.delete_json(path, group_name: group_name)
65
+ response.parsed_response
66
+ end
67
+
68
+ def same?(src, dst)
69
+ src_params = valid_params(src)
70
+ dst_params = valid_params(dst)
71
+ src_params.eql?(dst_params)
72
+ # hash_same?(src_params, dst_params)
73
+ end
74
+
75
+ def valid_params(params={})
76
+ valid_property_params(params)
77
+ end
78
+
79
+ private
80
+
81
+ def filter_results(results, key, include, exclude)
82
+ key = key.to_s
83
+ results.select { |result|
84
+ (include.blank? || include.include?(result[key])) &&
85
+ (exclude.blank? || !exclude.include?(result[key]))
86
+ }
87
+ end
88
+
89
+ def valid_property_params(params)
90
+ return {} if params.blank?
91
+ result = params.slice(*PROPERTY_SPECS[:field_names])
92
+ result.delete('fieldType') unless check_field_type(result['fieldType'])
93
+ result.delete('type') unless check_type(result['type'])
94
+ result['options'] = valid_option_params(result['options'])
95
+ result
96
+ end
97
+
98
+ def valid_group_params(params)
99
+ return {} if params.blank?
100
+ result = params.slice(*PROPERTY_SPECS[:group_field_names])
101
+ result['properties'] = valid_property_params(result['properties']) unless result['properties'].blank?
102
+ result
103
+ end
104
+
105
+ def check_field_type(val)
106
+ return true if PROPERTY_SPECS[:valid_field_types].include?(val)
107
+ puts "Invalid field type: #{val}"
108
+ false
109
+ end
110
+
111
+ def check_type(val)
112
+ return true if PROPERTY_SPECS[:valid_types].include?(val)
113
+ puts "Invalid type: #{val}"
114
+ false
115
+ end
116
+
117
+ def valid_option_params(options)
118
+ return [] if options.blank?
119
+ options.map { |o| o.slice(*PROPERTY_SPECS[:options]) }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,10 @@
1
+ require 'hubspot-api-ruby'
2
+ require 'rails'
3
+ module Hubspot
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ spec = Gem::Specification.find_by_name('hubspot-api-ruby')
7
+ load "#{spec.gem_dir}/lib/tasks/hubspot.rake"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,270 @@
1
+ module Hubspot
2
+ class Resource
3
+
4
+ class_attribute :id_field, instance_writer: false
5
+ class_attribute :property_name_field, instance_writer: false
6
+ class_attribute :update_method, instance_writer: false
7
+
8
+ self.id_field = "id"
9
+ self.property_name_field = "property"
10
+ self.update_method = "put"
11
+
12
+ class << self
13
+ def from_result(result)
14
+ resource = new(result[id_field])
15
+ resource.send(:initialize_from, result.with_indifferent_access)
16
+ resource
17
+ end
18
+
19
+ def find(id)
20
+ instance = new(id)
21
+ instance.reload
22
+ end
23
+
24
+ def create(properties = {})
25
+ request = {
26
+ properties: Hubspot::Utils.hash_to_properties(properties.stringify_keys, key_name: property_name_field)
27
+ }
28
+ response = Hubspot::Connection.post_json(create_path, params: {}, body: request)
29
+ from_result(response)
30
+ end
31
+
32
+ def update(id, properties = {})
33
+ begin
34
+ update!(id, properties)
35
+ rescue Hubspot::RequestError => e
36
+ false
37
+ end
38
+ end
39
+
40
+ def update!(id, properties = {})
41
+ request = {
42
+ properties: Hubspot::Utils.hash_to_properties(properties.stringify_keys, key_name: property_name_field)
43
+ }
44
+
45
+ if update_method == "put"
46
+ response = Hubspot::Connection.put_json(update_path, params: { id: id, no_parse: true }, body: request)
47
+ else
48
+ response = Hubspot::Connection.post_json(update_path, params: { id: id, no_parse: true }, body: request)
49
+ end
50
+
51
+ response.success?
52
+ end
53
+ end
54
+
55
+ def initialize(id_or_properties = nil)
56
+ @changes = HashWithIndifferentAccess.new
57
+ @properties = HashWithIndifferentAccess.new
58
+
59
+ if id_or_properties.is_a?(Integer) || id_or_properties.nil?
60
+ @id = id_or_properties
61
+ elsif id_or_properties.is_a?(Hash)
62
+ @id = id_or_properties.delete(id_field) || id_or_properties.delete(:id)
63
+
64
+ add_accessors(id_or_properties.keys)
65
+ id_or_properties.each do |k, v|
66
+ send "#{k}=", v
67
+ end
68
+ else
69
+ raise InvalidParams.new("#{self.class.name} must be initialized with an ID, hash, or nil")
70
+ end
71
+
72
+ @persisted = @id.present?
73
+ @deleted = false
74
+ end
75
+
76
+ def id
77
+ @id
78
+ end
79
+
80
+ def id=(id)
81
+ @id = id
82
+ end
83
+
84
+ def to_i
85
+ @id
86
+ end
87
+
88
+ def metadata
89
+ @metadata
90
+ end
91
+
92
+ def changes
93
+ @changes
94
+ end
95
+
96
+ def changed?
97
+ !@changes.empty?
98
+ end
99
+
100
+ def [](name)
101
+ @changes[name] || @properties[name]
102
+ end
103
+
104
+ def reload
105
+ raise(Hubspot::InvalidParams.new("Resource must have an ID")) if @id.nil?
106
+
107
+ response = Hubspot::Connection.get_json(find_path, id: @id)
108
+ initialize_from(response.with_indifferent_access)
109
+
110
+ self
111
+ end
112
+
113
+ def persisted?
114
+ @persisted
115
+ end
116
+
117
+ def save
118
+ request = {
119
+ properties: Hubspot::Utils.hash_to_properties(@changes.stringify_keys, key_name: property_name_field)
120
+ }
121
+
122
+ if persisted?
123
+ if update_method == "put"
124
+ response = Hubspot::Connection.put_json(update_path, params: { id: @id }, body: request)
125
+ else
126
+ response = Hubspot::Connection.post_json(update_path, params: { id: @id }, body: request)
127
+ end
128
+
129
+ update_from_changes
130
+ else
131
+ response = Hubspot::Connection.post_json(create_path, params: {}, body: request)
132
+
133
+ # Grab the new ID from the response
134
+ @id = response[id_field]
135
+
136
+ # Update the fields with the response
137
+ initialize_from(response.with_indifferent_access)
138
+ end
139
+
140
+ @persisted = true
141
+ true
142
+ end
143
+
144
+ def update(properties)
145
+ if properties && !properties.is_a?(Hash)
146
+ raise ArgumentError, "When assigning properties, you must pass a hash as an argument."
147
+ end
148
+
149
+ @changes = @changes.merge(properties)
150
+ save
151
+ end
152
+
153
+ def delete
154
+ raise(Hubspot::InvalidParams.new("Resource must have an ID")) if @id.nil?
155
+
156
+ Hubspot::Connection.delete_json(delete_path, id: @id)
157
+
158
+ @deleted = true
159
+ @changes = HashWithIndifferentAccess.new
160
+ true
161
+ end
162
+
163
+ def deleted?
164
+ @deleted
165
+ end
166
+
167
+ protected
168
+
169
+ def self.create_path
170
+ begin
171
+ self::CREATE_PATH
172
+ rescue NameError
173
+ raise "CREATE_PATH not defined for #{self.class.name}"
174
+ end
175
+ end
176
+
177
+ def create_path
178
+ self.class.create_path
179
+ end
180
+
181
+ def self.find_path
182
+ begin
183
+ self::FIND_PATH
184
+ rescue NameError
185
+ raise "FIND_PATH not defined for #{self.class.name}"
186
+ end
187
+ end
188
+
189
+ def find_path
190
+ self.class.find_path
191
+ end
192
+
193
+ def self.update_path
194
+ begin
195
+ self::UPDATE_PATH
196
+ rescue NameError
197
+ raise "UPDATE_PATH not defined for #{self.class.name}"
198
+ end
199
+ end
200
+
201
+ def update_path
202
+ self.class.update_path
203
+ end
204
+
205
+ def self.delete_path
206
+ begin
207
+ self::DELETE_PATH
208
+ rescue NameError
209
+ raise "CREATE_PATH not defined for #{self.class.name}"
210
+ end
211
+ end
212
+
213
+ def delete_path
214
+ self.class.delete_path
215
+ end
216
+
217
+ def initialize_from(response)
218
+ @properties = response["properties"] || HashWithIndifferentAccess.new
219
+ @metadata = response.except "properties"
220
+
221
+ add_accessors(@properties.keys)
222
+
223
+ # Clear any changes
224
+ @changes = HashWithIndifferentAccess.new
225
+ end
226
+
227
+ def update_from_changes
228
+ @changes.each do |k, v|
229
+ @properties[k] ||= {}
230
+ @properties[k]["value"] = v
231
+ end
232
+
233
+ # Clear any changes
234
+ @changes = HashWithIndifferentAccess.new
235
+ end
236
+
237
+ def add_accessors(keys)
238
+ singleton_class.instance_eval do
239
+ keys.each do |k|
240
+ # Define a getter
241
+ define_method(k) { @changes[k.to_sym] || @properties.dig(k, "value") }
242
+
243
+ # Define a setter
244
+ define_method("#{k}=") do |v|
245
+ @changes[k.to_sym] = v
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def method_missing(method_name, *arguments, &block)
252
+ # When assigning a missing attribute define the accessors and set the value
253
+ if method_name.to_s.end_with?("=")
254
+ attr = method_name.to_s[0...-1].to_sym
255
+ add_accessors([attr])
256
+
257
+ # Call the new setter
258
+ return send(method_name, arguments[0])
259
+ elsif @properties.key?(method_name)
260
+ return @properties[method_name]
261
+ else
262
+ super
263
+ end
264
+ end
265
+
266
+ def respond_to_missing?(method_name, include_private = false)
267
+ (@properties && @properties.key?(method_name)) || super
268
+ end
269
+ end
270
+ end