File: databasedotcom/client.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Databasedotcom#6
  class: Client#8
inherits from
  Object ( Builtin-Module )
has properties
attribute: client_id [RW] #10
attribute: client_secret [RW] #12
attribute: oauth_token [RW] #14
attribute: refresh_token [RW] #16
attribute: instance_url [RW] #18
attribute: debugging [RW] #20
attribute: host [RW] #22
attribute: version [RW] #24
attribute: sobject_module [RW] #26
attribute: user_id [R] #28
attribute: username [RW] #30
attribute: password [RW] #32
attribute: org_id (1/2) [R] #34
attribute: ca_file [RW] #36
attribute: verify_mode [RW] #38
method: initialize / 1 #64
method: authenticate / 1 #103
method: org_id (2/E) #136
method: list_sobjects #141
method: materialize / 1 #156
method: describe_sobjects #174
method: describe_sobject / 1 #180
method: find / 2 #189
method: query / 1 #203
method: search / 1 #211
method: next_page / 1 #217
method: previous_page / 1 #223
method: create / 2 #231
method: update / 3 #247
method: upsert / 4 #257
method: delete / 2 #266
method: recent #272
method: trending_topics #278
method: http_get / 3 #287
method: http_delete / 3 #297
method: http_post / 4 #306
method: http_patch / 4 #315
method: http_multipart_post / 4 #325
method: with_encoded_path_and_checked_response / 3 #333
method: with_logging / 2 #341
method: ensure_expected_response / 1 #348
method: https_request / 1 #382
method: encode_path_with_params / 2 #390
method: encode_parameters / 1 #394
method: log_request / 2 #398
method: uri_escape / 1 #403
method: log_response / 1 #407
method: find_or_materialize / 1 #411
method: module_namespace #425
method: collection_from / 1 #431
method: record_from_hash / 1 #437
method: collection_from_hash / 1 #463
method: set_value / 4 #476
method: coerced_json / 2 #493
method: key_from_label / 1 #518
method: user_and_pass? / 1 #522
method: parse_user_id_and_org_id_from_identity_url / 1 #526
method: parse_auth_response / 1 #532
method: query_org_id #539
method: const_defined_in_module / 2 #543

Class Hierarchy

Object ( Builtin-Module )
  Client ( Databasedotcom ) #8

Code

   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 = "/services/oauth2/token?grant_type=password&client_id=#{self.client_id}&client_secret=#{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)
 498              when "multipicklist"
 499                coerced_attrs[key] = (attrs[key] || []).join(';')
 500              when "datetime"
 501                coerced_attrs[key] = 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") : nil
 502              when "date"
 503                if attrs[key]
 504                  coerced_attrs[key] = attrs[key].respond_to?(:strftime) ? attrs[key].strftime("%Y-%m-%d") : attrs[key]
 505                else
 506                  coerced_attrs[key] = nil
 507                end
 508              else
 509                coerced_attrs[key] = attrs[key]
 510            end
 511          end
 512          coerced_attrs.to_json
 513        else
 514          attrs
 515        end
 516      end
 517 
 518      def key_from_label(label)
 519        label.gsub(' ', '_')
 520      end
 521 
 522      def user_and_pass?(options)
 523        (self.username && self.password) || (options && options[:username] && options[:password])
 524      end
 525 
 526      def parse_user_id_and_org_id_from_identity_url(identity_url)
 527        m = identity_url.match(/\/id\/([^\/]+)\/([^\/]+)$/)
 528        @org_id = m[1] rescue nil
 529        @user_id = m[2] rescue nil
 530      end
 531 
 532      def parse_auth_response(body)
 533        json = JSON.parse(body)
 534        parse_user_id_and_org_id_from_identity_url(json["id"])
 535        self.instance_url = json["instance_url"]
 536        self.oauth_token = json["access_token"]
 537      end
 538 
 539      def query_org_id
 540        query("select id from Organization")[0]["Id"]
 541      end
 542 
 543      def const_defined_in_module(mod, const)
 544        mod.method(:const_defined?).arity == 1 ? mod.const_defined?(const) : mod.const_defined?(const, false)
 545      end
 546    end
 547  end