soapforce 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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