glg-databasedotcom 1.3.2.1

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