salesforce_adapter 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5db31804a0b4354b11b1e8cdd09ace23a9fc106
4
+ data.tar.gz: 15d3305e9a909da4ff4afed346144df47ddf8b7e
5
+ SHA512:
6
+ metadata.gz: 017c7c92736a178b66243ccbf7c8dbecf533dfc113a38b88d7c353117013d7e07040265c91b9c7c352d6d6db74ad31fbe560e3a731a7f1901951ea7c0324b14b
7
+ data.tar.gz: 28a734ff1747df86d47497c39cc5b33dab8fb35abe6ddc7bb63bc5bcb0f6b895c5ddb8c9501e2e59a0a5fbb0ee4415fdf5be568353d42290ffee05aedd1f6970
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # SalesforceAdapter
2
+
3
+ [![Build Status](https://travis-ci.org/mru2/salesforce_adapter.png?branch=master)](https://travis-ci.org/mru2/salesforce_adapter) [![Coverage Status](https://coveralls.io/repos/mru2/salesforce_adapter/badge.png)](https://coveralls.io/r/mru2/salesforce_adapter) [![Code Climate](https://codeclimate.com/github/mru2/salesforce_adapter.png)](https://codeclimate.com/github/mru2/salesforce_adapter)
4
+
5
+
6
+
7
+ Ruby client for the salesforce API
8
+ Actually, lightweight wrapper around the RForce gem
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'salesforce_adapter'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install salesforce_adapter
23
+
24
+ ## Usage
25
+
26
+ ```
27
+ require 'salesforce_adapter'
28
+
29
+ adapter = SalesforceAdapter::Base.new(
30
+ :url => 'https://test.salesforce.com/services/Soap/u/16.0',
31
+ :login => 'your_salesforce_login',
32
+ :password => 'your_salesforce_password'
33
+ )
34
+
35
+ > adapter.query("SELECT a.Id, a.Name FROM Account a WHERE Id='a_salesforce_id'")
36
+ => [ {:type => "Account", :Id => "a_salesforce_id", :Name => "an_account" } ]
37
+
38
+ > adapter.create(:Lead, {:type => 'Lead', :FirstName => 'Mace', :LastName => 'Windu', :Company => 'JEDI inc'})
39
+ => 'the_new_lead_id'
40
+
41
+ > adapter.update(:Lead, {:type => 'Lead', :Id => 'the_new_lead_id', :Company => 'One Arm Support Group'})
42
+ => 'the_new_lead_id'
43
+
44
+
45
+ > adapter.webservice('MyWebserviceClass').myMethod('foo')
46
+ => 'bar'
47
+ ```
48
+
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it ( http://github.com/<my-github-username>/salesforce_adapter/fork )
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
@@ -0,0 +1,90 @@
1
+ # Global helper methods used in the salesforce adapter
2
+
3
+ module SalesforceAdapter
4
+
5
+
6
+ # Exception : Salesforce API timeout
7
+ class SalesforceTimeout < StandardError ; end
8
+
9
+
10
+ module Helpers
11
+
12
+ class << self
13
+
14
+ # Attemps to run a salesforce operation a given number of times,
15
+ # Handle timeout errors and sleep between retries
16
+ def handle_timeout(opts= {}, &block)
17
+
18
+ max_tries = opts[:max_tries] || 1
19
+ sleep_between_tries = opts[:sleep_between_tries] || 1
20
+
21
+ counter = 0
22
+
23
+ begin
24
+ counter += 1
25
+ yield
26
+
27
+ rescue Timeout::Error => e
28
+ raise SalesforceAdapter::SalesforceTimeout.new(e.message) if counter >= max_tries
29
+
30
+ sleep sleep_between_tries
31
+ retry
32
+ end
33
+
34
+ end
35
+
36
+
37
+ # Format a fields hash in an array, to be sent with a create or update query
38
+ # Necessary to handle nil fields (must be submitted under the :fieldsToNull key, and there can be more than one)
39
+ def format_fields_for_create_or_update(fields)
40
+ fields_array = []
41
+ fields = fields.dup
42
+
43
+ # The type and Id have to be the first fields given
44
+ type = fields.delete(:type)
45
+ id = fields.delete(:Id)
46
+
47
+ fields_array << :type << type if type
48
+ fields_array << :Id << id if id
49
+
50
+ # Then append the other fields, handling the nil and boolean ones
51
+ fields.each do |key, value|
52
+ if value.nil?
53
+ fields_array << :fieldsToNull << key.to_s
54
+ else
55
+ fields_array << key << format_value_for_create_or_update(value)
56
+ end
57
+ end
58
+
59
+ fields_array
60
+ end
61
+
62
+
63
+ # Format a single value to be send in a create or update query
64
+ def format_value_for_create_or_update(value)
65
+ # Boolean have to be casted to strings
66
+ if !!value == value
67
+ value.to_s
68
+ else
69
+ value
70
+ end
71
+ end
72
+
73
+
74
+ # Format a hash for its display in logs / error messages
75
+ # Here for ruby 1.8's OrderedHash, to remove when migrated to ruby 2.0 +
76
+ def hash_to_s(h)
77
+ if h.class == Hash
78
+ h.inspect
79
+ elsif h.class < Hash
80
+ Hash[h.to_a].inspect
81
+ else
82
+ raise ArgumentError.new('h is not a Hash or a subclass of Hash')
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,80 @@
1
+ # Base class for performing rforce operations
2
+ # Template for formatting input, performing operation and validating results
3
+ # Implementation details defined in subclasses
4
+
5
+ # Responsible for validating, running and formatting the results of a query
6
+
7
+ module SalesforceAdapter
8
+
9
+ # Exception : Fault from the API, no results returned. The error code has its accessor : :code
10
+ class SalesforceApiFault < StandardError
11
+ attr_reader :code
12
+ def initialize(code, message = nil)
13
+ @code = code
14
+ super(message)
15
+ end
16
+ end
17
+
18
+
19
+ module Operations
20
+
21
+ class Base
22
+
23
+ attr_reader :rforce_binding
24
+
25
+
26
+ def initialize(rforce_binding)
27
+ @rforce_binding = rforce_binding
28
+ end
29
+
30
+
31
+ def run
32
+ validate_request!
33
+
34
+ Helpers.handle_timeout(:max_tries => 4, :sleep_between_tries => 5) do
35
+ @response = perform
36
+ end
37
+
38
+ validate_response!
39
+
40
+ format_response
41
+ end
42
+
43
+
44
+ private
45
+
46
+ def context
47
+ "No context given"
48
+ end
49
+
50
+ def validate_request!
51
+ # nothing by default...
52
+ end
53
+
54
+ def perform!
55
+ raise NotImplementedError.new("Must be defined in subclass")
56
+ end
57
+
58
+ def validate_response!
59
+ # Check for API errors and raise them
60
+ if @response[:Fault]
61
+
62
+ fault_code = @response[:Fault][:faultcode] # "sf:MALFORMED_QUERY"
63
+ fault_details = @response[:Fault][:faultstring]
64
+
65
+ message = "Salesforce API Fault : #{fault_details}.\nContext : #{context}"
66
+
67
+ raise SalesforceApiFault.new(fault_code), message
68
+ end
69
+
70
+ end
71
+
72
+ def format_response
73
+ # Nothing by default
74
+ @response
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,59 @@
1
+ # Responsible for validating, running and formatting the results of a salesforce creation
2
+
3
+ module SalesforceAdapter
4
+
5
+ # Exception : Creation failure, (still a response but no record created done)
6
+ class SalesforceFailedCreate < StandardError
7
+ attr_reader :code
8
+ def initialize(code, message = nil)
9
+ @code = code
10
+ super(message)
11
+ end
12
+ end
13
+
14
+
15
+ module Operations
16
+
17
+ class Create < Base
18
+
19
+ def initialize(rforce_binding, table_name, attributes)
20
+ @table_name = table_name
21
+ @attributes = attributes
22
+
23
+ super(rforce_binding)
24
+ end
25
+
26
+
27
+ private
28
+
29
+ def context
30
+ "creating salesforce #{@table_name} with attributes #{Helpers.hash_to_s(@attributes)}"
31
+ end
32
+
33
+ def validate_request!
34
+ raise ArgumentError.new("type must be specified for a Salesforce Create") unless @attributes[:type]
35
+ end
36
+
37
+ def perform
38
+ rforce_binding.create( @table_name => Helpers.format_fields_for_create_or_update(@attributes) )
39
+ end
40
+
41
+ def validate_response!
42
+ super
43
+
44
+ # Raise exception on update failure
45
+ if !@response[:createResponse][:result][:success]
46
+ sf_error_code = @response[:createResponse][:result][:errors][:statusCode]
47
+ sf_error_message = @response[:createResponse][:result][:errors][:message]
48
+ raise SalesforceFailedCreate.new(sf_error_code), "#{sf_error_message}\nContext : #{context}"
49
+ end
50
+ end
51
+
52
+ def format_response
53
+ # Return the id
54
+ @response[:createResponse][:result][:id]
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # Responsible for validating, running and formatting the results of a query
2
+
3
+ module SalesforceAdapter
4
+ module Operations
5
+
6
+ class Query < Base
7
+
8
+ def initialize(rforce_binding, query_string)
9
+ @query_string = query_string
10
+
11
+ super(rforce_binding)
12
+ end
13
+
14
+
15
+ private
16
+
17
+ def context
18
+ "querying salesforce with : #{@query_string}"
19
+ end
20
+
21
+ def perform
22
+ rforce_binding.query(:queryString => @query_string)
23
+ end
24
+
25
+ def format_response
26
+ # If no results, return an empty array
27
+ return [] if @response[:queryResponse][:result][:size] == "0"
28
+
29
+ # Otherwise return an array of the results (can be empty, or contain only one result)
30
+ records = @response[:queryResponse][:result][:records]
31
+
32
+ if records.is_a?(Array)
33
+ return records
34
+ else
35
+ return [records].compact # if nil => returning an empty array
36
+ end
37
+ end
38
+
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ # Responsible for validating, running and formatting the results of a salesforce update
2
+
3
+ module SalesforceAdapter
4
+
5
+ # Exception : Update failure, (still a response but no update done)
6
+ class SalesforceFailedUpdate < StandardError
7
+ attr_reader :code
8
+ def initialize(code, message = nil)
9
+ @code = code
10
+ super(message)
11
+ end
12
+ end
13
+
14
+
15
+ module Operations
16
+
17
+ class Update < Base
18
+
19
+ def initialize(rforce_binding, table_name, attributes)
20
+ @table_name = table_name
21
+ @attributes = attributes
22
+
23
+ super(rforce_binding)
24
+ end
25
+
26
+
27
+ private
28
+
29
+ def context
30
+ "updating salesforce #{@table_name} with attributes #{Helpers.hash_to_s(@attributes)}"
31
+ end
32
+
33
+ def validate_request!
34
+ raise ArgumentError.new("Id must be specified for a Salesforce Update") unless @attributes[:Id]
35
+ raise ArgumentError.new("type must be specified for a Salesforce Update") unless @attributes[:type]
36
+ end
37
+
38
+ def perform
39
+ rforce_binding.update( @table_name => Helpers.format_fields_for_create_or_update(@attributes) )
40
+ end
41
+
42
+ def validate_response!
43
+ super
44
+
45
+ # Raise exception on update failure
46
+ if !@response[:updateResponse][:result][:success]
47
+ sf_error_code = @response[:updateResponse][:result][:errors][:statusCode]
48
+ sf_error_message = @response[:updateResponse][:result][:errors][:message]
49
+ raise SalesforceFailedUpdate.new(sf_error_code), "#{sf_error_message}\nContext : #{context}"
50
+ end
51
+ end
52
+
53
+ def format_response
54
+ # Return the id
55
+ @response[:updateResponse][:result][:id]
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,69 @@
1
+ # Responsible for validating, running and formatting the results of a webservice operation
2
+
3
+ module SalesforceAdapter
4
+ module Operations
5
+
6
+ class WebserviceCall < Base
7
+
8
+ def initialize(rforce_binding, method_name, arguments, schema_url, service_path)
9
+ @method_name = method_name.to_s
10
+ @arguments = arguments || {}
11
+ @schema_url = schema_url
12
+ @service_path = service_path
13
+
14
+ super(rforce_binding)
15
+ end
16
+
17
+
18
+ private
19
+
20
+ def context
21
+ "calling webservice method #{@method_name} with arguments #{@arguments.inspect}.\n Schema url: #{@schema_url}, service path: #{@service_path}"
22
+ end
23
+
24
+ def perform
25
+ rforce_binding.call_remote(@method_name, formatted_arguments){ [@schema_url, @service_path] }
26
+ end
27
+
28
+ def format_response
29
+ @response[:"#{@method_name}Response"][:result]
30
+ end
31
+
32
+ # Rescue missing methods by inspecting the content of the response
33
+ def validate_response!
34
+ begin
35
+ super
36
+ rescue SalesforceApiFault => e
37
+
38
+ # Change the exception to a NoMethodError if undefined on salesforce
39
+ if e.message =~ /No operation available for request/
40
+ raise NoMethodError.new(e.message)
41
+ else
42
+ raise e
43
+ end
44
+ end
45
+ end
46
+
47
+
48
+ # Formatted arguments for WS call : need to be like [:key1, value1, :key2, value2]
49
+ def formatted_arguments
50
+ [].tap do |formatted_arguments|
51
+ @arguments.each do |name, value|
52
+ formatted_arguments << name << cast(value)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Convert a value before sending it to salesforce
58
+ def cast(value)
59
+ if [TrueClass, FalseClass].include? value.class
60
+ value
61
+ else
62
+ value.to_s
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,55 @@
1
+ # Wrapper around the rforce binding, responsible to guarantee it is logged in
2
+
3
+ module SalesforceAdapter
4
+
5
+ class RforceBinding
6
+
7
+ def initialize(url, login, password)
8
+ @url = url
9
+ @login = login
10
+ @password = password
11
+
12
+ @rforce = RForce::Binding.new(@url)
13
+ end
14
+
15
+
16
+ # Attempts to login to salesforce
17
+ def login
18
+ Helpers.handle_timeout(:max_tries => 4, :sleep_between_tries => 2) do
19
+ @rforce.login( @login , @password )
20
+ end
21
+ end
22
+
23
+
24
+ # Is it currently logged in?
25
+ def logged_in?
26
+ # An unlogged rforce binding has a session_id defined as nil
27
+ # Does not handle session expiration however
28
+ !!@rforce.instance_variable_get(:@session_id)
29
+ end
30
+
31
+
32
+ # Delegate all these methods to the rforce binding
33
+ [:query, :create, :update, :call_remote].each do |method_name|
34
+
35
+ define_method method_name do |*args, &block|
36
+
37
+ begin
38
+
39
+ # Guarantee the binding is logged in
40
+ login unless logged_in?
41
+
42
+ @rforce.send method_name, *args, &block
43
+
44
+ # Handle timeouts from salesforce
45
+ rescue Errno::ECONNRESET => e
46
+ raise SalesforceAdapter::SalesforceTimeout.new(e.message)
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceAdapter
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,20 @@
1
+ # Proxy for a salesforce webservice.
2
+ # Uses method_missing behind the scenes to forward methods to salesforce calls
3
+
4
+ module SalesforceAdapter
5
+
6
+ class WebserviceProxy
7
+
8
+ def initialize(adapter, webservice_class)
9
+ @adapter = adapter
10
+ @schema_url = "http://soap.sforce.com/schemas/class/#{webservice_class}"
11
+ @service_path = "/services/Soap/class/#{webservice_class}"
12
+ end
13
+
14
+ # Forward calls to salesforce
15
+ def method_missing(method_name, args = {})
16
+ @adapter.call_webservice(method_name, args, @schema_url, @service_path)
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ require 'salesforce_adapter/version'
2
+
3
+ require 'salesforce_adapter/helpers'
4
+ require 'salesforce_adapter/rforce_binding'
5
+ require 'salesforce_adapter/webservice_proxy'
6
+
7
+ require 'salesforce_adapter/operations/base'
8
+ require 'salesforce_adapter/operations/query'
9
+ require 'salesforce_adapter/operations/create'
10
+ require 'salesforce_adapter/operations/update'
11
+ require 'salesforce_adapter/operations/webservice_call'
12
+
13
+
14
+ # Wrapper around a rforce binding
15
+ # Forwards the 'query' method, handling the logging and error raising
16
+
17
+ require 'rforce'
18
+
19
+
20
+ module SalesforceAdapter
21
+
22
+ class Base
23
+
24
+ attr_reader :rforce_binding
25
+ attr_reader :webservice
26
+
27
+
28
+ # Initialize with the credentials
29
+ def initialize(config)
30
+ @rforce_binding = RforceBinding.new(config[:url], config[:login], config[:password])
31
+ end
32
+
33
+ def webservice(webservice_class)
34
+ WebserviceProxy.new(self, webservice_class)
35
+ end
36
+
37
+ # Runs a query on salesforce and returns the result
38
+ def query( query_string )
39
+
40
+ # Perform the query
41
+ Operations::Query.new(@rforce_binding, query_string).run()
42
+
43
+ end
44
+
45
+
46
+ # Updates a salesforce record
47
+ def update(table_name, attributes)
48
+
49
+ # Perform the update and returns the id
50
+ Operations::Update.new(@rforce_binding, table_name, attributes).run()
51
+
52
+ end
53
+
54
+
55
+ # Creates a salesforce record
56
+ def create(table_name, attributes)
57
+
58
+ # Perform the creation and returns the id
59
+ Operations::Create.new(@rforce_binding, table_name, attributes).run()
60
+
61
+ end
62
+
63
+
64
+
65
+ # Queries a salesforce webservice
66
+ def call_webservice(method_name, arguments, schema_url, service_path)
67
+
68
+ # Perform the call to the webservice and returns the result
69
+ Operations::WebserviceCall.new(@rforce_binding, method_name, arguments, schema_url, service_path).run()
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ClicRDV
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rforce
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - david.ruyer@clicrdv.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/salesforce_adapter.rb
64
+ - lib/salesforce_adapter/helpers.rb
65
+ - lib/salesforce_adapter/operations/base.rb
66
+ - lib/salesforce_adapter/operations/create.rb
67
+ - lib/salesforce_adapter/operations/query.rb
68
+ - lib/salesforce_adapter/operations/update.rb
69
+ - lib/salesforce_adapter/operations/webservice_call.rb
70
+ - lib/salesforce_adapter/rforce_binding.rb
71
+ - lib/salesforce_adapter/version.rb
72
+ - lib/salesforce_adapter/webservice_proxy.rb
73
+ homepage: ''
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.2.2
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Lightweight client for the salesforce API
97
+ test_files: []
98
+ has_rdoc: