databasedotcom 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A GroupMembership represents the membership of a certain User in a certain Group.
6
+ class GroupMembership < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A like on a FeedItem
6
+ class Like < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A private message between two or more Users
6
+ class Message < Record
7
+
8
+ # Send a private message with the content _text_ to each user in the _recipients_ list.
9
+ def self.send_message(client, recipients, text)
10
+ url = "/services/data/v#{client.version}/chatter/users/me/messages"
11
+ recipients = recipients.is_a?(Array) ? recipients : [recipients]
12
+ response = client.http_post(url, nil, :text => text, :recipients => recipients.join(','))
13
+ Message.new(client, response.body)
14
+ end
15
+
16
+ # Send a reply to the message identified by _in_reply_to_message_id_ with content _text_.
17
+ def self.reply(client, in_reply_to_message_id, text)
18
+ url = "/services/data/v#{client.version}/chatter/users/me/messages"
19
+ response = client.http_post(url, nil, :text => text, :inReplyTo => in_reply_to_message_id)
20
+ Message.new(client, response.body)
21
+ end
22
+
23
+ # Send a reply to this Message with content _text_.
24
+ def reply(text)
25
+ self.class.reply(self.client, self.id, text)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ module Databasedotcom
2
+ module Chatter
3
+ # Defines methods for entities that can have photos i.e. Users, Groups.
4
+ module PhotoMethods
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ # Defines class methods for resources that can have photos.
10
+ module ClassMethods
11
+ # Returns a Hash with urls for the small and large versions of the photo for a resource.
12
+ def photo(client, resource_id)
13
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
14
+ result = client.http_get(url)
15
+ JSON.parse(result.body)
16
+ end
17
+
18
+ # Uploads a photo for a resource with id _resource_id_.
19
+ #
20
+ # User.upload_photo(@client, "me", File.open("SomePicture.png"), "image/png")
21
+ def upload_photo(client, resource_id, io, file_type)
22
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
23
+ result = client.http_multipart_post(url, {"fileUpload" => UploadIO.new(io, file_type)})
24
+ JSON.parse(result.body)
25
+ end
26
+
27
+ # Deletes the photo for the resource with id _resource_id_.
28
+ def delete_photo(client, resource_id)
29
+ client.http_delete "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
30
+ end
31
+ end
32
+
33
+ # Returns a Hash with urls for the small and large versions of the photo for this resource.
34
+ #
35
+ # User.find(@client, "me").photo #=> {"smallPhotoUrl"=>"/small/photo/url", "largePhotoUrl"=>"/large/photo/url"}
36
+ def photo
37
+ self.raw_hash["photo"]
38
+ end
39
+
40
+ # Uploads a photo for this resource.
41
+ #
42
+ # me = User.find(@client)
43
+ # me.upload_photo(File.open("SomePicture.png"), "image/png")
44
+ def upload_photo(io, file_type)
45
+ self.class.upload_photo(self.client, self.id, io, file_type)
46
+ end
47
+
48
+ # Deletes the photo for this resource.
49
+ def delete_photo
50
+ self.class.delete_photo(self.client, self.id)
51
+ photo
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,122 @@
1
+ require 'json'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # Superclasses all Chatter resources except feeds. Some methods may not be supported by the Force.com API for certain subclasses.
6
+ class Record
7
+ attr_reader :raw_hash, :name, :id, :url, :type, :client
8
+
9
+ # Create a new record from the returned JSON response of an API request. Sets the client, name, id, url, and type attributes. Saves the raw response as +raw_hash+.
10
+ def initialize(client, response)
11
+ @client = client
12
+ @raw_hash = response.is_a?(Hash) ? response : JSON.parse(response)
13
+ @name = @raw_hash["name"]
14
+ @id = @raw_hash["id"]
15
+ @url = @raw_hash["url"]
16
+ @type = @raw_hash["type"]
17
+ end
18
+
19
+ # Find a single Record or a Collection of records by id. _resource_id_ can be a single id or a list of ids.
20
+ def self.find(client, resource_id, parameters={})
21
+ if resource_id.is_a?(Array)
22
+ resource_ids = resource_id.join(',')
23
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/batch/#{resource_ids}"
24
+ response = JSON.parse(client.http_get(url, parameters).body)
25
+ good_results = response["results"].select { |r| r["statusCode"] == 200 }
26
+ collection = Databasedotcom::Collection.new(client, good_results.length)
27
+ good_results.each do |result|
28
+ collection << self.new(client, result["result"])
29
+ end
30
+ collection
31
+ else
32
+ path_components = ["/services/data/v#{client.version}/chatter"]
33
+ if parameters.has_key?(:user_id)
34
+ path_components << "users/#{parameters[:user_id]}"
35
+ parameters.delete(:user_id)
36
+ end
37
+ path_components << "#{self.resource_name}/#{resource_id}"
38
+ url = path_components.join('/')
39
+ response = JSON.parse(client.http_get(url, parameters).body)
40
+ self.new(client, response)
41
+ end
42
+ end
43
+
44
+ # Return a Collection of records that match the _query_.
45
+ def self.search(client, query, parameters={})
46
+ self.all(client, parameters.merge(self.search_parameter_name => query))
47
+ end
48
+
49
+ # Return a Collection of all records.
50
+ def self.all(client, parameters={})
51
+ path_components = ["/services/data/v#{client.version}/chatter"]
52
+ if parameters.has_key?(:user_id)
53
+ path_components << "users/#{parameters[:user_id]}"
54
+ parameters.delete(:user_id)
55
+ end
56
+ path_components << self.resource_name
57
+ url = path_components.join('/')
58
+ result = client.http_get(url, parameters)
59
+ response = JSON.parse(result.body)
60
+ collection = Databasedotcom::Collection.new(client, self.total_size_of_collection(response), response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
61
+ self.collection_from_response(response).each do |resource|
62
+ collection << self.new(client, resource)
63
+ end
64
+ collection
65
+ end
66
+
67
+ # Delete the Record identified by _resource_id_.
68
+ def self.delete(client, resource_id, parameters={})
69
+ path_components = ["/services/data/v#{client.version}/chatter"]
70
+ if parameters.has_key?(:user_id)
71
+ path_components << "users/#{parameters[:user_id]}"
72
+ parameters.delete(:user_id)
73
+ end
74
+ path_components << self.resource_name
75
+ path_components << resource_id
76
+ path = path_components.join('/')
77
+ client.http_delete(path, parameters)
78
+ end
79
+
80
+ # A Hash representation of the User that created this Record.
81
+ def user
82
+ self.raw_hash["user"]
83
+ end
84
+
85
+ # A Hash representation of the entity that is the parent of this Record.
86
+ def parent
87
+ self.raw_hash["parent"]
88
+ end
89
+
90
+ # Delete this record.
91
+ def delete(parameters={})
92
+ self.class.delete(self.client, self.id, parameters)
93
+ end
94
+
95
+ # Reload this record.
96
+ def reload
97
+ self.class.find(self.client, self.id)
98
+ end
99
+
100
+ # The REST resource name of this Record.
101
+ #
102
+ # GroupMembership.resource_name #=> group-memberships
103
+ def self.resource_name
104
+ (self.name.split('::').last).resourcerize + "s"
105
+ end
106
+
107
+ protected
108
+
109
+ def self.total_size_of_collection(response)
110
+ response["total"] || response["totalMemberCount"]
111
+ end
112
+
113
+ def self.collection_from_response(response)
114
+ response[self.resource_name]
115
+ end
116
+
117
+ def self.search_parameter_name
118
+ :q
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A representation of a user "following" some other entity.
6
+ class Subscription < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,153 @@
1
+ require 'databasedotcom/chatter/record'
2
+ require 'databasedotcom/chatter/photo_methods'
3
+
4
+ module Databasedotcom
5
+ module Chatter
6
+ # Defines a User in your org.
7
+ class User < Record
8
+ include PhotoMethods
9
+
10
+ # Returns a Collection of Subscription objects that represents all followers of the User identified by _subject_id_.
11
+ def self.followers(client, subject_id="me")
12
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/followers"
13
+ result = client.http_get(url)
14
+ response = JSON.parse(result.body)
15
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16
+ response["followers"].each do |subscription|
17
+ collection << Subscription.new(client, subscription)
18
+ end
19
+ collection
20
+ end
21
+
22
+ # Returns a Collection of Subscription objects that represent all entities that the User identified by _subject_id_ is following.
23
+ def self.following(client, subject_id="me")
24
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/following"
25
+ result = client.http_get(url)
26
+ response = JSON.parse(result.body)
27
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
28
+ response["following"].each do |subscription|
29
+ collection << Subscription.new(client, subscription)
30
+ end
31
+ collection
32
+ end
33
+
34
+ # Returns a Collection of Group objects that represent all the groups that the User identified by _subject_id_ is a part of.
35
+ def self.groups(client, subject_id="me")
36
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/groups"
37
+ result = client.http_get(url)
38
+ response = JSON.parse(result.body)
39
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
40
+ response["groups"].each do |group|
41
+ collection << Group.new(client, group)
42
+ end
43
+ collection
44
+ end
45
+
46
+ # Returns the current status of the User identified by _subject_id_.
47
+ def self.status(client, subject_id="me")
48
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
49
+ result = client.http_get(url)
50
+ JSON.parse(result.body)
51
+ end
52
+
53
+ # Posts a status update as the User identified by _subject_id_ with content _text_.
54
+ def self.post_status(client, subject_id, text)
55
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
56
+ result = client.http_post(url, nil, :text => text)
57
+ JSON.parse(result.body)
58
+ end
59
+
60
+ # Deletes the status of User identified by _subject_id_.
61
+ def self.delete_status(client, subject_id="me")
62
+ client.http_delete "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
63
+ end
64
+
65
+ # Creates and returns a new Subscription object that represents the User identified by _subject_id_ following the resource identified by _resource_id_.
66
+ def self.follow(client, subject_id, resource_id)
67
+ response = client.http_post("/services/data/v#{client.version}/chatter/users/#{subject_id}/following", nil, :subjectId => resource_id)
68
+ Subscription.new(client, response.body)
69
+ end
70
+
71
+ # Returns a Collection of conversations that belong to the User identified by _subject_id_.
72
+ def self.conversations(client, subject_id)
73
+ Conversation.all(client, :user_id => subject_id)
74
+ end
75
+
76
+ # Returns a Collection of private messages that belong to the User identified by _subject_id_.
77
+ def self.messages(client, subject_id)
78
+ Message.all(client, :user_id => subject_id)
79
+ end
80
+
81
+ # Get a Collection of Subscription objects for this User. Always makes a call to the server.
82
+ def followers!
83
+ self.class.followers(self.client, self.id)
84
+ end
85
+
86
+ # Get a Collection of Subscription objects for this User. Returns cached data if it has been called before.
87
+ def followers
88
+ @followers ||= followers!
89
+ end
90
+
91
+ # Get a Collection of Subscription objects that represents all resources that this User is following. Always makes a call to the server.
92
+ def following!
93
+ self.class.following(self.client, self.id)
94
+ end
95
+
96
+ # Get a Collection of Subscription objects that represents all resources that this User is following. Returns cached data if it has been called before.
97
+ def following
98
+ @following ||= following!
99
+ end
100
+
101
+ # Returns this current status of this User.
102
+ def status
103
+ self.raw_hash["currentStatus"]
104
+ end
105
+
106
+ # Posts a new status with content _text_ for this User.
107
+ def post_status(text)
108
+ self.class.post_status(self.client, self.id, text)
109
+ end
110
+
111
+ # Deletes the current status of this User. Returns the deleted status.
112
+ def delete_status
113
+ self.class.delete_status(self.client, self.id)
114
+ status
115
+ end
116
+
117
+ # Get a Collection of Group objects that represents all groups that this User is in. Always makes a call to the server.
118
+ def groups!
119
+ self.class.groups(self.client, self.id)
120
+ end
121
+
122
+ # Get a Collection of Group objects that represents all groups that this User is in. Returns cached data if it has been called before.
123
+ def groups
124
+ @groups ||= groups!
125
+ end
126
+
127
+ # Creates a new Subscription that represents this User following the resource with id _record_id_.
128
+ def follow(record_id)
129
+ self.class.follow(self.client, self.id, record_id)
130
+ end
131
+
132
+ # Get a Collection of Conversation objects that represents the conversations for this User. Always makes a call to the server.
133
+ def conversations!
134
+ self.class.conversations(self.client, self.id)
135
+ end
136
+
137
+ # Get a Collection of Conversation objects that represents the conversations for this User. Returns cached data if it has been called before.
138
+ def conversations
139
+ @conversations ||= conversations!
140
+ end
141
+
142
+ # Get a Collection of Message objects that represents the messages for this User. Always makes a call to the server.
143
+ def messages!
144
+ self.class.messages(self.client, self.id)
145
+ end
146
+
147
+ # Get a Collection of Message objects that represents the messages for this User. Returns cached data if it has been called before.
148
+ def messages
149
+ @messages ||= messages!
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,421 @@
1
+ require 'net/https'
2
+ require 'json'
3
+ require 'net/http/post/multipart'
4
+
5
+ module Databasedotcom
6
+ # Interface for operating the Force.com REST API
7
+ class Client
8
+ # The client id (aka "Consumer Key") to use for OAuth2 authentication
9
+ attr_accessor :client_id
10
+ # The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
11
+ attr_accessor :client_secret
12
+ # The OAuth access token in use by the client
13
+ attr_accessor :oauth_token
14
+ # The base URL to the authenticated user's SalesForce instance
15
+ attr_accessor :instance_url
16
+ # If true, print API debugging information to stdout. Defaults to false.
17
+ attr_accessor :debugging
18
+ # The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
19
+ attr_accessor :host
20
+ # The API version the client is using. Defaults to 23.0
21
+ attr_accessor :version
22
+ # A Module in which to materialize Sobject classes. Defaults to the global module (Object)
23
+ attr_accessor :sobject_module
24
+ # The SalesForce user id of the authenticated user
25
+ attr_reader :user_id
26
+ # The SalesForce username
27
+ attr_accessor :username
28
+ # The SalesForce password
29
+ attr_accessor :password
30
+
31
+ # Returns a new client object. _options_ can be one of the following
32
+ #
33
+ # * A String containing the name of a YAML file formatted like:
34
+ # ---
35
+ # client_id: <your_salesforce_client_id>
36
+ # client_secret: <your_salesforce_client_secret>
37
+ # host: login.salesforce.com
38
+ # debugging: true
39
+ # version: 23.0
40
+ # sobject_module: My::Module
41
+ # * A Hash containing the following keys:
42
+ # client_id
43
+ # client_secret
44
+ # host
45
+ # debugging
46
+ # version
47
+ # sobject_module
48
+ # If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST,
49
+ # DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, and/or DATABASEDOTCOM_SOBJECT_MODULE are present, they
50
+ # override any other values provided
51
+ def initialize(options = {})
52
+ if options.is_a?(String)
53
+ @options = YAML.load_file(options)
54
+ else
55
+ @options = options
56
+ end
57
+ @options.symbolize_keys!
58
+
59
+ if ENV['DATABASE_COM_URL']
60
+ url = URI.parse(ENV['DATABASE_COM_URL'])
61
+ url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
62
+ self.host = url.host
63
+ self.client_id = url_options[:oauth_key]
64
+ self.client_secret = url_options[:oauth_secret]
65
+ self.username = url_options[:user]
66
+ self.password = url_options[:password]
67
+ self.sobject_module = "Databasedotcom::Sobject"
68
+ else
69
+ self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
70
+ self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
71
+ self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
72
+ self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
73
+ self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
74
+ self.version = self.version.to_s if self.version
75
+ self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || (@options && @options[:sobject_module])
76
+ end
77
+ end
78
+
79
+ # Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
80
+ #
81
+ # * 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
82
+ # * 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
83
+ # * 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
84
+ #
85
+ # Raises SalesForceError if an error occurs
86
+ def authenticate(options = nil)
87
+ if (options[:username] && options[:password])
88
+ req = Net::HTTP.new(self.host, 443)
89
+ req.use_ssl=true
90
+ path = "/services/oauth2/token?grant_type=password&client_id=#{self.client_id}&client_secret=#{client_secret}&username=#{options[:username]}&password=#{options[:password]}"
91
+ log_request("https://#{self.host}/#{path}")
92
+ result = req.post(path, "")
93
+ log_response(result)
94
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
95
+ json = JSON.parse(result.body)
96
+ @user_id = json["id"].match(/\/([^\/]+)$/)[1] rescue nil
97
+ self.instance_url = json["instance_url"]
98
+ self.oauth_token = json["access_token"]
99
+ elsif options.is_a?(Hash)
100
+ if options.has_key?("provider")
101
+ @user_id = options["extra"]["user_hash"]["user_id"] rescue nil
102
+ self.instance_url = options["credentials"]["instance_url"]
103
+ self.oauth_token = options["credentials"]["token"]
104
+ else
105
+ raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
106
+ self.instance_url = options[:instance_url]
107
+ self.oauth_token = options[:token]
108
+ end
109
+ end
110
+
111
+ self.version = "23.0" unless self.version
112
+
113
+ self.oauth_token
114
+ end
115
+
116
+ # Returns an Array of Strings listing the class names for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
117
+ def list_sobjects
118
+ result = http_get("/services/data/v#{self.version}/sobjects")
119
+ if result.is_a?(Net::HTTPOK)
120
+ JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
121
+ elsif result.is_a?(Net::HTTPBadRequest)
122
+ raise SalesForceError.new(result)
123
+ end
124
+ end
125
+
126
+ # 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.
127
+ #
128
+ # client.materialize("Contact") #=> Contact
129
+ # client.materialize(%w(Contact Company)) #=> [Contact, Company]
130
+ #
131
+ # 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.
132
+ def materialize(classnames)
133
+ classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
134
+ original_classname = clazz
135
+ clazz = original_classname[0].capitalize + original_classname[1..-1]
136
+ unless module_namespace.const_defined?(clazz)
137
+ new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
138
+ new_class.client = self
139
+ new_class.materialize(original_classname)
140
+ new_class
141
+ else
142
+ module_namespace.const_get(clazz)
143
+ end
144
+ end
145
+
146
+ classes.length == 1 ? classes.first : classes
147
+ end
148
+
149
+ # Returns a description of the Sobject specified by _class_name_. The description includes all fields and their properties for the Sobject.
150
+ def describe_sobject(class_name)
151
+ result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
152
+ JSON.parse(result.body)
153
+ end
154
+
155
+ # 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_.
156
+ # If given a Class that is not defined, it will attempt to materialize the class on demand.
157
+ #
158
+ # client.find(Account, "recordid") #=> #<Account @Id="recordid", ...>
159
+ def find(class_or_classname, record_id)
160
+ class_or_classname = find_or_materialize(class_or_classname)
161
+ result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
162
+ response = JSON.parse(result.body)
163
+ new_record = class_or_classname.new
164
+ class_or_classname.description["fields"].each do |field|
165
+ set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
166
+ end
167
+ new_record
168
+ end
169
+
170
+ # 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.
171
+ #
172
+ # client.query("SELECT Name FROM Account") #=> [#<Account @Id=nil, @Name="Foo", ...>, #<Account @Id=nil, @Name="Bar", ...> ...]
173
+ def query(soql_expr)
174
+ result = http_get("/services/data/v#{self.version}/query?q=#{soql_expr}")
175
+ collection_from(result.body)
176
+ end
177
+
178
+ # 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.
179
+ #
180
+ # client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]
181
+ def search(sosl_expr)
182
+ result = http_get("/services/data/v#{self.version}/search?q=#{sosl_expr}")
183
+ collection_from(result.body)
184
+ end
185
+
186
+ # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.
187
+ def next_page(path)
188
+ result = http_get(path)
189
+ collection_from(result.body)
190
+ end
191
+
192
+ # Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.
193
+ def previous_page(path)
194
+ result = http_get(path)
195
+ collection_from(result.body)
196
+ end
197
+
198
+ # Returns a new instance of _class_or_classname_ (which can be passed in as either a String or a Class) with the specified attributes.
199
+ #
200
+ # client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #<Car @Id="recordid", @Color="Blue", @Year="2011">
201
+ def create(class_or_classname, object_attrs)
202
+ class_or_classname = find_or_materialize(class_or_classname)
203
+ json_for_assignment = coerced_json(object_attrs, class_or_classname)
204
+ result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
205
+ new_object = class_or_classname.new
206
+ JSON.parse(json_for_assignment).each do |property, value|
207
+ set_value(new_object, property, value, class_or_classname.type_map[property][:type])
208
+ end
209
+ id = JSON.parse(result.body)["id"]
210
+ set_value(new_object, "Id", id, "id")
211
+ new_object
212
+ end
213
+
214
+ # 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
215
+ #
216
+ # client.update("Car", "rid", {"Color" => "Red"})
217
+ def update(class_or_classname, record_id, new_attrs)
218
+ class_or_classname = find_or_materialize(class_or_classname)
219
+ json_for_update = coerced_json(new_attrs, class_or_classname)
220
+ http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
221
+ end
222
+
223
+ # 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.
224
+ # If not found, it will create a new record with _attrs_.
225
+ #
226
+ # client.upsert(Car, "Color", "Blue", {"Year" => "2012"})
227
+ def upsert(class_or_classname, field, value, attrs)
228
+ clazz = find_or_materialize(class_or_classname)
229
+ json_for_update = coerced_json(attrs, clazz)
230
+ http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
231
+ end
232
+
233
+ # Deletes the record of type _class_or_classname_ with id of _record_id_. _class_or_classname_ can be a String or a Class.
234
+ #
235
+ # client.delete(Car, "rid")
236
+ def delete(class_or_classname, record_id)
237
+ clazz = find_or_materialize(class_or_classname)
238
+ http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
239
+ end
240
+
241
+ # Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.
242
+ def recent
243
+ result = http_get("/services/data/v#{self.version}/recent")
244
+ collection_from(result.body)
245
+ end
246
+
247
+ # Returns an array of trending topic names.
248
+ def trending_topics
249
+ result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
250
+ result = JSON.parse(result.body)
251
+ result["topics"].collect { |topic| topic["name"] }
252
+ end
253
+
254
+ # Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
255
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
256
+ # HTTPSuccess- raises SalesForceError otherwise.
257
+ def http_get(path, parameters={}, headers={})
258
+ req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
259
+ req.use_ssl = true
260
+ path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
261
+ encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
262
+ log_request(encoded_path)
263
+ result = req.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
264
+ log_response(result)
265
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
266
+ result
267
+ end
268
+
269
+
270
+ # Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
271
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
272
+ # HTTPSuccess- raises SalesForceError otherwise.
273
+ def http_delete(path, parameters={}, headers={})
274
+ req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
275
+ req.use_ssl = true
276
+ path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
277
+ encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
278
+ log_request(encoded_path)
279
+ result = req.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
280
+ log_response(result)
281
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPNoContent)
282
+ result
283
+ end
284
+
285
+ # Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
286
+ # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
287
+ # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
288
+ def http_post(path, data=nil, parameters={}, headers={})
289
+ req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
290
+ req.use_ssl = true
291
+ path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
292
+ encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
293
+ log_request(encoded_path, data)
294
+ result = req.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
295
+ log_response(result)
296
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
297
+ result
298
+ end
299
+
300
+ # Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
301
+ # Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
302
+ # headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
303
+ def http_patch(path, data=nil, parameters={}, headers={})
304
+ req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
305
+ req.use_ssl = true
306
+ path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
307
+ encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
308
+ log_request(encoded_path, data)
309
+ result = req.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
310
+ log_response(result)
311
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
312
+ result
313
+ end
314
+
315
+ # Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
316
+ # The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
317
+ # +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
318
+ # Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
319
+ def http_multipart_post(path, parts, parameters={}, headers={})
320
+ req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
321
+ req.use_ssl = true
322
+ path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
323
+ encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
324
+ log_request(encoded_path)
325
+ result = req.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
326
+ log_response(result)
327
+ raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
328
+ result
329
+ end
330
+
331
+ private
332
+
333
+ def log_request(path, data=nil)
334
+ puts "***** REQUEST: #{path.include?(':') ? path : URI.join(self.instance_url, path)}#{data ? " => #{data}" : ''}" if self.debugging
335
+ end
336
+
337
+ def log_response(result)
338
+ puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging
339
+ end
340
+
341
+ def find_or_materialize(class_or_classname)
342
+ if class_or_classname.is_a?(Class)
343
+ clazz = class_or_classname
344
+ else
345
+ match = class_or_classname.match(/(?:(.+)::)?(\w+)$/)
346
+ preceding_namespace = match[1]
347
+ classname = match[2]
348
+ raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name
349
+ clazz = module_namespace.const_get(classname.to_sym) rescue nil
350
+ clazz ||= self.materialize(classname)
351
+ end
352
+ clazz
353
+ end
354
+
355
+ def module_namespace
356
+ self.sobject_module || Object
357
+ end
358
+
359
+ def collection_from(response)
360
+ response = JSON.parse(response)
361
+ array_response = response.is_a?(Array)
362
+ if array_response
363
+ records = response.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) }
364
+ else
365
+ records = response["records"].collect do |record|
366
+ attributes = record.delete('attributes')
367
+ new_record = find_or_materialize(attributes["type"]).new
368
+ record.each do |name, value|
369
+ field = new_record.description['fields'].find do |field|
370
+ key_from_label(field["label"]) == name || field["name"] == name
371
+ end
372
+ set_value(new_record, field["name"], value, field["type"]) if field
373
+ end
374
+ new_record
375
+ end
376
+ end
377
+
378
+ Databasedotcom::Collection.new(self, array_response ? records.length : response["totalSize"], array_response ? nil : response["nextRecordsUrl"]).concat(records)
379
+ end
380
+
381
+ def set_value(record, attr, value, attr_type)
382
+ value_to_set = value
383
+
384
+ case attr_type
385
+ when "datetime"
386
+ value_to_set = DateTime.parse(value) rescue nil
387
+
388
+ when "date"
389
+ value_to_set = Date.parse(value) rescue nil
390
+
391
+ when "multipicklist"
392
+ value_to_set = value.split(";") rescue []
393
+ end
394
+
395
+ record.send("#{attr}=", value_to_set)
396
+ end
397
+
398
+ def coerced_json(attrs, clazz)
399
+ if attrs.is_a?(Hash)
400
+ coerced_attrs = {}
401
+ attrs.keys.each do |key|
402
+ case clazz.field_type(key)
403
+ when "multipicklist"
404
+ coerced_attrs[key] = (attrs[key] || []).join(';')
405
+ when "datetime"
406
+ coerced_attrs[key] = attrs[key] ? attrs[key].strftime("%Y-%m-%dT%H:%M:%S.%L%z") : nil
407
+ else
408
+ coerced_attrs[key] = attrs[key]
409
+ end
410
+ end
411
+ coerced_attrs.to_json
412
+ else
413
+ attrs
414
+ end
415
+ end
416
+
417
+ def key_from_label(label)
418
+ label.gsub(' ', '_')
419
+ end
420
+ end
421
+ end