restforce 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of restforce might be problematic. Click here for more details.

Files changed (69) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +161 -0
  7. data/Rakefile +10 -0
  8. data/lib/restforce.rb +18 -0
  9. data/lib/restforce/client.rb +287 -0
  10. data/lib/restforce/collection.rb +23 -0
  11. data/lib/restforce/config.rb +68 -0
  12. data/lib/restforce/mash.rb +61 -0
  13. data/lib/restforce/middleware.rb +30 -0
  14. data/lib/restforce/middleware/authentication.rb +32 -0
  15. data/lib/restforce/middleware/authentication/oauth.rb +22 -0
  16. data/lib/restforce/middleware/authentication/password.rb +27 -0
  17. data/lib/restforce/middleware/authorization.rb +19 -0
  18. data/lib/restforce/middleware/instance_url.rb +27 -0
  19. data/lib/restforce/middleware/mashify.rb +18 -0
  20. data/lib/restforce/middleware/raise_error.rb +18 -0
  21. data/lib/restforce/sobject.rb +41 -0
  22. data/lib/restforce/version.rb +3 -0
  23. data/restforce.gemspec +28 -0
  24. data/spec/fixtures/auth_error_response.json +1 -0
  25. data/spec/fixtures/auth_success_response.json +1 -0
  26. data/spec/fixtures/expired_session_response.json +1 -0
  27. data/spec/fixtures/reauth_success_response.json +1 -0
  28. data/spec/fixtures/refresh_error_response.json +1 -0
  29. data/spec/fixtures/refresh_success_response.json +7 -0
  30. data/spec/fixtures/services_data_success_response.json +12 -0
  31. data/spec/fixtures/sobject/create_success_response.json +5 -0
  32. data/spec/fixtures/sobject/delete_error_response.json +1 -0
  33. data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
  34. data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
  35. data/spec/fixtures/sobject/org_query_response.json +11 -0
  36. data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
  37. data/spec/fixtures/sobject/query_empty_response.json +5 -0
  38. data/spec/fixtures/sobject/query_error_response.json +4 -0
  39. data/spec/fixtures/sobject/query_paginated_first_page_response.json +12 -0
  40. data/spec/fixtures/sobject/query_paginated_last_page_response.json +11 -0
  41. data/spec/fixtures/sobject/query_success_response.json +36 -0
  42. data/spec/fixtures/sobject/recent_success_response.json +18 -0
  43. data/spec/fixtures/sobject/search_error_response.json +4 -0
  44. data/spec/fixtures/sobject/search_success_response.json +16 -0
  45. data/spec/fixtures/sobject/sobject_describe_error_response.json +4 -0
  46. data/spec/fixtures/sobject/sobject_describe_success_response.json +1304 -0
  47. data/spec/fixtures/sobject/sobject_find_error_response.json +4 -0
  48. data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
  49. data/spec/fixtures/sobject/upsert_created_success_response.json +2 -0
  50. data/spec/fixtures/sobject/upsert_error_response.json +1 -0
  51. data/spec/fixtures/sobject/upsert_multiple_error_response.json +1 -0
  52. data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
  53. data/spec/fixtures/sobject/write_error_response.json +6 -0
  54. data/spec/lib/client_spec.rb +214 -0
  55. data/spec/lib/collection_spec.rb +50 -0
  56. data/spec/lib/config_spec.rb +70 -0
  57. data/spec/lib/middleware/authentication/oauth_spec.rb +30 -0
  58. data/spec/lib/middleware/authentication/password_spec.rb +37 -0
  59. data/spec/lib/middleware/authentication_spec.rb +67 -0
  60. data/spec/lib/middleware/authorization_spec.rb +17 -0
  61. data/spec/lib/middleware/instance_url_spec.rb +48 -0
  62. data/spec/lib/middleware/mashify_spec.rb +28 -0
  63. data/spec/lib/middleware/raise_error_spec.rb +27 -0
  64. data/spec/lib/sobject_spec.rb +93 -0
  65. data/spec/spec_helper.rb +17 -0
  66. data/spec/support/basic_client.rb +35 -0
  67. data/spec/support/fixture_helpers.rb +20 -0
  68. data/spec/support/middleware.rb +33 -0
  69. metadata +257 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ script: bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in restforce.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Eric J. Holmes
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Restforce [![travis-ci](https://secure.travis-ci.org/ejholmes/restforce.png)](https://secure.travis-ci.org/ejholmes/restforce)
2
+
3
+ Restforce is a ruby gem for the [Salesforce REST api](http://www.salesforce.com/us/developer/docs/api_rest/index.htm).
4
+ It's meant to be a lighter weight alternative to the [databasedotcom gem](https://github.com/heroku/databasedotcom).
5
+
6
+ It attempts to solve a couple of key issues that the databasedotcom gem has been unable to solve:
7
+
8
+ * Support for interacting with multiple users from different orgs.
9
+ * Support for parent-to-child relationships.
10
+ * Support for aggregate queries.
11
+ * Remove the need to materialize constants.
12
+ * Support for the Streaming API
13
+ * A clean and modular architecture using [Faraday middleware](https://github.com/technoweenie/faraday)
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'restforce
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install restforce
28
+
29
+ ## Usage
30
+
31
+ Restforce is designed with flexibility and ease of use in mind. By default, all api calls will
32
+ return [Hashie::Mash](https://github.com/intridea/hashie/tree/v1.2.0) objects,
33
+ so you can do things like `client.query('select Id, (select Name from Children__r) from Account').Children__r.first.Name`.
34
+
35
+ ### Initialization
36
+
37
+ Which authentication method you use really depends on your use case. If you're
38
+ building an application where many users from different orgs are authenticated
39
+ through oauth and you need to interact with data in their org on their behalf,
40
+ you should use the OAuth token authentication method.
41
+
42
+ If you're using the gem to interact with a single org (maybe you're building some
43
+ salesforce integration internally?) then you should use the username/password
44
+ authentication method.
45
+
46
+ If you have an access token and an instance url obtained through oauth:
47
+
48
+ #### OAuth token authentication
49
+
50
+ ```ruby
51
+ client = Restforce::Client.new :oauth_token => 'oauth token',
52
+ :instance_url => 'instance url'
53
+ ```
54
+
55
+ Although the above will work, you'll probably want to take advantage of the
56
+ (re)authentication middleware by specifying a refresh token, client id and client secret:
57
+
58
+ ```ruby
59
+ client = Restforce::Client.new :oauth_token => 'oauth token',
60
+ :refresh_token => 'refresh token',
61
+ :instance_url => 'instance url',
62
+ :client_id => 'client_id',
63
+ :client_secret => 'client_secret'
64
+ ```
65
+
66
+ #### Username/Password authentication
67
+
68
+ If you prefer to use a username and password to authenticate:
69
+
70
+ ```ruby
71
+ client = Restforce::Client.new :username => 'foo',
72
+ :password => 'bar',
73
+ :security_token => 'security token'
74
+ :client_id => 'client_id',
75
+ :client_secret => 'client_secret'
76
+ ```
77
+
78
+ #### Sandbox Orgs
79
+
80
+ You can connect to sandbox orgs by specifying a host. The default host is
81
+ 'login.salesforce.com':
82
+
83
+ ```ruby
84
+ client = Restforce::Client.new :host => 'test.salesforce.com'
85
+ ```
86
+
87
+ #### Global configuration
88
+
89
+ You can set any of the options passed into Restforce::Client.new globally:
90
+
91
+ ```ruby
92
+ Restforce.configure do |config|
93
+ config.client_id = ENV['SALESFORCE_CLIENT_ID']
94
+ config.client_secret = ENV['SALESFORCE_CLIENT_SECRET']
95
+ end
96
+ ```
97
+
98
+ ### Query
99
+
100
+ ```ruby
101
+ accounts = client.query("select Id, Something__c from Account where Id = 'someid'")
102
+ # => #<Restforce::Collection >
103
+
104
+ account = records.first
105
+ # => #<Restforce::SObject >
106
+
107
+ account.Id
108
+ # => "someid"
109
+
110
+ account.Name = 'Foobar'
111
+ account.save
112
+ # => true
113
+
114
+ account.destroy
115
+ # => true
116
+ ```
117
+
118
+ ### Search
119
+
120
+ ```ruby
121
+ # Find all occurrences of 'bar'
122
+ client.search('FIND {bar}')
123
+ # => #<Restforce::Collection >
124
+
125
+ # Find accounts match the term 'genepoint' and return the Name field
126
+ client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
127
+ # => ['GenePoint']
128
+ ```
129
+
130
+ ### Create
131
+
132
+ ```ruby
133
+ # Add a new account
134
+ client.create('Account', Name: 'Foobar Inc.')
135
+ # => '0016000000MRatd'
136
+ ```
137
+
138
+ ### Update
139
+
140
+ ```ruby
141
+ # Update the Account with Id '0016000000MRatd'
142
+ client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
143
+ # => true
144
+ ```
145
+
146
+ ### Destroy
147
+
148
+ ```ruby
149
+ # Delete the Account with Id '0016000000MRatd'
150
+ client.destroy('Account', '0016000000MRatd')
151
+ # => true
152
+ ```
153
+
154
+
155
+ ## Contributing
156
+
157
+ 1. Fork it
158
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
159
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
160
+ 4. Push to the branch (`git push origin my-new-feature`)
161
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => [:spec]
5
+
6
+ require 'rspec/core/rake_task'
7
+ desc "Run specs"
8
+ RSpec::Core::RakeTask.new do |t|
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ end
data/lib/restforce.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'json'
4
+
5
+ require 'restforce/version'
6
+ require 'restforce/config'
7
+ require 'restforce/mash'
8
+ require 'restforce/collection'
9
+ require 'restforce/sobject'
10
+ require 'restforce/client'
11
+
12
+ require 'restforce/middleware'
13
+
14
+ module Restforce
15
+ class AuthenticationError < StandardError; end
16
+ class UnauthorizedError < StandardError; end
17
+ class InstanceURLError < StandardError; end
18
+ end
@@ -0,0 +1,287 @@
1
+ module Restforce
2
+ class Client
3
+ # Public: Creates a new client instance
4
+ #
5
+ # options - A hash of options to be passed in (default: {}).
6
+ # :username - The String username to use (required for password authentication).
7
+ # :password - The String password to use (required for password authentication).
8
+ # :security_token - The String security token to use
9
+ # (required for password authentication).
10
+ #
11
+ # :oauth_token - The String oauth access token to authenticate api
12
+ # calls (required unless password
13
+ # authentication is used).
14
+ # :refresh_token - The String refresh token to obtain fresh
15
+ # oauth access tokens (required if oauth
16
+ # authentication is used).
17
+ # :instance_url - The String base url for all api requests
18
+ # (required if oauth authentication is used).
19
+ #
20
+ # :client_id - The oauth client id to use. Needed for both
21
+ # password and oauth authentication
22
+ # :client_secret - The oauth client secret to use.
23
+ #
24
+ # :host - The String hostname to use during
25
+ # authentication requests (default: 'login.salesforce.com').
26
+ #
27
+ # :api_version - The String REST api version to use (default: '24.0')
28
+ #
29
+ # Examples
30
+ #
31
+ # # Initialize a new client using password authentication:
32
+ # Restforce::Client.new :username => 'user',
33
+ # :password => 'pass',
34
+ # :security_token => 'security token',
35
+ # :client_id => 'client id',
36
+ # :client_secret => 'client secret'
37
+ # # => #<Restforce::Client:0x007f934aa2dc28 @options={ ... }>
38
+ #
39
+ # # Initialize a new client using oauth authentication:
40
+ # Restforce::Client.new :oauth_token => 'access token',
41
+ # :refresh_token => 'refresh token',
42
+ # :instance_url => 'https://na1.salesforce.com',
43
+ # :client_id => 'client id',
44
+ # :client_secret => 'client secret'
45
+ # # => #<Restforce::Client:0x007f934aaaa0e8 @options={ ... }>
46
+ #
47
+ # # Initialize a new client with using any authentication middleware:
48
+ # Restforce::Client.new :oauth_token => 'access token',
49
+ # :instance_url => 'https://na1.salesforce.com'
50
+ # # => #<Restforce::Client:0x007f934aab9980 @options={ ... }>
51
+ def initialize(options = {})
52
+ raise 'Please specify a hash of options' unless options.is_a?(Hash)
53
+ @options = {}.tap do |options|
54
+ [:username, :password, :security_token, :client_id, :client_secret, :host,
55
+ :api_version, :oauth_token, :refresh_token, :instance_url].each do |option|
56
+ options[option] = Restforce.configuration.send option
57
+ end
58
+ end
59
+ @options.merge!(options)
60
+ end
61
+
62
+ # Public: Get the global describe for all sobjects.
63
+ #
64
+ # Returns the Hash representation of the describe call.
65
+ def describe_sobjects
66
+ response = api_get 'sobjects'
67
+ response.body['sobjects']
68
+ end
69
+
70
+ # Public: Get the names of all sobjects on the org.
71
+ #
72
+ # Examples
73
+ #
74
+ # # get the names of all sobjects on the org
75
+ # client.list_sobjects
76
+ # # => ['Account', 'Lead', ... ]
77
+ #
78
+ # Returns an Array of String names for each SObject.
79
+ def list_sobjects
80
+ describe_sobjects.collect { |sobject| sobject['name'] }
81
+ end
82
+
83
+ # Public: Returns a detailed describe result for the specified sobject
84
+ #
85
+ # sobject - Stringish name of the sobject (default: nil).
86
+ #
87
+ # Examples
88
+ #
89
+ # # get the describe for the Account object
90
+ # client.describe('Account')
91
+ # # => { ... }
92
+ #
93
+ # Returns the Hash representation of the describe call.
94
+ def describe(sobject)
95
+ response = api_get "sobject/#{sobject.to_s}/describe"
96
+ response.body
97
+ end
98
+
99
+ # Public: Get the current organization's Id.
100
+ #
101
+ # Examples
102
+ #
103
+ # client.org_id
104
+ # # => '00Dx0000000BV7z'
105
+ #
106
+ # Returns the String organization Id
107
+ def org_id
108
+ query('select id from Organization').first['Id']
109
+ end
110
+
111
+ # Public: Executs a SOQL query and returns the result.
112
+ #
113
+ # Examples
114
+ #
115
+ # # Find the names of all Accounts
116
+ # client.query('select Name from Account').map(&:Name)
117
+ # # => ['Foo Bar Inc.', 'Whizbang Corp']
118
+ #
119
+ # Returns a Restforce::Collection if Restforce.configuration.mashify is true.
120
+ # Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
121
+ def query(query)
122
+ response = api_get 'query', q: query
123
+ mashify? ? response.body : response.body['records']
124
+ end
125
+
126
+ # Public: Perform a SOSL search
127
+ #
128
+ # Examples
129
+ #
130
+ # # Find all occurrences of 'bar'
131
+ # client.search('FIND {bar}')
132
+ # # => #<Restforce::Collection >
133
+ #
134
+ # # Find accounts match the term 'genepoint' and return the Name field
135
+ # client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
136
+ # # => ['GenePoint']
137
+ #
138
+ # Returns a Restforce::Collection if Restforce.configuration.mashify is true.
139
+ # Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
140
+ def search(term)
141
+ response = api_get 'search', q: term
142
+ response.body
143
+ end
144
+
145
+ # Public: Insert a new record.
146
+ #
147
+ # Examples
148
+ #
149
+ # # Add a new account
150
+ # client.create('Account', Name: 'Foobar Inc.')
151
+ # # => '0016000000MRatd'
152
+ #
153
+ # Returns the String Id of the newly created sobject.
154
+ def create(sobject, attrs)
155
+ response = api_post "sobjects/#{sobject}", attrs
156
+ response.body['id']
157
+ end
158
+
159
+ # Public: Update a record.
160
+ #
161
+ # Examples
162
+ #
163
+ # # Update the Account with Id '0016000000MRatd'
164
+ # client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
165
+ #
166
+ # Returns true if the sobject was successfully update, false otherwise.
167
+ def update(sobject, attrs)
168
+ id = attrs.has_key?(:Id) ? attrs.delete(:Id) : attrs.delete('Id')
169
+ response = api_patch "sobjects/#{sobject}/#{id}", attrs
170
+ true
171
+ end
172
+
173
+ # Public: Delete a record.
174
+ #
175
+ # Examples
176
+ #
177
+ # # Delete the Account with Id '0016000000MRatd'
178
+ # client.delete('Account', '0016000000MRatd')
179
+ #
180
+ # Returns true if the sobject was successfully deleted, false otherwise.
181
+ def destroy(sobject, id)
182
+ response = api_delete "sobjects/#{sobject}/#{id}"
183
+ true
184
+ end
185
+
186
+ # Public: Helper methods for performing arbitrary actions against the API using
187
+ # various HTTP verbs.
188
+ #
189
+ # Examples
190
+ #
191
+ # # Perform a get request
192
+ # client.get '/services/data/v24.0/sobjects'
193
+ # client.api_get 'sobjects'
194
+ #
195
+ # # Perform a post request
196
+ # client.post '/services/data/v24.0/sobjects/Account', { ... }
197
+ # client.api_post 'sobjects/Account', { ... }
198
+ #
199
+ # # Perform a put request
200
+ # client.put '/services/data/v24.0/sobjects/Account/001D000000INjVe', { ... }
201
+ # client.api_put 'sobjects/Account/001D000000INjVe', { ... }
202
+ #
203
+ # # Perform a delete request
204
+ # client.delete '/services/data/v24.0/sobjects/Account/001D000000INjVe'
205
+ # client.api_delete 'sobjects/Account/001D000000INjVe'
206
+ #
207
+ # Returns the Faraday::Response.
208
+ [:get, :post, :put, :delete, :patch].each do |method|
209
+ define_method method do |*args|
210
+ begin
211
+ connection.send(method, *args)
212
+ rescue Restforce::InstanceURLError
213
+ connection.url_prefix = @options[:instance_url]
214
+ connection.send(method, *args)
215
+ end
216
+ end
217
+
218
+ define_method :"api_#{method}" do |*args|
219
+ args[0] = api_path(args[0])
220
+ send(method, *args)
221
+ end
222
+ end
223
+
224
+ private
225
+
226
+ # Internal: Returns a path to an api endpoint
227
+ #
228
+ # Examples
229
+ #
230
+ # api_path('sobjects')
231
+ # # => '/services/data/v24.0/sobjects'
232
+ def api_path(path)
233
+ "/services/data/v#{@options[:api_version]}/#{path}"
234
+ end
235
+
236
+ # Internal: Internal faraday connection where all requests go through
237
+ def connection
238
+ @connection ||= Faraday.new do |builder|
239
+ builder.use Restforce::Middleware::Mashify, self, @options
240
+ builder.request :json
241
+ builder.use authentication_middleware, self, @options if authentication_middleware
242
+ builder.use Restforce::Middleware::Authorization, self, @options
243
+ builder.use Restforce::Middleware::InstanceURL, self, @options
244
+ builder.use Restforce::Middleware::RaiseError
245
+ builder.response :logger, Restforce.configuration.logger if Restforce.log?
246
+ builder.response :json
247
+ builder.adapter Faraday.default_adapter
248
+ end
249
+ @connection.headers['Content-Type'] = 'application/json'
250
+ @connection
251
+ end
252
+
253
+ # Internal: Determines what middleware will be used based on the options provided
254
+ def authentication_middleware
255
+ if username_password?
256
+ Restforce::Middleware::Authentication::Password
257
+ elsif oauth_refresh?
258
+ Restforce::Middleware::Authentication::OAuth
259
+ end
260
+ end
261
+
262
+ # Internal: Returns true if username/password (autonomous) flow should be used for
263
+ # authentication.
264
+ def username_password?
265
+ @options[:username] &&
266
+ @options[:password] &&
267
+ @options[:security_token] &&
268
+ @options[:client_id] &&
269
+ @options[:client_secret]
270
+ end
271
+
272
+ # Internal: Returns true if oauth token refresh flow should be used for
273
+ # authentication.
274
+ def oauth_refresh?
275
+ @options[:oauth_token] &&
276
+ @options[:refresh_token] &&
277
+ @options[:client_id] &&
278
+ @options[:client_secret]
279
+ end
280
+
281
+ # Internal: Returns true if the middlware stack includes the
282
+ # Restforce::Middleware::Mashify middleware.
283
+ def mashify?
284
+ connection.builder.handlers.find { |handler| handler == Restforce::Middleware::Mashify }
285
+ end
286
+ end
287
+ end