restforce 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of restforce might be problematic. Click here for more details.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +161 -0
- data/Rakefile +10 -0
- data/lib/restforce.rb +18 -0
- data/lib/restforce/client.rb +287 -0
- data/lib/restforce/collection.rb +23 -0
- data/lib/restforce/config.rb +68 -0
- data/lib/restforce/mash.rb +61 -0
- data/lib/restforce/middleware.rb +30 -0
- data/lib/restforce/middleware/authentication.rb +32 -0
- data/lib/restforce/middleware/authentication/oauth.rb +22 -0
- data/lib/restforce/middleware/authentication/password.rb +27 -0
- data/lib/restforce/middleware/authorization.rb +19 -0
- data/lib/restforce/middleware/instance_url.rb +27 -0
- data/lib/restforce/middleware/mashify.rb +18 -0
- data/lib/restforce/middleware/raise_error.rb +18 -0
- data/lib/restforce/sobject.rb +41 -0
- data/lib/restforce/version.rb +3 -0
- data/restforce.gemspec +28 -0
- data/spec/fixtures/auth_error_response.json +1 -0
- data/spec/fixtures/auth_success_response.json +1 -0
- data/spec/fixtures/expired_session_response.json +1 -0
- data/spec/fixtures/reauth_success_response.json +1 -0
- data/spec/fixtures/refresh_error_response.json +1 -0
- data/spec/fixtures/refresh_success_response.json +7 -0
- data/spec/fixtures/services_data_success_response.json +12 -0
- data/spec/fixtures/sobject/create_success_response.json +5 -0
- data/spec/fixtures/sobject/delete_error_response.json +1 -0
- data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
- data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
- data/spec/fixtures/sobject/org_query_response.json +11 -0
- data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
- data/spec/fixtures/sobject/query_empty_response.json +5 -0
- data/spec/fixtures/sobject/query_error_response.json +4 -0
- data/spec/fixtures/sobject/query_paginated_first_page_response.json +12 -0
- data/spec/fixtures/sobject/query_paginated_last_page_response.json +11 -0
- data/spec/fixtures/sobject/query_success_response.json +36 -0
- data/spec/fixtures/sobject/recent_success_response.json +18 -0
- data/spec/fixtures/sobject/search_error_response.json +4 -0
- data/spec/fixtures/sobject/search_success_response.json +16 -0
- data/spec/fixtures/sobject/sobject_describe_error_response.json +4 -0
- data/spec/fixtures/sobject/sobject_describe_success_response.json +1304 -0
- data/spec/fixtures/sobject/sobject_find_error_response.json +4 -0
- data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
- data/spec/fixtures/sobject/upsert_created_success_response.json +2 -0
- data/spec/fixtures/sobject/upsert_error_response.json +1 -0
- data/spec/fixtures/sobject/upsert_multiple_error_response.json +1 -0
- data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
- data/spec/fixtures/sobject/write_error_response.json +6 -0
- data/spec/lib/client_spec.rb +214 -0
- data/spec/lib/collection_spec.rb +50 -0
- data/spec/lib/config_spec.rb +70 -0
- data/spec/lib/middleware/authentication/oauth_spec.rb +30 -0
- data/spec/lib/middleware/authentication/password_spec.rb +37 -0
- data/spec/lib/middleware/authentication_spec.rb +67 -0
- data/spec/lib/middleware/authorization_spec.rb +17 -0
- data/spec/lib/middleware/instance_url_spec.rb +48 -0
- data/spec/lib/middleware/mashify_spec.rb +28 -0
- data/spec/lib/middleware/raise_error_spec.rb +27 -0
- data/spec/lib/sobject_spec.rb +93 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/basic_client.rb +35 -0
- data/spec/support/fixture_helpers.rb +20 -0
- data/spec/support/middleware.rb +33 -0
- metadata +257 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Collection < Array
|
3
|
+
attr_reader :total_size, :next_page_url
|
4
|
+
|
5
|
+
def initialize(hash, client)
|
6
|
+
@client = client
|
7
|
+
@total_size = hash['totalSize']
|
8
|
+
@next_page_url = hash['nextRecordsUrl']
|
9
|
+
super(self.build(hash['records']))
|
10
|
+
end
|
11
|
+
|
12
|
+
# Converts an array of Hash's into an array of Restforce::SObject.
|
13
|
+
def build(array)
|
14
|
+
array.map { |record| Restforce::SObject.new(record, @client) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def next_page
|
18
|
+
response = @client.get next_page_url
|
19
|
+
response.body
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Restforce
|
4
|
+
class << self
|
5
|
+
attr_writer :log
|
6
|
+
|
7
|
+
# Returns the current Configuration
|
8
|
+
#
|
9
|
+
# Example
|
10
|
+
#
|
11
|
+
# Restforce.configuration.username = "username"
|
12
|
+
# Restforce.configuration.password = "password"
|
13
|
+
def configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Yields the Configuration
|
18
|
+
#
|
19
|
+
# Example
|
20
|
+
#
|
21
|
+
# Restforce.configure do |config|
|
22
|
+
# config.username = "username"
|
23
|
+
# config.password = "password"
|
24
|
+
# end
|
25
|
+
def configure
|
26
|
+
yield configuration
|
27
|
+
end
|
28
|
+
|
29
|
+
def log?
|
30
|
+
@log ||= false
|
31
|
+
end
|
32
|
+
|
33
|
+
def log(message)
|
34
|
+
return unless Restforce.log?
|
35
|
+
Restforce.configuration.logger.send :debug, message
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Configuration
|
40
|
+
attr_accessor :api_version
|
41
|
+
# The username to use during login.
|
42
|
+
attr_accessor :username
|
43
|
+
# The password to use during login.
|
44
|
+
attr_accessor :password
|
45
|
+
# The security token to use during login.
|
46
|
+
attr_accessor :security_token
|
47
|
+
# The OAuth client id
|
48
|
+
attr_accessor :client_id
|
49
|
+
# The OAuth client secret
|
50
|
+
attr_accessor :client_secret
|
51
|
+
# Set this to true if you're authenticating with a Sandbox instance.
|
52
|
+
# Defaults to false.
|
53
|
+
attr_accessor :host
|
54
|
+
|
55
|
+
attr_accessor :oauth_token
|
56
|
+
attr_accessor :refresh_token
|
57
|
+
attr_accessor :instance_url
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
@api_version ||= '24.0'
|
61
|
+
@host ||= 'login.salesforce.com'
|
62
|
+
end
|
63
|
+
|
64
|
+
def logger
|
65
|
+
@logger ||= ::Logger.new STDOUT
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'hashie/mash'
|
2
|
+
|
3
|
+
module Restforce
|
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 Restforce::Collection, Restforce::SObject and
|
10
|
+
# Restforce::Mash objects.
|
11
|
+
def build(val, client)
|
12
|
+
if val.is_a?(Array)
|
13
|
+
val.collect { |e| self.klass(e).new(e, 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
|
+
Restforce::Collection
|
28
|
+
elsif val.has_key? 'attributes'
|
29
|
+
# When the hash contains an attributes key, it should be considered an
|
30
|
+
# sobject record
|
31
|
+
Restforce::SObject
|
32
|
+
else
|
33
|
+
# Fallback to a standard Restforce::Mash for everything else
|
34
|
+
Restforce::Mash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(source_hash = nil, client = nil, default = nil, &blk)
|
41
|
+
@client = client
|
42
|
+
deep_update(source_hash) if source_hash
|
43
|
+
default ? super(default) : super(&blk)
|
44
|
+
end
|
45
|
+
|
46
|
+
def convert_value(val, duping=false)
|
47
|
+
case val
|
48
|
+
when self.class
|
49
|
+
val.dup
|
50
|
+
when ::Hash
|
51
|
+
val = val.dup if duping
|
52
|
+
self.class.klass(val).new(val, @client)
|
53
|
+
when Array
|
54
|
+
val.collect{ |e| convert_value(e) }
|
55
|
+
else
|
56
|
+
val
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Base class that all middleware can extend. Provides some convenient helper
|
4
|
+
# functions.
|
5
|
+
class Middleware < Faraday::Middleware
|
6
|
+
|
7
|
+
def initialize(app, client, options)
|
8
|
+
@app = app
|
9
|
+
@client = client
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def client
|
14
|
+
@client
|
15
|
+
end
|
16
|
+
|
17
|
+
def connection
|
18
|
+
client.send(:connection)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'restforce/middleware/raise_error'
|
25
|
+
require 'restforce/middleware/authentication'
|
26
|
+
require 'restforce/middleware/authentication/password'
|
27
|
+
require 'restforce/middleware/authentication/oauth'
|
28
|
+
require 'restforce/middleware/authorization'
|
29
|
+
require 'restforce/middleware/instance_url'
|
30
|
+
require 'restforce/middleware/mashify'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Faraday middleware that allows for on the fly authentication of requests.
|
4
|
+
# When a request fails (ie. A status of 401 is returned). The middleware
|
5
|
+
# will attempt to either reauthenticate (username and password) or refresh
|
6
|
+
# the oauth access token (if a refresh token is present).
|
7
|
+
class Middleware::Authentication < Restforce::Middleware
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
begin
|
11
|
+
@app.call(env)
|
12
|
+
rescue Restforce::UnauthorizedError
|
13
|
+
authenticate!
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def authenticate!
|
19
|
+
raise 'must subclass'
|
20
|
+
end
|
21
|
+
|
22
|
+
def connection
|
23
|
+
@connection ||= Faraday.new(:url => "https://#{@options[:host]}") do |builder|
|
24
|
+
builder.response :json
|
25
|
+
builder.response :logger, Restforce.configuration.logger if Restforce.log?
|
26
|
+
builder.adapter Faraday.default_adapter
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Authentication middleware used if oauth_token and refresh_token are set
|
4
|
+
class Middleware::Authentication::OAuth < Restforce::Middleware::Authentication
|
5
|
+
|
6
|
+
def authenticate!
|
7
|
+
response = connection.post '/services/oauth2/token' do |req|
|
8
|
+
req.body = URI.encode_www_form(
|
9
|
+
:grant_type => 'refresh_token',
|
10
|
+
:refresh_token => @options[:refresh_token],
|
11
|
+
:client_id => @options[:client_id],
|
12
|
+
:client_secret => @options[:client_secret]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
raise Restforce::AuthenticationError if response.status != 200
|
16
|
+
@options[:instance_url] = response.body['instance_url']
|
17
|
+
@options[:oauth_token] = response.body['access_token']
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Authentication middleware used if username and password flow is used
|
4
|
+
class Middleware::Authentication::Password < Restforce::Middleware::Authentication
|
5
|
+
|
6
|
+
def authenticate!
|
7
|
+
response = connection.post '/services/oauth2/token' do |req|
|
8
|
+
req.body = URI.encode_www_form(
|
9
|
+
:grant_type => 'password',
|
10
|
+
:client_id => @options[:client_id],
|
11
|
+
:client_secret => @options[:client_secret],
|
12
|
+
:username => @options[:username],
|
13
|
+
:password => password
|
14
|
+
)
|
15
|
+
end
|
16
|
+
raise Restforce::AuthenticationError if response.status != 200
|
17
|
+
@options[:instance_url] = response.body['instance_url']
|
18
|
+
@options[:oauth_token] = response.body['access_token']
|
19
|
+
end
|
20
|
+
|
21
|
+
def password
|
22
|
+
"#{@options[:password]}#{@options[:security_token]}"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Piece of middleware that simply injects the OAuth token into the request
|
4
|
+
# headers.
|
5
|
+
class Middleware::Authorization < Restforce::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,27 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
# Middleware which asserts that the instance_url is always set
|
4
|
+
class Middleware::InstanceURL < Restforce::Middleware
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
# If the instance url isn't set in options, raise a
|
8
|
+
# Restforce::UnauthorizedError to trigger reauthentication.
|
9
|
+
raise Restforce::UnauthorizedError, 'instance url not set' unless @options[:instance_url]
|
10
|
+
|
11
|
+
# If the url_prefix for the connection doesn't match the instance_url
|
12
|
+
# set in the options, we raise an error which gets caught outside of
|
13
|
+
# middleware, where the url_prefix is then set before retrying the
|
14
|
+
# request. It would be ideal if this could all be handled in
|
15
|
+
# middleware...
|
16
|
+
raise Restforce::InstanceURLError unless connection.url_prefix == instance_url
|
17
|
+
|
18
|
+
@app.call(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def instance_url
|
22
|
+
URI.parse(@options[:instance_url])
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Restforce
|
2
|
+
# Middleware the converts sobject records from JSON into Restforce::SObject objects
|
3
|
+
# and collections of records into Restforce::Collection objects.
|
4
|
+
class Middleware::Mashify < Restforce::Middleware
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
@env = env
|
8
|
+
response = @app.call(env)
|
9
|
+
env[:body] = Restforce::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,18 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Middleware::RaiseError < Faraday::Response::Middleware
|
3
|
+
def on_complete(env)
|
4
|
+
case env[:status]
|
5
|
+
when 404
|
6
|
+
raise Faraday::Error::ResourceNotFound, message(env)
|
7
|
+
when 401
|
8
|
+
raise Restforce::UnauthorizedError, message(env)
|
9
|
+
when 400...600
|
10
|
+
raise Faraday::Error::ClientError, message(env)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def message(env)
|
15
|
+
"#{env[:body].first['errorCode']}: #{env[:body].first['message']}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Restforce
|
2
|
+
class SObject < Restforce::Mash
|
3
|
+
|
4
|
+
def sobject_type
|
5
|
+
self.attributes.type
|
6
|
+
end
|
7
|
+
|
8
|
+
# Public: Persist the attributes to Salesforce.
|
9
|
+
#
|
10
|
+
# Examples
|
11
|
+
#
|
12
|
+
# account = client.query('select Id, Name from Account').first
|
13
|
+
# account.Name = 'Foobar'
|
14
|
+
# account.save
|
15
|
+
def save
|
16
|
+
# Remove 'attributes' and parent/child relationships. We only want to
|
17
|
+
# persist the attributes on the sobject.
|
18
|
+
ensure_id
|
19
|
+
attrs = self.to_hash.reject { |key, _| key =~ /.*__r/ || key =~ /^attributes$/ }
|
20
|
+
@client.update(sobject_type, attrs)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Destroy this record.
|
24
|
+
#
|
25
|
+
# Examples
|
26
|
+
#
|
27
|
+
# account = client.query('select Id, Name from Account').first
|
28
|
+
# account.destroy
|
29
|
+
def destroy
|
30
|
+
ensure_id
|
31
|
+
@client.destroy(sobject_type, self.Id)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def ensure_id
|
37
|
+
raise 'You need to query the Id for the record first.' unless self.Id?
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/restforce.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/restforce/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Eric J. Holmes"]
|
6
|
+
gem.email = ["eric@ejholmes.net"]
|
7
|
+
gem.description = %q{A lightweight ruby client for the Salesforce REST api.}
|
8
|
+
gem.summary = %q{A lightweight ruby client for the Salesforce REST api.}
|
9
|
+
gem.homepage = "https://github.com/ejholmes/restforce"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "restforce"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Restforce::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'rake'
|
19
|
+
gem.add_dependency 'faraday', '~> 0.8.4'
|
20
|
+
gem.add_dependency 'faraday_middleware', '~> 0.8.8'
|
21
|
+
gem.add_dependency 'json', '~> 1.7.5'
|
22
|
+
gem.add_dependency 'hashie', '~> 1.2.0'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'webmock'
|
26
|
+
gem.add_development_dependency 'mocha'
|
27
|
+
gem.add_development_dependency 'simplecov'
|
28
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
{"error":"invalid_grant","error_description":"authentication failure - Invalid Password"}
|