soapforce 0.1.1

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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
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
18
+ vendor
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ notifications:
5
+ email:
6
+ recipients:
7
+ - joeheth@gmail.com
8
+ on_success: never
9
+ on_failure: always
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in soapforce.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Joe Heth
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,145 @@
1
+ # Soapforce
2
+
3
+
4
+ [![Build Status](https://travis-ci.org/TinderBox/soapforce.png)](https://travis-ci.org/TinderBox/soapforce)
5
+
6
+
7
+ Soapforce is a ruby gem for the [Salesforce SOAP API](http://www.salesforce.com/us/developer/docs/api/index.htm).
8
+ This gem was modeled after the [restforce](https://github.com/ejholmes/restforce) gem and depends on [Savon 2](http://savonrb.com/version2/).
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'soapforce'
15
+
16
+ Or to get the latest changes from the source:
17
+
18
+ gem 'soapforce', :git => "git://github.com/TinderBox/soapforce.git"
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install soapforce
27
+
28
+
29
+ ## Usage
30
+
31
+ For ISV Partners you can specify your client_id in a configuration block which will get included in the CallOptions header of every request.
32
+
33
+ # config/initializers/soapforce.rb
34
+ # This is your ISV Partner Client ID.
35
+ # It needs to be whitelisted to enable SOAP requests in Professional and Group Editions.
36
+ Soapforce.configure do |config|
37
+ config.client_id = "ParterName/SomeValue/"
38
+ end
39
+
40
+
41
+ #### Username/Password authentication
42
+
43
+ If you prefer to use a username and password to authenticate:
44
+
45
+ ```ruby
46
+ client = Soapforce::Client.new
47
+ client.authenticate(:username => 'foo', :password => 'password_and_security_token')
48
+ ```
49
+
50
+ #### Session authentication
51
+
52
+ ```ruby
53
+ client = Soapforce::Client.new
54
+ client.authenticate(:session_id => 'session id', :server_url => 'server url')
55
+ ```
56
+
57
+ ### find
58
+
59
+ ```ruby
60
+ client.find('Account', '006A000000Lbiiz')
61
+ # => #<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >
62
+
63
+ client.find('Account', '1234', 'Some_External_Id_Field__c')
64
+ # => #<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >
65
+ ```
66
+
67
+ ### find_where
68
+
69
+ ```ruby
70
+ client.find_where('Account', Name: "Test")
71
+ # => [#<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >]
72
+
73
+ client.find_where('Account', Some_External_Id_Field__c: 1, ["Id", "Name, "CreatedBy"])
74
+ # => [#<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" CreatedBy="005G0000003f1ABPIN" ... >]
75
+ ```
76
+
77
+ ### search
78
+
79
+ ```ruby
80
+ # Find all occurrences of 'bar'
81
+ client.search('FIND {bar}')
82
+ # => #[<Hash>]
83
+ ```
84
+
85
+ ### create
86
+
87
+ ```ruby
88
+ # Add a new account
89
+ client.create('Account', Name: 'Foobar Inc.')
90
+ # => {id: '006A000000Lbiiz', success: => true}
91
+ ```
92
+
93
+ ### update
94
+
95
+ ```ruby
96
+ # Update the Account with Id '006A000000Lbiiz'
97
+ client.update('Account', Id: '006A000000Lbiiz', Name: 'Whizbang Corp')
98
+ # => {id: '006A000000Lbiiz', success: => true}
99
+ ```
100
+
101
+ ### upsert
102
+
103
+ ```ruby
104
+ # Update the record with external ID of 12
105
+ client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
106
+ # => {id: '006A000000Lbiiz', success: => true, created: false}
107
+ ```
108
+
109
+ ### destroy
110
+
111
+ ```ruby
112
+ # Delete the Account with Id '006A000000Lbiiz'
113
+ client.destroy('Account', '006A000000Lbiiz')
114
+ # => {id: '0016000000MRatd', success: => true}
115
+ ```
116
+
117
+ ### describe
118
+
119
+ ```ruby
120
+ # get the global describe for all sobjects
121
+ client.describe_global
122
+ # => { ... }
123
+
124
+ # get the describe for the Account object
125
+ client.describe('Account')
126
+ # => { ... }
127
+
128
+ # get the describe for Account and Opportunity object
129
+ client.describe(['Account', 'Opportunity'])
130
+ # => [{ ... },{ ... }]
131
+ ```
132
+
133
+ ### logout
134
+
135
+ ```ruby
136
+ client.logout
137
+ ```
138
+
139
+ ## Contributing
140
+
141
+ 1. Fork it
142
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
143
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
144
+ 4. Push to the branch (`git push origin my-new-feature`)
145
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => [:spec]
4
+
5
+ require 'rspec/core/rake_task'
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.pattern = 'spec/**/*_spec.rb'
9
+ end
data/lib/soapforce.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'savon'
2
+
3
+ require "soapforce/version"
4
+ require "soapforce/configuration"
5
+
6
+ require "soapforce/client"
7
+ require "soapforce/query_result"
8
+ require "soapforce/sobject"
9
+
10
+ module Soapforce
11
+
12
+ end
@@ -0,0 +1,333 @@
1
+ module Soapforce
2
+ class Client
3
+
4
+ attr_reader :client
5
+ attr_reader :headers
6
+
7
+ # The partner.wsdl is used by default but can be changed by passing in a new :wsdl option.
8
+ # A client_id can be
9
+ def initialize(options={})
10
+ @describe_cache = {}
11
+ @headers = {}
12
+ @wsdl = options[:wsdl] || File.dirname(__FILE__) + "/../../resources/partner.wsdl.xml"
13
+
14
+ # If a client_id is provided then it needs to be included
15
+ # in the header for every request. This allows ISV Partners
16
+ # to make SOAP calls in Professional/Group Edition organizations.
17
+
18
+ client_id = options[:client_id] || Soapforce.configuration.client_id
19
+ @headers = {"tns:CallOptions" => {"tns:client" => client_id}} if client_id
20
+
21
+ @client = Savon.client(
22
+ wsdl: @wsdl,
23
+ soap_header: @headers,
24
+ convert_request_keys_to: :none,
25
+ pretty_print_xml: true
26
+ )
27
+ end
28
+
29
+ # Public: Get the names of all wsdl operations.
30
+ # List all available operations from the partner.wsdl
31
+ def operations
32
+ @client.operations
33
+ end
34
+
35
+ # Public: Get the names of all wsdl operations.
36
+ #
37
+ # Supports a username/password (with token) combination or session_id/server_url pair.
38
+ #
39
+ # Examples
40
+ #
41
+ # client.login(username: 'test', password: 'password_and_token')
42
+ # # => {...}
43
+ #
44
+ # client.login(session_id: 'abcd1234', server_url: 'https://na1.salesforce.com/')
45
+ # # => {...}
46
+ #
47
+ # Returns Hash of login response and user info
48
+ def login(options={})
49
+ result = nil
50
+ if options[:username] && options[:password]
51
+ response = @client.call(:login) do |locals|
52
+ locals.message :username => options[:username], :password => options[:password]
53
+ end
54
+
55
+ result = response.to_hash[:login_response][:result]
56
+ returned_endpoint = result[:server_url]
57
+
58
+ @session_id = result[:session_id]
59
+ @server_url = result[:server_url]
60
+ elsif options[:session_id] && options[:server_url]
61
+ @session_id = options[:session_id]
62
+ @server_url = options[:server_url]
63
+ else
64
+ raise ArgumentError.new("Must provide username/password or session_id/server_url.")
65
+ end
66
+
67
+ @headers = @headers.merge({"tns:SessionHeader" => {"tns:sessionId" => @session_id}})
68
+
69
+ @client = Savon.client(
70
+ wsdl: @wsdl,
71
+ soap_header: @headers,
72
+ convert_request_keys_to: :none,
73
+ endpoint: @server_url
74
+ )
75
+
76
+ # If a session_id/server_url were passed in then invoke get_user_info for confirmation.
77
+ # Method missing to call_soap_api
78
+ result = self.get_user_info if options[:session_id]
79
+
80
+ result
81
+ end
82
+ alias_method :authenticate, :login
83
+
84
+
85
+ # Public: Get the names of all sobjects on the org.
86
+ #
87
+ # Examples
88
+ #
89
+ # # get the names of all sobjects on the org
90
+ # client.list_sobjects
91
+ # # => ['Account', 'Lead', ... ]
92
+ #
93
+ # Returns an Array of String names for each SObject.
94
+ def list_sobjects
95
+ response = describe_global # method_missing
96
+ response[:sobjects].collect { |sobject| sobject[:name] }
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
+ object = query('select id from Organization').first
109
+ if object && object[:id]
110
+ return object[:id].is_a?(Array) ? object[:id].first : object[:id]
111
+ end
112
+ end
113
+
114
+ # Public: Returns a detailed describe result for the specified sobject
115
+ #
116
+ # sobject - String name of the sobject.
117
+ #
118
+ # Examples
119
+ #
120
+ # # get the describe for the Account object
121
+ # client.describe('Account')
122
+ # # => { ... }
123
+ #
124
+ # # get the describe for the Account object
125
+ # client.describe(['Account', 'Contact'])
126
+ # # => { ... }
127
+ #
128
+ # Returns the Hash representation of the describe call.
129
+ def describe(sobject_type)
130
+ if sobject_type.is_a?(Array)
131
+ list = sobject_type.map do |type|
132
+ {:sObjectType => type}
133
+ end
134
+ response = call_soap_api(:describe_s_objects, :sObjectType => sobject_type)
135
+ else
136
+ # Cache objects to avoid repeat lookups.
137
+ if @describe_cache[sobject_type].nil?
138
+ response = call_soap_api(:describe_s_object, :sObjectType => sobject_type)
139
+ @describe_cache[sobject_type] = response
140
+ else
141
+ response = @describe_cache[sobject_type]
142
+ end
143
+ end
144
+
145
+ response
146
+ end
147
+
148
+ def query(soql)
149
+ result = call_soap_api(:query, {:queryString => soql})
150
+ QueryResult.new(result)
151
+ end
152
+
153
+ # Includes deleted (isDeleted) or archived (isArchived) records
154
+ def query_all(soql)
155
+ result = call_soap_api(:query_all, {:queryString => soql})
156
+ QueryResult.new(result)
157
+ end
158
+
159
+ def query_more(locator)
160
+ result = call_soap_api(:query_more, {:queryLocator => locator})
161
+ QueryResult.new(result)
162
+ end
163
+
164
+ def search(sosl)
165
+ call_soap_api(:search, {:searchString => sosl})
166
+ end
167
+
168
+ def create(sobject_type, properties)
169
+ call_soap_api(:create, sobjects_hash(sobject_type, properties))
170
+ end
171
+
172
+ def update(sobject_type, properties)
173
+ call_soap_api(:update, sobjects_hash(sobject_type, properties))
174
+ end
175
+
176
+ def upsert(sobject_type, external_id_field_name, objects)
177
+ message = {externalIDFieldName: external_id_field_name}.merge(sobjects_hash(sobject_type, objects))
178
+ call_soap_api(:upsert, message)
179
+ end
180
+
181
+ def delete(id)
182
+ ids = id.is_a?(Array) ? id : [id]
183
+ call_soap_api(:delete, {:ids => ids})
184
+ end
185
+
186
+ # Public: Finds a single record and returns all fields.
187
+ #
188
+ # sobject - The String name of the sobject.
189
+ # id - The id of the record. If field is specified, id should be the id
190
+ # of the external field.
191
+ # field - External ID field to use (default: nil).
192
+ #
193
+ # Returns Hash of sobject record.
194
+ def find(sobject, id, field=nil)
195
+ if field.nil? || field.downcase == "id"
196
+ retrieve(sobject, id)
197
+ else
198
+ find_by_field(sobject, id, field)
199
+ end
200
+ end
201
+
202
+ # Public: Finds record based on where condition and returns all fields.
203
+ #
204
+ # sobject - The String name of the sobject.
205
+ # where - String where clause or Hash made up of field => value pairs.
206
+ # select - Optional array of field names to return.
207
+ #
208
+ # Returns Hash of sobject record.
209
+ def find_where(sobject, where={}, select_fields=[])
210
+
211
+ if where.is_a?(String)
212
+ where_clause = where
213
+ elsif where.is_a?(Hash)
214
+ conditions = []
215
+ where.each {|k,v|
216
+ # Wrap strings in single quotes.
217
+ v = v.is_a?(String) ? "'#{v}'" : v
218
+ v = 'NULL' if v.nil?
219
+
220
+ # Handle IN clauses when value is an array.
221
+ if v.is_a?(Array)
222
+ # Wrap single quotes around String values.
223
+ values = v.map {|s| s.is_a?(String) ? "'#{s}'" : s}.join(", ")
224
+ conditions << "#{k} IN (#{values})"
225
+ else
226
+ conditions << "#{k} = #{v}"
227
+ end
228
+ }
229
+ where_clause = conditions.join(" AND ")
230
+
231
+ end
232
+
233
+ # Get list of fields if none were specified.
234
+ if select_fields.empty?
235
+ field_names = field_list(sobject)
236
+ else
237
+ field_names = select_fields
238
+ end
239
+
240
+ soql = "Select #{field_names.join(", ")} From #{sobject} Where #{where_clause}"
241
+ result = query(soql)
242
+ end
243
+
244
+ # Public: Finds a single record and returns all fields.
245
+ #
246
+ # sobject - The String name of the sobject.
247
+ # id - The id of the record. If field is specified, id should be the id
248
+ # of the external field.
249
+ # field - External ID field to use.
250
+ #
251
+ # Returns Hash of sobject record.
252
+ def find_by_field(sobject, id, field_name)
253
+ field_details = field_details(sobject, field_name)
254
+ field_names = field_list(sobject).join(", ")
255
+
256
+ if ["int", "currency", "double", "boolean", "percent"].include?(field_details[:type])
257
+ search_value = id
258
+ else
259
+ # default to quoted value
260
+ search_value = "'#{id}'"
261
+ end
262
+
263
+ soql = "Select #{field_names} From #{sobject} Where #{field_name} = #{search_value}"
264
+ result = query(soql)
265
+ # Return first query result.
266
+ result ? result.first : nil
267
+ end
268
+
269
+ # Public: Finds a single record and returns all fields.
270
+ #
271
+ # sobject - The String name of the sobject.
272
+ # id - The id of the record. If field is specified, id should be the id
273
+ # of the external field.
274
+ #
275
+ # Returns Hash of sobject record.
276
+ def retrieve(sobject, id)
277
+ ids = id.is_a?(Array) ? id : [id]
278
+ sobject = call_soap_api(:retrieve, {fieldList: field_list(sobject).join(","), sObjectType: sobject, ids: ids})
279
+ sobject ? SObject.new(sobject) : nil
280
+ end
281
+
282
+ def field_list(sobject)
283
+ description = describe(sobject)
284
+ field_list = description[:fields].collect {|c| c[:name] }
285
+ end
286
+
287
+ def field_details(sobject, field_name)
288
+ description = describe(sobject)
289
+ fields = description[:fields]
290
+ fields.find {|f| field_name.downcase == f[:name].downcase }
291
+ end
292
+
293
+ # Supports the following No Argument methods:
294
+ # get_user_info
295
+ # describe_global
296
+ # describe_softphone_layout
297
+ # describe_tabs
298
+ # logout
299
+ # get_server_timestamp
300
+ def method_missing(method, *args)
301
+ call_soap_api(method, *args)
302
+ end
303
+
304
+ def call_soap_api(method, message_hash={})
305
+
306
+ response = @client.call(method.to_sym) do |locals|
307
+ locals.message message_hash
308
+ end
309
+ # Convert SOAP XML to Hash
310
+ response = response.to_hash
311
+ # Get Response Body
312
+ response_body = response["#{method}_response".to_sym]
313
+ # Grab result section if exists.
314
+ result = response_body ? response_body[:result] : nil
315
+ return result
316
+ end
317
+
318
+ def sobjects_hash(sobject_type, sobject_hash)
319
+
320
+ if sobject_hash.is_a?(Array)
321
+ sobjects = sobject_hash
322
+ else
323
+ sobjects = [sobject_hash]
324
+ end
325
+
326
+ sobjects.map! do |obj|
327
+ {"ins0:type" => sobject_type}.merge(obj)
328
+ end
329
+
330
+ {sObjects: sobjects}
331
+ end
332
+ end
333
+ end