glg-databasedotcom 1.3.2.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.
@@ -0,0 +1,552 @@
1
+ require 'net/https'
2
+ require 'json'
3
+ require 'net/http/post/multipart'
4
+ require 'date'
5
+
6
+ module Databasedotcom
7
+ # Interface for operating the Force.com REST API
8
+ class Client
9
+ # The client id (aka "Consumer Key") to use for OAuth2 authentication
10
+ attr_accessor :client_id
11
+ # The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
12
+ attr_accessor :client_secret
13
+ # The OAuth access token in use by the client
14
+ attr_accessor :oauth_token
15
+ # The OAuth refresh token in use by the client
16
+ attr_accessor :refresh_token
17
+ # The base URL to the authenticated user's SalesForce instance
18
+ attr_accessor :instance_url
19
+ # If true, print API debugging information to stdout. Defaults to false.
20
+ attr_accessor :debugging
21
+ # The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
22
+ attr_accessor :host
23
+ # The API version the client is using. Defaults to 23.0
24
+ attr_accessor :version
25
+ # A Module in which to materialize Sobject classes. Defaults to the global module (Object)
26
+ attr_accessor :sobject_module
27
+ # The SalesForce user id of the authenticated user
28
+ attr_reader :user_id
29
+ # The SalesForce username
30
+ attr_accessor :username
31
+ # The SalesForce password
32
+ attr_accessor :password
33
+ # The SalesForce organization id for the authenticated user's Salesforce instance
34
+ attr_reader :org_id
35
+ # The CA file configured for this instance, if any
36
+ attr_accessor :ca_file
37
+ # The SSL verify mode configured for this instance, if any
38
+ attr_accessor :verify_mode
39
+
40
+ # Returns a new client object. _options_ can be one of the following
41
+ #
42
+ # * A String containing the name of a YAML file formatted like:
43
+ # ---
44
+ # client_id: <your_salesforce_client_id>
45
+ # client_secret: <your_salesforce_client_secret>
46
+ # host: login.salesforce.com
47
+ # debugging: true
48
+ # version: 23.0
49
+ # sobject_module: My::Module
50
+ # ca_file: some/ca/file.cert
51
+ # verify_mode: OpenSSL::SSL::VERIFY_PEER
52
+ # * A Hash containing the following keys:
53
+ # client_id
54
+ # client_secret
55
+ # host
56
+ # debugging
57
+ # version
58
+ # sobject_module
59
+ # ca_file
60
+ # verify_mode
61
+ # If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST,
62
+ # DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, DATABASEDOTCOM_SOBJECT_MODULE, DATABASEDOTCOM_CA_FILE, and/or
63
+ # DATABASEDOTCOM_VERIFY_MODE are present, they override any other values provided
64
+ def initialize(options = {})
65
+ if options.is_a?(String)
66
+ @options = YAML.load_file(options)
67
+ @options["verify_mode"] = @options["verify_mode"].constantize if @options["verify_mode"] && @options["verify_mode"].is_a?(String)
68
+ else
69
+ @options = options
70
+ end
71
+ @options.symbolize_keys!
72
+
73
+ if ENV['DATABASE_COM_URL']
74
+ url = URI.parse(ENV['DATABASE_COM_URL'])
75
+ url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
76
+ self.host = url.host
77
+ self.client_id = url_options[:oauth_key]
78
+ self.client_secret = url_options[:oauth_secret]
79
+ self.username = url_options[:user]
80
+ self.password = url_options[:password]
81
+ else
82
+ self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
83
+ self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
84
+ self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
85
+ end
86
+
87
+ self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
88
+ self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
89
+ self.version = self.version.to_s if self.version
90
+ self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || @options[:sobject_module]
91
+ self.ca_file = ENV['DATABASEDOTCOM_CA_FILE'] || @options[:ca_file]
92
+ self.verify_mode = ENV['DATABASEDOTCOM_VERIFY_MODE'] || @options[:verify_mode]
93
+ self.verify_mode = self.verify_mode.to_i if self.verify_mode
94
+ end
95
+
96
+ # Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
97
+ #
98
+ # * If _options_ contains the keys <tt>:username</tt> and <tt>:password</tt>, those credentials are used to authenticate. In this case, the value of <tt>:password</tt> may need to include a concatenated security token, if required by your Salesforce org
99
+ # * If _options_ contains the key <tt>:provider</tt>, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication
100
+ # * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source. _options_ may also optionally contain the key <tt>:refresh_token</tt>
101
+ #
102
+ # Raises SalesForceError if an error occurs
103
+ def authenticate(options = nil)
104
+ if user_and_pass?(options)
105
+ req = https_request(self.host)
106
+ user = self.username || options[:username]
107
+ pass = self.password || options[:password]
108
+ path = encode_path_with_params('/services/oauth2/token', :grant_type => 'password', :client_id => self.client_id, :client_secret => self.client_secret, :username => user, :password => pass)
109
+ log_request("https://#{self.host}/#{path}")
110
+ result = req.post(path, "")
111
+ log_response(result)
112
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
113
+ self.username = user
114
+ self.password = pass
115
+ parse_auth_response(result.body)
116
+ elsif options.is_a?(Hash)
117
+ if options.has_key?("provider")
118
+ parse_user_id_and_org_id_from_identity_url(options["uid"])
119
+ self.instance_url = options["credentials"]["instance_url"]
120
+ self.oauth_token = options["credentials"]["token"]
121
+ self.refresh_token = options["credentials"]["refresh_token"]
122
+ else
123
+ raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
124
+ self.instance_url = options[:instance_url]
125
+ self.oauth_token = options[:token]
126
+ self.refresh_token = options[:refresh_token]
127
+ end
128
+ end
129
+
130
+ self.version = "22.0" unless self.version
131
+
132
+ self.oauth_token
133
+ end
134
+
135
+ # The SalesForce organization id for the authenticated user's Salesforce instance
136
+ def org_id
137
+ @org_id ||= query_org_id # lazy query org_id when not set by login response
138
+ end
139
+
140
+ # Returns an Array of Strings listing the class names for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
141
+ def list_sobjects
142
+ result = http_get("/services/data/v#{self.version}/sobjects")
143
+ if result.is_a?(Net::HTTPOK)
144
+ JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
145
+ elsif result.is_a?(Net::HTTPBadRequest)
146
+ raise SalesForceError.new(result)
147
+ end
148
+ end
149
+
150
+ # Dynamically defines classes for Force.com class names. _classnames_ can be a single String or an Array of Strings. Returns the class or Array of classes defined.
151
+ #
152
+ # client.materialize("Contact") #=> Contact
153
+ # client.materialize(%w(Contact Company)) #=> [Contact, Company]
154
+ #
155
+ # The classes defined by materialize derive from Sobject, and have getters and setters defined for all the attributes defined by the associated Force.com Sobject.
156
+ def materialize(classnames)
157
+ classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
158
+ original_classname = clazz
159
+ clazz = original_classname[0,1].capitalize + original_classname[1..-1]
160
+ unless const_defined_in_module(module_namespace, clazz)
161
+ new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
162
+ new_class.client = self
163
+ new_class.materialize(original_classname)
164
+ new_class
165
+ else
166
+ module_namespace.const_get(clazz)
167
+ end
168
+ end
169
+
170
+ classes.length == 1 ? classes.first : classes
171
+ end
172
+
173
+ # Returns an Array of Hashes listing the properties for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
174
+ def describe_sobjects
175
+ result = http_get("/services/data/v#{self.version}/sobjects")
176
+ JSON.parse(result.body)["sobjects"]
177
+ end
178
+
179
+ # Returns a description of the Sobject specified by _class_name_. The description includes all fields and their properties for the Sobject.
180
+ def describe_sobject(class_name)
181
+ result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
182
+ JSON.parse(result.body)
183
+ end
184
+
185
+ # Returns an instance of the Sobject specified by _class_or_classname_ (which can be either a String or a Class) populated with the values of the Force.com record specified by _record_id_.
186
+ # If given a Class that is not defined, it will attempt to materialize the class on demand.
187
+ #
188
+ # client.find(Account, "recordid") #=> #<Account @Id="recordid", ...>
189
+ def find(class_or_classname, record_id)
190
+ class_or_classname = find_or_materialize(class_or_classname)
191
+ result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
192
+ response = JSON.parse(result.body)
193
+ new_record = class_or_classname.new
194
+ class_or_classname.description["fields"].each do |field|
195
+ set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
196
+ end
197
+ new_record
198
+ end
199
+
200
+ # Returns a Collection of Sobjects of the class specified in the _soql_expr_, which is a valid SOQL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_soql.htm] expression. The objects will only be populated with the values of attributes specified in the query.
201
+ #
202
+ # client.query("SELECT Name FROM Account") #=> [#<Account @Id=nil, @Name="Foo", ...>, #<Account @Id=nil, @Name="Bar", ...> ...]
203
+ def query(soql_expr)
204
+ result = http_get("/services/data/v#{self.version}/query", :q => soql_expr)
205
+ collection_from(result.body)
206
+ end
207
+
208
+ # Returns a Collection of Sobject instances form the results of the SOSL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_sosl.htm] search.
209
+ #
210
+ # client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]
211
+ def search(sosl_expr)
212
+ result = http_get("/services/data/v#{self.version}/search", :q => sosl_expr)
213
+ collection_from(result.body)
214
+ end
215
+
216
+ # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.
217
+ def next_page(path)
218
+ result = http_get(path)
219
+ collection_from(result.body)
220
+ end
221
+
222
+ # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.
223
+ def previous_page(path)
224
+ result = http_get(path)
225
+ collection_from(result.body)
226
+ end
227
+
228
+ # Returns a new instance of _class_or_classname_ (which can be passed in as either a String or a Class) with the specified attributes.
229
+ #
230
+ # client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #<Car @Id="recordid", @Color="Blue", @Year="2011">
231
+ def create(class_or_classname, object_attrs)
232
+ class_or_classname = find_or_materialize(class_or_classname)
233
+ json_for_assignment = coerced_json(object_attrs, class_or_classname)
234
+ result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
235
+ new_object = class_or_classname.new
236
+ JSON.parse(json_for_assignment).each do |property, value|
237
+ set_value(new_object, property, value, class_or_classname.type_map[property][:type])
238
+ end
239
+ id = JSON.parse(result.body)["id"]
240
+ set_value(new_object, "Id", id, "id")
241
+ new_object
242
+ end
243
+
244
+ # Updates the attributes of the record of type _class_or_classname_ and specified by _record_id_ with the values of _new_attrs_ in the Force.com database. _new_attrs_ is a hash of attribute => value
245
+ #
246
+ # client.update("Car", "rid", {"Color" => "Red"})
247
+ def update(class_or_classname, record_id, new_attrs)
248
+ class_or_classname = find_or_materialize(class_or_classname)
249
+ json_for_update = coerced_json(new_attrs, class_or_classname)
250
+ http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
251
+ end
252
+
253
+ # Attempts to find the record on Force.com of type _class_or_classname_ with attribute _field_ set as _value_. If found, it will update the record with the _attrs_ hash.
254
+ # If not found, it will create a new record with _attrs_.
255
+ #
256
+ # client.upsert(Car, "Color", "Blue", {"Year" => "2012"})
257
+ def upsert(class_or_classname, field, value, attrs)
258
+ clazz = find_or_materialize(class_or_classname)
259
+ json_for_update = coerced_json(attrs, clazz)
260
+ http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
261
+ end
262
+
263
+ # Deletes the record of type _class_or_classname_ with id of _record_id_. _class_or_classname_ can be a String or a Class.
264
+ #
265
+ # client.delete(Car, "rid")
266
+ def delete(class_or_classname, record_id)
267
+ clazz = find_or_materialize(class_or_classname)
268
+ http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
269
+ end
270
+
271
+ # Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.
272
+ def recent
273
+ result = http_get("/services/data/v#{self.version}/recent")
274
+ collection_from(result.body)
275
+ end
276
+
277
+ # Returns an array of trending topic names.
278
+ def trending_topics
279
+ result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
280
+ result = JSON.parse(result.body)
281
+ result["topics"].collect { |topic| topic["name"] }
282
+ end
283
+
284
+ # Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
285
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
286
+ # HTTPSuccess- raises SalesForceError otherwise.
287
+ def http_get(path, parameters={}, headers={})
288
+ with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
289
+ https_request.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
290
+ end
291
+ end
292
+
293
+
294
+ # Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
295
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
296
+ # HTTPSuccess- raises SalesForceError otherwise.
297
+ def http_delete(path, parameters={}, headers={})
298
+ with_encoded_path_and_checked_response(path, parameters, {:expected_result_class => Net::HTTPNoContent}) do |encoded_path|
299
+ https_request.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
300
+ end
301
+ end
302
+
303
+ # Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
304
+ # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
305
+ # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
306
+ def http_post(path, data=nil, parameters={}, headers={})
307
+ with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
308
+ https_request.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
309
+ end
310
+ end
311
+
312
+ # Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
313
+ # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
314
+ # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
315
+ def http_patch(path, data=nil, parameters={}, headers={})
316
+ with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
317
+ https_request.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
318
+ end
319
+ end
320
+
321
+ # Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
322
+ # The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
323
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
324
+ # Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
325
+ def http_multipart_post(path, parts, parameters={}, headers={})
326
+ with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
327
+ https_request.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
328
+ end
329
+ end
330
+
331
+ private
332
+
333
+ def with_encoded_path_and_checked_response(path, parameters, options = {})
334
+ ensure_expected_response(options[:expected_result_class]) do
335
+ with_logging(encode_path_with_params(path, parameters), options) do |encoded_path|
336
+ yield(encoded_path)
337
+ end
338
+ end
339
+ end
340
+
341
+ def with_logging(encoded_path, options)
342
+ log_request(encoded_path, options)
343
+ response = yield encoded_path
344
+ log_response(response)
345
+ response
346
+ end
347
+
348
+ def ensure_expected_response(expected_result_class)
349
+ response = yield
350
+
351
+ unless response.is_a?(expected_result_class || Net::HTTPSuccess)
352
+ if response.is_a?(Net::HTTPUnauthorized)
353
+ if self.refresh_token
354
+ response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "refresh_token", :refresh_token => self.refresh_token, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
355
+ response = https_request(self.host).post(encoded_path, nil)
356
+ if response.is_a?(Net::HTTPOK)
357
+ parse_auth_response(response.body)
358
+ end
359
+ response
360
+ end
361
+ elsif self.username && self.password
362
+ response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "password", :username => self.username, :password => self.password, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
363
+ response = https_request(self.host).post(encoded_path, nil)
364
+ if response.is_a?(Net::HTTPOK)
365
+ parse_auth_response(response.body)
366
+ end
367
+ response
368
+ end
369
+ end
370
+
371
+ if response.is_a?(Net::HTTPSuccess)
372
+ response = yield
373
+ end
374
+ end
375
+
376
+ raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess)
377
+ end
378
+
379
+ response
380
+ end
381
+
382
+ def https_request(host=nil)
383
+ Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap do |http|
384
+ http.use_ssl = true
385
+ http.ca_file = self.ca_file if self.ca_file
386
+ http.verify_mode = self.verify_mode if self.verify_mode
387
+ end
388
+ end
389
+
390
+ def encode_path_with_params(path, parameters={})
391
+ [URI.escape(path), encode_parameters(parameters)].reject{|el| el.empty?}.join('?')
392
+ end
393
+
394
+ def encode_parameters(parameters={})
395
+ (parameters || {}).collect { |k, v| "#{uri_escape(k)}=#{uri_escape(v)}" }.join('&')
396
+ end
397
+
398
+ def log_request(path, options={})
399
+ base_url = options[:host] ? "https://#{options[:host]}" : self.instance_url
400
+ puts "***** REQUEST: #{path.include?(':') ? path : URI.join(base_url, path)}#{options[:data] ? " => #{options[:data]}" : ''}" if self.debugging
401
+ end
402
+
403
+ def uri_escape(str)
404
+ URI.escape(str.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
405
+ end
406
+
407
+ def log_response(result)
408
+ puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging
409
+ end
410
+
411
+ def find_or_materialize(class_or_classname)
412
+ if class_or_classname.is_a?(Class)
413
+ clazz = class_or_classname
414
+ else
415
+ match = class_or_classname.match(/(?:(.+)::)?(\w+)$/)
416
+ preceding_namespace = match[1]
417
+ classname = match[2]
418
+ raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name
419
+ clazz = module_namespace.const_get(classname.to_sym) rescue nil
420
+ clazz ||= self.materialize(classname)
421
+ end
422
+ clazz
423
+ end
424
+
425
+ def module_namespace
426
+ _module = self.sobject_module
427
+ _module = _module.constantize if _module.is_a? String
428
+ _module || Object
429
+ end
430
+
431
+ def collection_from(response)
432
+ response = JSON.parse(response)
433
+ collection_from_hash( response )
434
+ end
435
+
436
+ # Converts a Hash of object data into a concrete SObject
437
+ def record_from_hash(data)
438
+ attributes = data.delete('attributes')
439
+ new_record = find_or_materialize(attributes["type"]).new
440
+ data.each do |name, value|
441
+ field = new_record.description['fields'].find do |field|
442
+ key_from_label(field["label"]) == name || field["name"] == name || field["relationshipName"] == name
443
+ end
444
+
445
+ # Field not found
446
+ if field == nil
447
+ break
448
+ end
449
+
450
+ # If reference/lookup field data was fetched, recursively build the child record and apply
451
+ if value.is_a?(Hash) and field['type'] == 'reference' and field["relationshipName"]
452
+ relation = record_from_hash( value )
453
+ set_value( new_record, field["relationshipName"], relation, 'reference' )
454
+
455
+ # Apply the raw value for all other field types
456
+ else
457
+ set_value(new_record, field["name"], value, field["type"]) if field
458
+ end
459
+ end
460
+ new_record
461
+ end
462
+
463
+ def collection_from_hash(data)
464
+ array_response = data.is_a?(Array)
465
+ if array_response
466
+ records = data.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) }
467
+ else
468
+ records = data["records"].collect do |record|
469
+ record_from_hash( record )
470
+ end
471
+ end
472
+
473
+ Databasedotcom::Collection.new(self, array_response ? records.length : data["totalSize"], array_response ? nil : data["nextRecordsUrl"]).concat(records)
474
+ end
475
+
476
+ def set_value(record, attr, value, attr_type)
477
+ value_to_set = value
478
+
479
+ case attr_type
480
+ when "datetime"
481
+ value_to_set = DateTime.parse(value) rescue nil
482
+
483
+ when "date"
484
+ value_to_set = Date.parse(value) rescue nil
485
+
486
+ when "multipicklist"
487
+ value_to_set = value.split(";") rescue []
488
+ end
489
+
490
+ record.send("#{attr}=", value_to_set)
491
+ end
492
+
493
+ def coerced_json(attrs, clazz)
494
+ if attrs.is_a?(Hash)
495
+ coerced_attrs = {}
496
+ attrs.keys.each do |key|
497
+ case clazz.field_type(key.to_s)
498
+ when "multipicklist"
499
+ coerced_attrs[key] = (attrs[key] || []).join(';')
500
+ when "datetime"
501
+ begin
502
+ attrs[key] = DateTime.parse(attrs[key]) if attrs[key].is_a?(String)
503
+ coerced_attrs[key] = attrs[key].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z")
504
+ rescue
505
+ nil
506
+ end
507
+ when "date"
508
+ if attrs[key]
509
+ coerced_attrs[key] = attrs[key].respond_to?(:strftime) ? attrs[key].strftime("%Y-%m-%d") : attrs[key]
510
+ else
511
+ coerced_attrs[key] = nil
512
+ end
513
+ else
514
+ coerced_attrs[key] = attrs[key]
515
+ end
516
+ end
517
+ coerced_attrs.to_json
518
+ else
519
+ attrs
520
+ end
521
+ end
522
+
523
+ def key_from_label(label)
524
+ label.gsub(' ', '_')
525
+ end
526
+
527
+ def user_and_pass?(options)
528
+ (self.username && self.password) || (options && options[:username] && options[:password])
529
+ end
530
+
531
+ def parse_user_id_and_org_id_from_identity_url(identity_url)
532
+ m = identity_url.match(/\/id\/([^\/]+)\/([^\/]+)$/)
533
+ @org_id = m[1] rescue nil
534
+ @user_id = m[2] rescue nil
535
+ end
536
+
537
+ def parse_auth_response(body)
538
+ json = JSON.parse(body)
539
+ parse_user_id_and_org_id_from_identity_url(json["id"])
540
+ self.instance_url = json["instance_url"]
541
+ self.oauth_token = json["access_token"]
542
+ end
543
+
544
+ def query_org_id
545
+ query("select id from Organization")[0]["Id"]
546
+ end
547
+
548
+ def const_defined_in_module(mod, const)
549
+ mod.method(:const_defined?).arity == 1 ? mod.const_defined?(const) : mod.const_defined?(const, false)
550
+ end
551
+ end
552
+ end