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.

Files changed (69) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +161 -0
  7. data/Rakefile +10 -0
  8. data/lib/restforce.rb +18 -0
  9. data/lib/restforce/client.rb +287 -0
  10. data/lib/restforce/collection.rb +23 -0
  11. data/lib/restforce/config.rb +68 -0
  12. data/lib/restforce/mash.rb +61 -0
  13. data/lib/restforce/middleware.rb +30 -0
  14. data/lib/restforce/middleware/authentication.rb +32 -0
  15. data/lib/restforce/middleware/authentication/oauth.rb +22 -0
  16. data/lib/restforce/middleware/authentication/password.rb +27 -0
  17. data/lib/restforce/middleware/authorization.rb +19 -0
  18. data/lib/restforce/middleware/instance_url.rb +27 -0
  19. data/lib/restforce/middleware/mashify.rb +18 -0
  20. data/lib/restforce/middleware/raise_error.rb +18 -0
  21. data/lib/restforce/sobject.rb +41 -0
  22. data/lib/restforce/version.rb +3 -0
  23. data/restforce.gemspec +28 -0
  24. data/spec/fixtures/auth_error_response.json +1 -0
  25. data/spec/fixtures/auth_success_response.json +1 -0
  26. data/spec/fixtures/expired_session_response.json +1 -0
  27. data/spec/fixtures/reauth_success_response.json +1 -0
  28. data/spec/fixtures/refresh_error_response.json +1 -0
  29. data/spec/fixtures/refresh_success_response.json +7 -0
  30. data/spec/fixtures/services_data_success_response.json +12 -0
  31. data/spec/fixtures/sobject/create_success_response.json +5 -0
  32. data/spec/fixtures/sobject/delete_error_response.json +1 -0
  33. data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
  34. data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
  35. data/spec/fixtures/sobject/org_query_response.json +11 -0
  36. data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
  37. data/spec/fixtures/sobject/query_empty_response.json +5 -0
  38. data/spec/fixtures/sobject/query_error_response.json +4 -0
  39. data/spec/fixtures/sobject/query_paginated_first_page_response.json +12 -0
  40. data/spec/fixtures/sobject/query_paginated_last_page_response.json +11 -0
  41. data/spec/fixtures/sobject/query_success_response.json +36 -0
  42. data/spec/fixtures/sobject/recent_success_response.json +18 -0
  43. data/spec/fixtures/sobject/search_error_response.json +4 -0
  44. data/spec/fixtures/sobject/search_success_response.json +16 -0
  45. data/spec/fixtures/sobject/sobject_describe_error_response.json +4 -0
  46. data/spec/fixtures/sobject/sobject_describe_success_response.json +1304 -0
  47. data/spec/fixtures/sobject/sobject_find_error_response.json +4 -0
  48. data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
  49. data/spec/fixtures/sobject/upsert_created_success_response.json +2 -0
  50. data/spec/fixtures/sobject/upsert_error_response.json +1 -0
  51. data/spec/fixtures/sobject/upsert_multiple_error_response.json +1 -0
  52. data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
  53. data/spec/fixtures/sobject/write_error_response.json +6 -0
  54. data/spec/lib/client_spec.rb +214 -0
  55. data/spec/lib/collection_spec.rb +50 -0
  56. data/spec/lib/config_spec.rb +70 -0
  57. data/spec/lib/middleware/authentication/oauth_spec.rb +30 -0
  58. data/spec/lib/middleware/authentication/password_spec.rb +37 -0
  59. data/spec/lib/middleware/authentication_spec.rb +67 -0
  60. data/spec/lib/middleware/authorization_spec.rb +17 -0
  61. data/spec/lib/middleware/instance_url_spec.rb +48 -0
  62. data/spec/lib/middleware/mashify_spec.rb +28 -0
  63. data/spec/lib/middleware/raise_error_spec.rb +27 -0
  64. data/spec/lib/sobject_spec.rb +93 -0
  65. data/spec/spec_helper.rb +17 -0
  66. data/spec/support/basic_client.rb +35 -0
  67. data/spec/support/fixture_helpers.rb +20 -0
  68. data/spec/support/middleware.rb +33 -0
  69. 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
@@ -0,0 +1,3 @@
1
+ module Restforce
2
+ VERSION = "0.0.1"
3
+ 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"}