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.
Files changed (47) hide show
  1. checksums.yaml +8 -8
  2. data/lib/sfdc.rb +39 -1
  3. data/lib/sfdc/attachment.rb +23 -0
  4. data/lib/sfdc/chatter/comment.rb +10 -0
  5. data/lib/sfdc/chatter/conversation.rb +100 -0
  6. data/lib/sfdc/chatter/feed.rb +64 -0
  7. data/lib/sfdc/chatter/feed_item.rb +40 -0
  8. data/lib/sfdc/chatter/feeds.rb +5 -0
  9. data/lib/sfdc/chatter/filter_feed.rb +14 -0
  10. data/lib/sfdc/chatter/group.rb +45 -0
  11. data/lib/sfdc/chatter/group_membership.rb +9 -0
  12. data/lib/sfdc/chatter/like.rb +9 -0
  13. data/lib/sfdc/chatter/message.rb +29 -0
  14. data/lib/sfdc/chatter/photo_methods.rb +55 -0
  15. data/lib/sfdc/chatter/record.rb +122 -0
  16. data/lib/sfdc/chatter/subscription.rb +9 -0
  17. data/lib/sfdc/chatter/user.rb +153 -0
  18. data/lib/sfdc/client.rb +98 -0
  19. data/lib/sfdc/client/api.rb +320 -0
  20. data/lib/sfdc/client/authentication.rb +40 -0
  21. data/lib/sfdc/client/caching.rb +26 -0
  22. data/lib/sfdc/client/canvas.rb +12 -0
  23. data/lib/sfdc/client/connection.rb +74 -0
  24. data/lib/sfdc/client/picklists.rb +90 -0
  25. data/lib/sfdc/client/streaming.rb +31 -0
  26. data/lib/sfdc/client/verbs.rb +68 -0
  27. data/lib/sfdc/collection.rb +40 -0
  28. data/lib/sfdc/config.rb +136 -0
  29. data/lib/sfdc/mash.rb +65 -0
  30. data/lib/sfdc/middleware.rb +29 -0
  31. data/lib/sfdc/middleware/authentication.rb +63 -0
  32. data/lib/sfdc/middleware/authentication/password.rb +20 -0
  33. data/lib/sfdc/middleware/authentication/token.rb +15 -0
  34. data/lib/sfdc/middleware/authorization.rb +19 -0
  35. data/lib/sfdc/middleware/caching.rb +24 -0
  36. data/lib/sfdc/middleware/gzip.rb +31 -0
  37. data/lib/sfdc/middleware/instance_url.rb +18 -0
  38. data/lib/sfdc/middleware/logger.rb +40 -0
  39. data/lib/sfdc/middleware/mashify.rb +18 -0
  40. data/lib/sfdc/middleware/multipart.rb +53 -0
  41. data/lib/sfdc/middleware/raise_error.rb +23 -0
  42. data/lib/sfdc/signed_request.rb +48 -0
  43. data/lib/sfdc/sobject.rb +64 -0
  44. data/lib/sfdc/upload_io.rb +20 -0
  45. data/lib/sfdc/version.rb +1 -1
  46. metadata +59 -4
  47. 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