databasedotcom_cloudfuji 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,45 @@
1
+ require 'databasedotcom/chatter/record'
2
+ require 'databasedotcom/chatter/photo_methods'
3
+
4
+ module Databasedotcom
5
+ module Chatter
6
+ # A group of Users
7
+ class Group < Record
8
+ include PhotoMethods
9
+
10
+ # Returns a Collection of GroupMembership instances for the Group identified by _group_id_.
11
+ def self.members(client, group_id)
12
+ url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
13
+ result = client.http_get(url)
14
+ response = JSON.parse(result.body)
15
+ collection = Databasedotcom::Collection.new(client, response["totalMemberCount"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16
+ response["members"].each do |member|
17
+ collection << GroupMembership.new(client, member)
18
+ end
19
+ collection
20
+ end
21
+
22
+ # Join the group identified by _group_id_ as the user identified by _user_id_.
23
+ def self.join(client, group_id, user_id="me")
24
+ url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
25
+ response = client.http_post(url, nil, :userId => user_id)
26
+ GroupMembership.new(client, response.body)
27
+ end
28
+
29
+ # Get a Collection of GroupMembership objects for this Group. Always makes a call to the server.
30
+ def members!
31
+ self.class.members(self.client, self.id)
32
+ end
33
+
34
+ # Get a Collection of GroupMembership objects for this Group. Returns cached data if it has been called before.
35
+ def members
36
+ @members ||= members!
37
+ end
38
+
39
+ # Join this Group as the user identified by _user_id_.
40
+ def join(user_id="me")
41
+ self.class.join(self.client, self.id, user_id)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -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,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