sfdc 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/lib/sfdc.rb +39 -1
- data/lib/sfdc/attachment.rb +23 -0
- data/lib/sfdc/chatter/comment.rb +10 -0
- data/lib/sfdc/chatter/conversation.rb +100 -0
- data/lib/sfdc/chatter/feed.rb +64 -0
- data/lib/sfdc/chatter/feed_item.rb +40 -0
- data/lib/sfdc/chatter/feeds.rb +5 -0
- data/lib/sfdc/chatter/filter_feed.rb +14 -0
- data/lib/sfdc/chatter/group.rb +45 -0
- data/lib/sfdc/chatter/group_membership.rb +9 -0
- data/lib/sfdc/chatter/like.rb +9 -0
- data/lib/sfdc/chatter/message.rb +29 -0
- data/lib/sfdc/chatter/photo_methods.rb +55 -0
- data/lib/sfdc/chatter/record.rb +122 -0
- data/lib/sfdc/chatter/subscription.rb +9 -0
- data/lib/sfdc/chatter/user.rb +153 -0
- data/lib/sfdc/client.rb +98 -0
- data/lib/sfdc/client/api.rb +320 -0
- data/lib/sfdc/client/authentication.rb +40 -0
- data/lib/sfdc/client/caching.rb +26 -0
- data/lib/sfdc/client/canvas.rb +12 -0
- data/lib/sfdc/client/connection.rb +74 -0
- data/lib/sfdc/client/picklists.rb +90 -0
- data/lib/sfdc/client/streaming.rb +31 -0
- data/lib/sfdc/client/verbs.rb +68 -0
- data/lib/sfdc/collection.rb +40 -0
- data/lib/sfdc/config.rb +136 -0
- data/lib/sfdc/mash.rb +65 -0
- data/lib/sfdc/middleware.rb +29 -0
- data/lib/sfdc/middleware/authentication.rb +63 -0
- data/lib/sfdc/middleware/authentication/password.rb +20 -0
- data/lib/sfdc/middleware/authentication/token.rb +15 -0
- data/lib/sfdc/middleware/authorization.rb +19 -0
- data/lib/sfdc/middleware/caching.rb +24 -0
- data/lib/sfdc/middleware/gzip.rb +31 -0
- data/lib/sfdc/middleware/instance_url.rb +18 -0
- data/lib/sfdc/middleware/logger.rb +40 -0
- data/lib/sfdc/middleware/mashify.rb +18 -0
- data/lib/sfdc/middleware/multipart.rb +53 -0
- data/lib/sfdc/middleware/raise_error.rb +23 -0
- data/lib/sfdc/signed_request.rb +48 -0
- data/lib/sfdc/sobject.rb +64 -0
- data/lib/sfdc/upload_io.rb +20 -0
- data/lib/sfdc/version.rb +1 -1
- metadata +59 -4
- data/lib/sfdc/sfdc.rb +0 -0
data/lib/sfdc/mash.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'hashie/mash'
|
2
|
+
|
3
|
+
module Sfdc
|
4
|
+
class Mash < Hashie::Mash
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Pass in an Array or Hash and it will be recursively converted into the
|
9
|
+
# appropriate Sfdc::Collection, Sfdc::SObject and
|
10
|
+
# Sfdc::Mash objects.
|
11
|
+
def build(val, client)
|
12
|
+
if val.is_a?(Array)
|
13
|
+
val.collect { |val| self.build(val, client) }
|
14
|
+
elsif val.is_a?(Hash)
|
15
|
+
self.klass(val).new(val, client)
|
16
|
+
else
|
17
|
+
val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# When passed a hash, it will determine what class is appropriate to
|
22
|
+
# represent the data.
|
23
|
+
def klass(val)
|
24
|
+
if val.has_key? 'records'
|
25
|
+
# When the hash has a records key, it should be considered a collection
|
26
|
+
# of sobject records.
|
27
|
+
Sfdc::Collection
|
28
|
+
elsif val.has_key? 'attributes'
|
29
|
+
if val['attributes']['type'] == 'Attachment'
|
30
|
+
Sfdc::Attachment
|
31
|
+
else
|
32
|
+
# When the hash contains an attributes key, it should be considered an
|
33
|
+
# sobject record
|
34
|
+
Sfdc::SObject
|
35
|
+
end
|
36
|
+
else
|
37
|
+
# Fallback to a standard Sfdc::Mash for everything else
|
38
|
+
Sfdc::Mash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(source_hash = nil, client = nil, default = nil, &blk)
|
45
|
+
@client = client
|
46
|
+
deep_update(source_hash) if source_hash
|
47
|
+
default ? super(default) : super(&blk)
|
48
|
+
end
|
49
|
+
|
50
|
+
def convert_value(val, duping=false)
|
51
|
+
case val
|
52
|
+
when self.class
|
53
|
+
val.dup
|
54
|
+
when ::Hash
|
55
|
+
val = val.dup if duping
|
56
|
+
self.class.klass(val).new(val, @client)
|
57
|
+
when Array
|
58
|
+
val.collect{ |e| convert_value(e) }
|
59
|
+
else
|
60
|
+
val
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Sfdc
|
2
|
+
# Base class that all middleware can extend. Provides some convenient helper
|
3
|
+
# functions.
|
4
|
+
class Middleware < Faraday::Middleware
|
5
|
+
autoload :RaiseError, 'sfdc/middleware/raise_error'
|
6
|
+
autoload :Authentication, 'sfdc/middleware/authentication'
|
7
|
+
autoload :Authorization, 'sfdc/middleware/authorization'
|
8
|
+
autoload :InstanceURL, 'sfdc/middleware/instance_url'
|
9
|
+
autoload :Multipart, 'sfdc/middleware/multipart'
|
10
|
+
autoload :Mashify, 'sfdc/middleware/mashify'
|
11
|
+
autoload :Caching, 'sfdc/middleware/caching'
|
12
|
+
autoload :Logger, 'sfdc/middleware/logger'
|
13
|
+
autoload :Gzip, 'sfdc/middleware/gzip'
|
14
|
+
|
15
|
+
def initialize(app, client, options)
|
16
|
+
@app, @client, @options = app, client, options
|
17
|
+
end
|
18
|
+
|
19
|
+
# Internal: Proxy to the client.
|
20
|
+
def client
|
21
|
+
@client
|
22
|
+
end
|
23
|
+
|
24
|
+
# Internal: Proxy to the client's faraday connection.
|
25
|
+
def connection
|
26
|
+
client.send(:connection)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Sfdc
|
2
|
+
# Faraday middleware that allows for on the fly authentication of requests.
|
3
|
+
# When a request fails (a status of 401 is returned), the middleware
|
4
|
+
# will attempt to either reauthenticate (username and password) or refresh
|
5
|
+
# the oauth access token (if a refresh token is present).
|
6
|
+
class Middleware::Authentication < Sfdc::Middleware
|
7
|
+
autoload :Password, 'sfdc/middleware/authentication/password'
|
8
|
+
autoload :Token, 'sfdc/middleware/authentication/token'
|
9
|
+
|
10
|
+
# Rescue from 401's, authenticate then raise the error again so the client
|
11
|
+
# can reissue the request.
|
12
|
+
def call(env)
|
13
|
+
@app.call(env)
|
14
|
+
rescue Sfdc::UnauthorizedError
|
15
|
+
authenticate!
|
16
|
+
raise
|
17
|
+
end
|
18
|
+
|
19
|
+
# Internal: Performs the authentication and returns the response body.
|
20
|
+
def authenticate!
|
21
|
+
response = connection.post '/services/oauth2/token' do |req|
|
22
|
+
req.body = encode_www_form(params)
|
23
|
+
end
|
24
|
+
raise Sfdc::AuthenticationError, error_message(response) if response.status != 200
|
25
|
+
@options[:instance_url] = response.body['instance_url']
|
26
|
+
@options[:oauth_token] = response.body['access_token']
|
27
|
+
response.body
|
28
|
+
end
|
29
|
+
|
30
|
+
# Internal: The params to post to the OAuth service.
|
31
|
+
def params
|
32
|
+
raise 'not implemented'
|
33
|
+
end
|
34
|
+
|
35
|
+
# Internal: Faraday connection to use when sending an authentication request.
|
36
|
+
def connection
|
37
|
+
@connection ||= Faraday.new(:url => "https://#{@options[:host]}") do |builder|
|
38
|
+
builder.use Sfdc::Middleware::Mashify, nil, @options
|
39
|
+
builder.response :json
|
40
|
+
builder.use Sfdc::Middleware::Logger, Sfdc.configuration.logger, @options if Sfdc.log?
|
41
|
+
builder.adapter Faraday.default_adapter
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Internal: The parsed error response.
|
46
|
+
def error_message(response)
|
47
|
+
"#{response.body['error']}: #{response.body['error_description']}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Featured detect form encoding.
|
51
|
+
# URI in 1.8 does not include encode_www_form
|
52
|
+
def encode_www_form(params)
|
53
|
+
if URI.respond_to?(:encode_www_form)
|
54
|
+
URI.encode_www_form(params)
|
55
|
+
else
|
56
|
+
params.map do |k, v|
|
57
|
+
k, v = CGI.escape(k.to_s), CGI.escape(v.to_s)
|
58
|
+
"#{k}=#{v}"
|
59
|
+
end.join('&')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Sfdc
|
2
|
+
|
3
|
+
# Authentication middleware used if username and password flow is used
|
4
|
+
class Middleware::Authentication::Password < Sfdc::Middleware::Authentication
|
5
|
+
|
6
|
+
def params
|
7
|
+
{ :grant_type => 'password',
|
8
|
+
:client_id => @options[:client_id],
|
9
|
+
:client_secret => @options[:client_secret],
|
10
|
+
:username => @options[:username],
|
11
|
+
:password => password }
|
12
|
+
end
|
13
|
+
|
14
|
+
def password
|
15
|
+
"#{@options[:password]}#{@options[:security_token]}"
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Sfdc
|
2
|
+
|
3
|
+
# Authentication middleware used if oauth_token and refresh_token are set
|
4
|
+
class Middleware::Authentication::Token < Sfdc::Middleware::Authentication
|
5
|
+
|
6
|
+
def params
|
7
|
+
{ :grant_type => 'refresh_token',
|
8
|
+
:refresh_token => @options[:refresh_token],
|
9
|
+
:client_id => @options[:client_id],
|
10
|
+
:client_secret => @options[:client_secret] }
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Sfdc
|
2
|
+
|
3
|
+
# Piece of middleware that simply injects the OAuth token into the request
|
4
|
+
# headers.
|
5
|
+
class Middleware::Authorization < Sfdc::Middleware
|
6
|
+
AUTH_HEADER = 'Authorization'.freeze
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
env[:request_headers][AUTH_HEADER] = %(OAuth #{token})
|
10
|
+
@app.call(env)
|
11
|
+
end
|
12
|
+
|
13
|
+
def token
|
14
|
+
@options[:oauth_token]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Sfdc
|
2
|
+
class Middleware::Caching < FaradayMiddleware::Caching
|
3
|
+
|
4
|
+
def call(env)
|
5
|
+
expire(cache_key(env)) unless use_cache?
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def expire(key)
|
10
|
+
cache.delete(key) if cache
|
11
|
+
end
|
12
|
+
|
13
|
+
# We don't want to cache requests for different clients, so append the
|
14
|
+
# oauth token to the cache key.
|
15
|
+
def cache_key(env)
|
16
|
+
super(env) + env[:request_headers][Sfdc::Middleware::Authorization::AUTH_HEADER].gsub(/\s/, '')
|
17
|
+
end
|
18
|
+
|
19
|
+
def use_cache?
|
20
|
+
!@options.has_key?(:use_cache) || @options[:use_cache]
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
module Sfdc
|
4
|
+
# Middleware to uncompress GZIP compressed responses from Salesforce.
|
5
|
+
class Middleware::Gzip < Sfdc::Middleware
|
6
|
+
ACCEPT_ENCODING_HEADER = 'Accept-Encoding'.freeze
|
7
|
+
CONTENT_ENCODING_HEADER = 'Content-Encoding'.freeze
|
8
|
+
ENCODING = 'gzip'.freeze
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env[:request_headers][ACCEPT_ENCODING_HEADER] = ENCODING if @options[:compress]
|
12
|
+
@app.call(env).on_complete do |environment|
|
13
|
+
on_complete(environment)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_complete(env)
|
18
|
+
env[:body] = decompress(env[:body]) if gzipped?(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Internal: Returns true if the response is gzipped.
|
22
|
+
def gzipped?(env)
|
23
|
+
env[:response_headers][CONTENT_ENCODING_HEADER] == ENCODING
|
24
|
+
end
|
25
|
+
|
26
|
+
# Internal: Decompresses a gzipped string.
|
27
|
+
def decompress(body)
|
28
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sfdc
|
2
|
+
|
3
|
+
# Middleware which asserts that the instance_url is always set
|
4
|
+
class Middleware::InstanceURL < Sfdc::Middleware
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
# If the connection url_prefix isn't set, we must not be authenticated.
|
8
|
+
raise Sfdc::UnauthorizedError, 'Connection prefix not set' unless url_prefix_set?
|
9
|
+
@app.call(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
def url_prefix_set?
|
13
|
+
!!(connection.url_prefix && connection.url_prefix.host)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Sfdc
|
4
|
+
class Middleware::Logger < Faraday::Response::Middleware
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def initialize(app, logger, options)
|
8
|
+
super(app)
|
9
|
+
@options = options
|
10
|
+
@logger = logger || begin
|
11
|
+
require 'logger'
|
12
|
+
::Logger.new(STDOUT)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
debug('request') do
|
20
|
+
dump :url => env[:url].to_s,
|
21
|
+
:method => env[:method],
|
22
|
+
:headers => env[:request_headers],
|
23
|
+
:body => env[:body]
|
24
|
+
end
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_complete(env)
|
29
|
+
debug('response') do
|
30
|
+
dump :status => env[:status].to_s,
|
31
|
+
:headers => env[:response_headers],
|
32
|
+
:body => env[:body]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def dump(hash)
|
37
|
+
"\n" + hash.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sfdc
|
2
|
+
# Middleware the converts sobject records from JSON into Sfdc::SObject objects
|
3
|
+
# and collections of records into Sfdc::Collection objects.
|
4
|
+
class Middleware::Mashify < Sfdc::Middleware
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
@env = env
|
8
|
+
response = @app.call(env)
|
9
|
+
env[:body] = Sfdc::Mash.build(body, client)
|
10
|
+
response
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
@env[:body]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Sfdc
|
2
|
+
class Middleware::Multipart < Faraday::Request::UrlEncoded
|
3
|
+
self.mime_type = 'multipart/form-data'.freeze
|
4
|
+
DEFAULT_BOUNDARY = "--boundary_string".freeze
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
match_content_type(env) do |params|
|
8
|
+
env[:request] ||= {}
|
9
|
+
env[:request][:boundary] ||= DEFAULT_BOUNDARY
|
10
|
+
env[:request_headers][CONTENT_TYPE] += ";boundary=#{env[:request][:boundary]}"
|
11
|
+
env[:body] = create_multipart(env, params)
|
12
|
+
end
|
13
|
+
@app.call env
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_request?(env)
|
17
|
+
type = request_type(env)
|
18
|
+
env[:body].respond_to?(:each_key) and !env[:body].empty? and (
|
19
|
+
(type.empty? and has_multipart?(env[:body])) or
|
20
|
+
type == self.class.mime_type
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_multipart?(obj)
|
25
|
+
# string is an enum in 1.8, returning list of itself
|
26
|
+
if obj.respond_to?(:each) && !obj.is_a?(String)
|
27
|
+
(obj.respond_to?(:values) ? obj.values : obj).each do |val|
|
28
|
+
return true if (val.respond_to?(:content_type) || has_multipart?(val))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_multipart(env, params)
|
35
|
+
boundary = env[:request][:boundary]
|
36
|
+
parts = []
|
37
|
+
|
38
|
+
# Fields
|
39
|
+
parts << Faraday::Parts::Part.new(boundary, 'entity_content', params.reject { |k,v| v.respond_to? :content_type }.to_json)
|
40
|
+
|
41
|
+
# Files
|
42
|
+
params.each do |k,v|
|
43
|
+
parts << Faraday::Parts::Part.new(boundary, k.to_s, v) if v.respond_to? :content_type
|
44
|
+
end
|
45
|
+
|
46
|
+
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
47
|
+
|
48
|
+
body = Faraday::CompositeReadIO.new(parts)
|
49
|
+
env[:request_headers]['Content-Length'] = body.length.to_s
|
50
|
+
return body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sfdc
|
2
|
+
class Middleware::RaiseError < Faraday::Response::Middleware
|
3
|
+
def on_complete(env)
|
4
|
+
@env = env
|
5
|
+
case env[:status]
|
6
|
+
when 404
|
7
|
+
raise Faraday::Error::ResourceNotFound, message
|
8
|
+
when 401
|
9
|
+
raise Sfdc::UnauthorizedError, message
|
10
|
+
when 400...600
|
11
|
+
raise Faraday::Error::ClientError, message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
"#{body.first['errorCode']}: #{body.first['message']}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
JSON.parse(@env[:body])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Sfdc
|
6
|
+
class SignedRequest
|
7
|
+
# Public: Initializes and decodes the signed request
|
8
|
+
#
|
9
|
+
# signed_request - The POST message containing the signed request from Salesforce.
|
10
|
+
# client_secret - The oauth client secret used to encrypt the signed request.
|
11
|
+
#
|
12
|
+
# Returns the parsed JSON context.
|
13
|
+
def self.decode(signed_request, client_secret)
|
14
|
+
new(signed_request, client_secret).decode
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(signed_request, client_secret)
|
18
|
+
@client_secret = client_secret
|
19
|
+
split_components(signed_request)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Decode the signed request.
|
23
|
+
#
|
24
|
+
# Returns the parsed JSON context.
|
25
|
+
# Returns nil if the signed request is invalid.
|
26
|
+
def decode
|
27
|
+
return nil if signature != hmac
|
28
|
+
JSON.parse(Base64.decode64(payload))
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
attr_reader :client_secret, :signature, :payload
|
33
|
+
|
34
|
+
def split_components(signed_request)
|
35
|
+
@signature, @payload = signed_request.split('.')
|
36
|
+
@signature = Base64.decode64(@signature)
|
37
|
+
end
|
38
|
+
|
39
|
+
def hmac
|
40
|
+
OpenSSL::HMAC.digest(digest, client_secret, payload)
|
41
|
+
end
|
42
|
+
|
43
|
+
def digest
|
44
|
+
OpenSSL::Digest::Digest.new('sha256')
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|