force 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +96 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +107 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE +22 -0
  7. data/README.md +421 -0
  8. data/Rakefile +10 -0
  9. data/coverage/assets/0.7.1/application.css +1110 -0
  10. data/coverage/assets/0.7.1/application.js +626 -0
  11. data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  12. data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  13. data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  14. data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  15. data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  16. data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  17. data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  18. data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  19. data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  20. data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  21. data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  22. data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  23. data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  24. data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  25. data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  26. data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  27. data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  28. data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  29. data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  30. data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  31. data/coverage/assets/0.7.1/favicon_green.png +0 -0
  32. data/coverage/assets/0.7.1/favicon_red.png +0 -0
  33. data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  34. data/coverage/assets/0.7.1/loading.gif +0 -0
  35. data/coverage/assets/0.7.1/magnify.png +0 -0
  36. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  37. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  38. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  39. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  40. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  41. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  42. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  43. data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  44. data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  45. data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  46. data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  47. data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  48. data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  49. data/coverage/index.html +19808 -0
  50. data/force.gemspec +27 -0
  51. data/lib/force.rb +74 -0
  52. data/lib/force/abstract_client.rb +9 -0
  53. data/lib/force/attachment.rb +21 -0
  54. data/lib/force/client.rb +3 -0
  55. data/lib/force/collection.rb +45 -0
  56. data/lib/force/concerns/api.rb +321 -0
  57. data/lib/force/concerns/authentication.rb +39 -0
  58. data/lib/force/concerns/base.rb +59 -0
  59. data/lib/force/concerns/caching.rb +24 -0
  60. data/lib/force/concerns/canvas.rb +10 -0
  61. data/lib/force/concerns/connection.rb +74 -0
  62. data/lib/force/concerns/picklists.rb +87 -0
  63. data/lib/force/concerns/streaming.rb +31 -0
  64. data/lib/force/concerns/verbs.rb +67 -0
  65. data/lib/force/config.rb +140 -0
  66. data/lib/force/data/client.rb +18 -0
  67. data/lib/force/mash.rb +66 -0
  68. data/lib/force/middleware.rb +27 -0
  69. data/lib/force/middleware/authentication.rb +73 -0
  70. data/lib/force/middleware/authentication/password.rb +17 -0
  71. data/lib/force/middleware/authentication/token.rb +15 -0
  72. data/lib/force/middleware/authorization.rb +15 -0
  73. data/lib/force/middleware/caching.rb +22 -0
  74. data/lib/force/middleware/gzip.rb +31 -0
  75. data/lib/force/middleware/instance_url.rb +14 -0
  76. data/lib/force/middleware/logger.rb +40 -0
  77. data/lib/force/middleware/mashify.rb +16 -0
  78. data/lib/force/middleware/multipart.rb +55 -0
  79. data/lib/force/middleware/raise_error.rb +25 -0
  80. data/lib/force/signed_request.rb +48 -0
  81. data/lib/force/sobject.rb +68 -0
  82. data/lib/force/tooling/client.rb +11 -0
  83. data/lib/force/upload_io.rb +20 -0
  84. data/lib/force/version.rb +3 -0
  85. data/spec/fixtures/auth_error_response.json +4 -0
  86. data/spec/fixtures/auth_success_response.json +7 -0
  87. data/spec/fixtures/blob.jpg +0 -0
  88. data/spec/fixtures/expired_session_response.json +6 -0
  89. data/spec/fixtures/reauth_success_response.json +7 -0
  90. data/spec/fixtures/refresh_error_response.json +4 -0
  91. data/spec/fixtures/refresh_success_response.json +7 -0
  92. data/spec/fixtures/services_data_success_response.json +12 -0
  93. data/spec/fixtures/sobject/create_success_response.json +5 -0
  94. data/spec/fixtures/sobject/delete_error_response.json +1 -0
  95. data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
  96. data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
  97. data/spec/fixtures/sobject/org_query_response.json +11 -0
  98. data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
  99. data/spec/fixtures/sobject/query_empty_response.json +5 -0
  100. data/spec/fixtures/sobject/query_error_response.json +6 -0
  101. data/spec/fixtures/sobject/query_paginated_first_page_response.json +14 -0
  102. data/spec/fixtures/sobject/query_paginated_last_page_response.json +13 -0
  103. data/spec/fixtures/sobject/query_success_response.json +38 -0
  104. data/spec/fixtures/sobject/recent_success_response.json +18 -0
  105. data/spec/fixtures/sobject/search_error_response.json +6 -0
  106. data/spec/fixtures/sobject/search_success_response.json +16 -0
  107. data/spec/fixtures/sobject/sobject_describe_error_response.json +6 -0
  108. data/spec/fixtures/sobject/sobject_describe_success_response.json +1429 -0
  109. data/spec/fixtures/sobject/sobject_find_error_response.json +6 -0
  110. data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
  111. data/spec/fixtures/sobject/upsert_created_success_response.json +5 -0
  112. data/spec/fixtures/sobject/upsert_error_response.json +6 -0
  113. data/spec/fixtures/sobject/upsert_multiple_error_response.json +4 -0
  114. data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
  115. data/spec/fixtures/sobject/write_error_response.json +6 -0
  116. data/spec/integration/abstract_client_spec.rb +306 -0
  117. data/spec/integration/data/client_spec.rb +90 -0
  118. data/spec/spec_helper.rb +20 -0
  119. data/spec/support/client_integration.rb +45 -0
  120. data/spec/support/concerns.rb +18 -0
  121. data/spec/support/event_machine.rb +14 -0
  122. data/spec/support/fixture_helpers.rb +45 -0
  123. data/spec/support/matchers.rb +11 -0
  124. data/spec/support/middleware.rb +76 -0
  125. data/spec/support/mock_cache.rb +13 -0
  126. data/spec/unit/abstract_client_spec.rb +11 -0
  127. data/spec/unit/attachment_spec.rb +15 -0
  128. data/spec/unit/collection_spec.rb +52 -0
  129. data/spec/unit/concerns/api_spec.rb +244 -0
  130. data/spec/unit/concerns/authentication_spec.rb +98 -0
  131. data/spec/unit/concerns/base_spec.rb +42 -0
  132. data/spec/unit/concerns/caching_spec.rb +29 -0
  133. data/spec/unit/concerns/canvas_spec.rb +30 -0
  134. data/spec/unit/concerns/connection_spec.rb +22 -0
  135. data/spec/unit/config_spec.rb +99 -0
  136. data/spec/unit/data/client_spec.rb +10 -0
  137. data/spec/unit/mash_spec.rb +36 -0
  138. data/spec/unit/middleware/authentication/password_spec.rb +31 -0
  139. data/spec/unit/middleware/authentication/token_spec.rb +24 -0
  140. data/spec/unit/middleware/authentication_spec.rb +67 -0
  141. data/spec/unit/middleware/authorization_spec.rb +11 -0
  142. data/spec/unit/middleware/gzip_spec.rb +66 -0
  143. data/spec/unit/middleware/instance_url_spec.rb +24 -0
  144. data/spec/unit/middleware/logger_spec.rb +19 -0
  145. data/spec/unit/middleware/mashify_spec.rb +11 -0
  146. data/spec/unit/middleware/raise_error_spec.rb +32 -0
  147. data/spec/unit/signed_request_spec.rb +24 -0
  148. data/spec/unit/sobject_spec.rb +86 -0
  149. data/spec/unit/tooling/client_spec.rb +7 -0
  150. data/tmp/rspec_guard_result +1 -0
  151. metadata +383 -0
data/force.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/force/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "force"
6
+ s.version = Force::VERSION
7
+ s.authors = ["Eric J. Holmes", "Mattt Thompson"]
8
+ s.email = ["eric@ejholmes.net", "mattt@heroku.com"]
9
+ s.description = "A lightweight ruby client for the Salesforce REST api."
10
+ s.summary = "A lightweight ruby client for the Salesforce REST api."
11
+ s.homepage = "https://github.com/heroku/force"
12
+
13
+ s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|example|log|pkg|script|spec|test|vendor)/ }
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'faraday', '~> 0.8.4'
19
+ s.add_dependency 'faraday_middleware', '>= 0.8.8'
20
+ s.add_dependency 'json', ['>= 1.7.5', '< 1.9.0']
21
+ s.add_dependency 'hashie', ['>= 1.2.0', '< 2.1']
22
+
23
+ s.add_development_dependency 'rspec', '~> 2.14.0'
24
+ s.add_development_dependency 'webmock', '~> 1.13.0'
25
+ s.add_development_dependency 'simplecov', '~> 0.7.1'
26
+ s.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
27
+ end
data/lib/force.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'json'
4
+
5
+ require 'force/version'
6
+ require 'force/config'
7
+
8
+ module Force
9
+ autoload :AbstractClient, 'force/abstract_client'
10
+ autoload :SignedRequest, 'force/signed_request'
11
+ autoload :Collection, 'force/collection'
12
+ autoload :Middleware, 'force/middleware'
13
+ autoload :Attachment, 'force/attachment'
14
+ autoload :UploadIO, 'force/upload_io'
15
+ autoload :SObject, 'force/sobject'
16
+ autoload :Client, 'force/client'
17
+ autoload :Mash, 'force/mash'
18
+
19
+ module Concerns
20
+ autoload :Authentication, 'force/concerns/authentication'
21
+ autoload :Connection, 'force/concerns/connection'
22
+ autoload :Picklists, 'force/concerns/picklists'
23
+ autoload :Streaming, 'force/concerns/streaming'
24
+ autoload :Caching, 'force/concerns/caching'
25
+ autoload :Canvas, 'force/concerns/canvas'
26
+ autoload :Verbs, 'force/concerns/verbs'
27
+ autoload :Base, 'force/concerns/base'
28
+ autoload :API, 'force/concerns/api'
29
+ end
30
+
31
+ module Data
32
+ autoload :Client, 'force/data/client'
33
+ end
34
+
35
+ module Tooling
36
+ autoload :Client, 'force/tooling/client'
37
+ end
38
+
39
+ Error = Class.new(StandardError)
40
+ AuthenticationError = Class.new(Error)
41
+ UnauthorizedError = Class.new(Error)
42
+
43
+ class << self
44
+ # Alias for Force::Data::Client.new
45
+ #
46
+ # Shamelessly pulled from https://github.com/pengwynn/octokit/blob/master/lib/octokit.rb
47
+ def new(*args)
48
+ data(*args)
49
+ end
50
+
51
+ def data(*args)
52
+ Force::Data::Client.new(*args)
53
+ end
54
+
55
+ def tooling(*args)
56
+ Force::Tooling::Client.new(*args)
57
+ end
58
+
59
+ # Helper for decoding signed requests.
60
+ def decode_signed_request(*args)
61
+ SignedRequest.decode(*args)
62
+ end
63
+ end
64
+
65
+ # Add .tap method in Ruby 1.8
66
+ module CoreExtensions
67
+ def tap
68
+ yield self
69
+ self
70
+ end
71
+ end
72
+
73
+ Object.send :include, Force::CoreExtensions unless Object.respond_to? :tap
74
+ end
@@ -0,0 +1,9 @@
1
+ module Force
2
+ class AbstractClient
3
+ include Force::Concerns::Base
4
+ include Force::Concerns::Connection
5
+ include Force::Concerns::Authentication
6
+ include Force::Concerns::Caching
7
+ include Force::Concerns::API
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Force
2
+ class Attachment < Force::SObject
3
+ # Public: Returns the body of the attachment.
4
+ #
5
+ # Examples
6
+ #
7
+ # attachment = client.query('select Id, Name, Body from Attachment').first
8
+ # File.open(attachment.Name, 'wb') { |f| f.write(attachment.Body) }
9
+ def Body
10
+ ensure_id && ensure_body
11
+ @client.get(super).body
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_body
17
+ return true if self.Body?
18
+ raise 'You need to query the Body for the record first.'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Force
2
+ Client = Data::Client
3
+ end
@@ -0,0 +1,45 @@
1
+ module Force
2
+ class Collection
3
+ include Enumerable
4
+
5
+ # Given a hash and client, will create an Enumerator that will lazily
6
+ # request Salesforce for the next page of results.
7
+ def initialize(hash, client)
8
+ @client = client
9
+ @raw_page = hash
10
+ end
11
+
12
+ # Yield each value on each page.
13
+ def each
14
+ @raw_page['records'].each { |record| yield Force::Mash.build(record, @client) }
15
+
16
+ next_page.each { |record| yield record } if has_next_page?
17
+ end
18
+
19
+ # Return the size of the Collection without making any additional requests.
20
+ def size
21
+ @raw_page['totalSize']
22
+ end
23
+ alias_method :length, :size
24
+
25
+ # Return array of the elements on the current page
26
+ def current_page
27
+ first(@raw_page['records'].size)
28
+ end
29
+
30
+ # Return the current and all of the following pages.
31
+ def pages
32
+ [self] + (has_next_page? ? next_page.pages : [])
33
+ end
34
+
35
+ # Returns true if there is a pointer to the next page.
36
+ def has_next_page?
37
+ !@raw_page['nextRecordsUrl'].nil?
38
+ end
39
+
40
+ # Returns the next page as a Force::Collection if it's available, nil otherwise.
41
+ def next_page
42
+ @next_page ||= @client.get(@raw_page['nextRecordsUrl']).body if has_next_page?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,321 @@
1
+ require 'force/concerns/verbs'
2
+
3
+ module Force
4
+ module Concerns
5
+ module API
6
+ extend Force::Concerns::Verbs
7
+
8
+ # Public: Helper methods for performing arbitrary actions against the API using
9
+ # various HTTP verbs.
10
+ #
11
+ # Examples
12
+ #
13
+ # # Perform a get request
14
+ # client.get '/services/data/v24.0/sobjects'
15
+ # client.api_get 'sobjects'
16
+ #
17
+ # # Perform a post request
18
+ # client.post '/services/data/v24.0/sobjects/Account', { ... }
19
+ # client.api_post 'sobjects/Account', { ... }
20
+ #
21
+ # # Perform a put request
22
+ # client.put '/services/data/v24.0/sobjects/Account/001D000000INjVe', { ... }
23
+ # client.api_put 'sobjects/Account/001D000000INjVe', { ... }
24
+ #
25
+ # # Perform a delete request
26
+ # client.delete '/services/data/v24.0/sobjects/Account/001D000000INjVe'
27
+ # client.api_delete 'sobjects/Account/001D000000INjVe'
28
+ #
29
+ # Returns the Faraday::Response.
30
+ define_verbs :get, :post, :put, :delete, :patch, :head
31
+
32
+ # Public: Get the names of all sobjects on the org.
33
+ #
34
+ # Examples
35
+ #
36
+ # # get the names of all sobjects on the org
37
+ # client.list_sobjects
38
+ # # => ['Account', 'Lead', ... ]
39
+ #
40
+ # Returns an Array of String names for each SObject.
41
+ def list_sobjects
42
+ describe.collect { |sobject| sobject['name'] }
43
+ end
44
+
45
+ # Public: Returns a detailed describe result for the specified sobject
46
+ #
47
+ # sobject - Stringish name of the sobject (default: nil).
48
+ #
49
+ # Examples
50
+ #
51
+ # # get the global describe for all sobjects
52
+ # client.describe
53
+ # # => { ... }
54
+ #
55
+ # # get the describe for the Account object
56
+ # client.describe('Account')
57
+ # # => { ... }
58
+ #
59
+ # Returns the Hash representation of the describe call.
60
+ def describe(sobject = nil)
61
+ if sobject
62
+ api_get("sobjects/#{sobject.to_s}/describe").body
63
+ else
64
+ api_get('sobjects').body['sobjects']
65
+ end
66
+ end
67
+
68
+ # Public: Returns a detailed description of the Page Layout for the
69
+ # specified sobject type, or URIs for layouts if the sobject has
70
+ # multiple Record Types.
71
+ #
72
+ # This resource was introduced in version 28.0.
73
+ #
74
+ # Examples:
75
+ # # get the layouts for the sobject
76
+ # client.describe_layouts('Account')
77
+ # # => { ... }
78
+ #
79
+ # # get the layout for the specified Id for the sobject
80
+ # client.describe_layouts('Account', '012E0000000RHEp')
81
+ # # => { ... }
82
+ #
83
+ # Returns the Hash representation of the describe_layouts result
84
+ def describe_layouts(sobject, layout_id = nil)
85
+ if layout_id
86
+ api_get("sobjects/#{sobject.to_s}/describe/layouts/#{layout_id}").body
87
+ else
88
+ api_get("sobjects/#{sobject.to_s}/describe/layouts").body
89
+ end
90
+ end
91
+
92
+ # Public: Get the current organization's Id.
93
+ #
94
+ # Examples
95
+ #
96
+ # client.org_id
97
+ # # => '00Dx0000000BV7z'
98
+ #
99
+ # Returns the String organization Id
100
+ def org_id
101
+ query('select id from Organization').first['Id']
102
+ end
103
+
104
+ # Public: Executs a SOQL query and returns the result.
105
+ #
106
+ # soql - A SOQL expression.
107
+ #
108
+ # Examples
109
+ #
110
+ # # Find the names of all Accounts
111
+ # client.query('select Name from Account').map(&:Name)
112
+ # # => ['Foo Bar Inc.', 'Whizbang Corp']
113
+ #
114
+ # Returns a Force::Collection if Force.configuration.mashify is true.
115
+ # Returns an Array of Hash for each record in the result if Force.configuration.mashify is false.
116
+ def query(soql)
117
+ response = api_get 'query', :q => soql
118
+ mashify? ? response.body : response.body['records']
119
+ end
120
+
121
+ # Public: Perform a SOSL search
122
+ #
123
+ # sosl - A SOSL expression.
124
+ #
125
+ # Examples
126
+ #
127
+ # # Find all occurrences of 'bar'
128
+ # client.search('FIND {bar}')
129
+ # # => #<Force::Collection >
130
+ #
131
+ # # Find accounts match the term 'genepoint' and return the Name field
132
+ # client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
133
+ # # => ['GenePoint']
134
+ #
135
+ # Returns a Force::Collection if Force.configuration.mashify is true.
136
+ # Returns an Array of Hash for each record in the result if Force.configuration.mashify is false.
137
+ def search(sosl)
138
+ api_get('search', :q => sosl).body
139
+ end
140
+
141
+ # Public: Insert a new record.
142
+ #
143
+ # sobject - String name of the sobject.
144
+ # attrs - Hash of attributes to set on the new record.
145
+ #
146
+ # Examples
147
+ #
148
+ # # Add a new account
149
+ # client.create('Account', Name: 'Foobar Inc.')
150
+ # # => '0016000000MRatd'
151
+ #
152
+ # Returns the String Id of the newly created sobject.
153
+ # Returns false if something bad happens.
154
+ def create(*args)
155
+ create!(*args)
156
+ rescue *exceptions
157
+ false
158
+ end
159
+ alias_method :insert, :create
160
+
161
+ # Public: Insert a new record.
162
+ #
163
+ # sobject - String name of the sobject.
164
+ # attrs - Hash of attributes to set on the new record.
165
+ #
166
+ # Examples
167
+ #
168
+ # # Add a new account
169
+ # client.create!('Account', Name: 'Foobar Inc.')
170
+ # # => '0016000000MRatd'
171
+ #
172
+ # Returns the String Id of the newly created sobject.
173
+ # Raises exceptions if an error is returned from Salesforce.
174
+ def create!(sobject, attrs)
175
+ api_post("sobjects/#{sobject}", attrs).body['id']
176
+ end
177
+ alias_method :insert!, :create!
178
+
179
+ # Public: Update a record.
180
+ #
181
+ # sobject - String name of the sobject.
182
+ # attrs - Hash of attributes to set on the record.
183
+ #
184
+ # Examples
185
+ #
186
+ # # Update the Account with Id '0016000000MRatd'
187
+ # client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
188
+ #
189
+ # Returns true if the sobject was successfully updated.
190
+ # Returns false if there was an error.
191
+ def update(*args)
192
+ update!(*args)
193
+ rescue *exceptions
194
+ false
195
+ end
196
+
197
+ # Public: Update a record.
198
+ #
199
+ # sobject - String name of the sobject.
200
+ # attrs - Hash of attributes to set on the record.
201
+ #
202
+ # Examples
203
+ #
204
+ # # Update the Account with Id '0016000000MRatd'
205
+ # client.update!('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
206
+ #
207
+ # Returns true if the sobject was successfully updated.
208
+ # Raises an exception if an error is returned from Salesforce.
209
+ def update!(sobject, attrs)
210
+ id = attrs.delete(attrs.keys.find { |k| k.to_s.downcase == 'id' })
211
+ raise ArgumentError, 'Id field missing from attrs.' unless id
212
+ api_patch "sobjects/#{sobject}/#{id}", attrs
213
+ true
214
+ end
215
+
216
+ # Public: Update or create a record based on an external ID
217
+ #
218
+ # sobject - The name of the sobject to created.
219
+ # field - The name of the external Id field to match against.
220
+ # attrs - Hash of attributes for the record.
221
+ #
222
+ # Examples
223
+ #
224
+ # # Update the record with external ID of 12
225
+ # client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
226
+ #
227
+ # Returns true if the record was found and updated.
228
+ # Returns the Id of the newly created record if the record was created.
229
+ # Returns false if something bad happens.
230
+ def upsert(*args)
231
+ upsert!(*args)
232
+ rescue *exceptions
233
+ false
234
+ end
235
+
236
+ # Public: Update or create a record based on an external ID
237
+ #
238
+ # sobject - The name of the sobject to created.
239
+ # field - The name of the external Id field to match against.
240
+ # attrs - Hash of attributes for the record.
241
+ #
242
+ # Examples
243
+ #
244
+ # # Update the record with external ID of 12
245
+ # client.upsert!('Account', 'External__c', External__c: 12, Name: 'Foobar')
246
+ #
247
+ # Returns true if the record was found and updated.
248
+ # Returns the Id of the newly created record if the record was created.
249
+ # Raises an exception if an error is returned from Salesforce.
250
+ def upsert!(sobject, field, attrs)
251
+ external_id = attrs.delete(attrs.keys.find { |k| k.to_s.downcase == field.to_s.downcase })
252
+ response = api_patch "sobjects/#{sobject}/#{field.to_s}/#{external_id}", attrs
253
+ (response.body && response.body['id']) ? response.body['id'] : true
254
+ end
255
+
256
+ # Public: Delete a record.
257
+ #
258
+ # sobject - String name of the sobject.
259
+ # id - The Salesforce ID of the record.
260
+ #
261
+ # Examples
262
+ #
263
+ # # Delete the Account with Id '0016000000MRatd'
264
+ # client.destroy('Account', '0016000000MRatd')
265
+ #
266
+ # Returns true if the sobject was successfully deleted.
267
+ # Returns false if an error is returned from Salesforce.
268
+ def destroy(*args)
269
+ destroy!(*args)
270
+ rescue *exceptions
271
+ false
272
+ end
273
+
274
+ # Public: Delete a record.
275
+ #
276
+ # sobject - String name of the sobject.
277
+ # id - The Salesforce ID of the record.
278
+ #
279
+ # Examples
280
+ #
281
+ # # Delete the Account with Id '0016000000MRatd'
282
+ # client.destroy('Account', '0016000000MRatd')
283
+ #
284
+ # Returns true of the sobject was successfully deleted.
285
+ # Raises an exception if an error is returned from Salesforce.
286
+ def destroy!(sobject, id)
287
+ api_delete "sobjects/#{sobject}/#{id}"
288
+ true
289
+ end
290
+
291
+ # Public: Finds a single record and returns all fields.
292
+ #
293
+ # sobject - The String name of the sobject.
294
+ # id - The id of the record. If field is specified, id should be the id
295
+ # of the external field.
296
+ # field - External ID field to use (default: nil).
297
+ #
298
+ # Returns the Force::SObject sobject record.
299
+ def find(sobject, id, field=nil)
300
+ api_get(field ? "sobjects/#{sobject}/#{field}/#{id}" : "sobjects/#{sobject}/#{id}").body
301
+ end
302
+
303
+ private
304
+
305
+ # Internal: Returns a path to an api endpoint
306
+ #
307
+ # Examples
308
+ #
309
+ # api_path('sobjects')
310
+ # # => '/services/data/v24.0/sobjects'
311
+ def api_path(path)
312
+ "/services/data/v#{options[:api_version]}/#{path}"
313
+ end
314
+
315
+ # Internal: Errors that should be rescued from in non-bang methods
316
+ def exceptions
317
+ [Faraday::Error::ClientError]
318
+ end
319
+ end
320
+ end
321
+ end