dm-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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Yehuda Katz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,121 @@
1
+ dm-salesforce-adapter
2
+ =====================
3
+
4
+ This gem provides a Salesforce Adapter under DataMapper 1.0.x. A version of
5
+ this adapter supporting DM 0.10.x exists under the name "dm-salesforce". Older
6
+ versions of DM (0.9.x) are no longer supported.
7
+
8
+ What it looks like
9
+ ==================
10
+
11
+ class Account
12
+ include DataMapper::Salesforce::Resource
13
+
14
+ def self.default_repository_name
15
+ :salesforce
16
+ end
17
+
18
+ property :id, Serial
19
+ property :name, String
20
+ property :description, String
21
+ property :fax, String
22
+ property :phone, String
23
+ property :type, String
24
+ property :website, String
25
+ property :is_awesome, Boolean
26
+
27
+ has 0..n, :contacts
28
+ end
29
+
30
+ class Contact
31
+ include DataMapper::Salesforce::Resource
32
+
33
+ def self.default_repository_name
34
+ :salesforce
35
+ end
36
+
37
+ property :id, Serial
38
+ property :first_name, String
39
+ property :last_name, String
40
+ property :email, String
41
+
42
+ belongs_to :account
43
+ end
44
+
45
+ DataMapper.setup(:salesforce, {:adapter => 'salesforce',
46
+ :username => 'salesforceuser@mydomain.com',
47
+ :password => 'skateboardsf938915c9cdc36ff5498881b',
48
+ :path => '/path/to/wsdl.xml',
49
+ :host => ''})
50
+
51
+ account = Account.first
52
+ account.is_awesome = true
53
+ account.save
54
+
55
+ See [the fixtures](http://github.com/cloudcrowd/dm-salesforce-adapter/tree/master/spec/fixtures) for more examples.
56
+
57
+ How it works
58
+ ============
59
+
60
+ Salesforce provides an XML-based WSDL definition of an existing schema/object
61
+ model for download. dm-salesforce-adapter uses this WSDL to auto-generate a
62
+ SOAP-based Ruby driver and classes, which is then used to implement a basic,
63
+ low-level DataMapper Adapter.
64
+
65
+ Upon first access, the driver and classes are cached locally on disk in one of
66
+ the following locations (in order of precedence):
67
+
68
+ * In `apidir`, defined in `database.yml` (see included database.yml-example)
69
+ * In `ENV['SALESFORCE_DIR']`
70
+ * In `ENV['HOME']/.salesforce/`
71
+
72
+ Getting set up
73
+ ==============
74
+
75
+ 1. Obtain a working salesforce.com account
76
+
77
+ 2. Get a valid security token (if you don't already have one)
78
+ * Login to `https://login.salesforce.com`
79
+ * [Click "Setup"][setup]
80
+ * [Click "Personal Setup" / "My Personal Information" / "Reset My Security Token"][gettoken]
81
+ * This will send a message to your account's email address with an "API key"
82
+ (looks like a 24 character token)
83
+
84
+ 3. Get the Enterprise WSDL for your object model
85
+ * Login to `https://login.salesforce.com`
86
+ * [Click "Setup"][setup]
87
+ * [Click "App Setup" / "Develop" / "API"][getwsdl]
88
+ * Click "Generate Enterprise WSDL", then click the "Generate" button
89
+ * Save that to an .xml file somewhere (path/extension doesn't matter - you specify it
90
+ in database.yml / DataMapper.setup)
91
+
92
+ 4. Copy and modify config/example.rb to use your info
93
+ * The :password field is the concatenation of your login password and the API key
94
+ * If your password is 'skateboards' and API key is 'f938915c9cdc36ff5498881b', then
95
+ the :password field you specify to DataMapper.setup should be
96
+ 'skateboardsf938915c9cdc36ff5498881b'
97
+
98
+ Run 'ruby example.rb' and you should have access to the Account and Contact models (schema
99
+ differences withstanding).
100
+
101
+ **Don't forget to:**
102
+
103
+ * Retrieve a new copy of your WSDL anytime you make changes to your Salesforce schema
104
+ * Wipe the auto-generated SOAP classes anytime you update your WSDL
105
+
106
+
107
+ Special Thanks to those who helped
108
+ ==================================================
109
+ * Yehuda Katz
110
+ * Corey Donohoe
111
+ * Tim Carey-Smith
112
+ * Andy Delcambre
113
+ * Ben Burkert
114
+ * Larry Diehl
115
+ * Jordan Ritter
116
+ * Martin Emde
117
+ * Jason Snell
118
+
119
+ [setup]: http://img.skitch.com/20090204-gaxdfxbi1emfita5dax48ids4m.jpg "Click on Setup"
120
+ [getwsdl]: http://img.skitch.com/20090204-nhurnuxwf5g3ufnjk2xkfjc5n4.jpg "Expand and Save"
121
+ [gettoken]: http://img.skitch.com/20090204-mnt182ce7bc4seecqbrjjxjbef.jpg "You can reset your token here"
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rubygems/specification'
3
+ require 'date'
4
+ require 'pp'
5
+ require 'tmpdir'
6
+
7
+ require 'bundler/setup'
8
+
9
+ Bundler.require
10
+
11
+ task :default => 'spec'
12
+ require 'spec'
13
+ require 'spec/rake/spectask'
14
+ desc "Run specs"
15
+ Spec::Rake::SpecTask.new(:spec) do |t|
16
+ t.spec_opts << %w(-fs --color) << %w(-O spec/spec.opts)
17
+ t.spec_opts << '--loadby' << 'random'
18
+ t.spec_files = Dir["spec/**/*_spec.rb"]
19
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
20
+ t.rcov_opts << '--exclude' << "~/.salesforce,gems,vendor,/var/folders,spec,config,tmp"
21
+ t.rcov_opts << '--text-summary'
22
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
23
+ end
24
+
25
+ desc "Release the version"
26
+ task :release => :repackage do
27
+ version = SalesforceAdapter::VERSION
28
+ puts "Releasing #{version}"
29
+
30
+ `git show-ref tags/v#{version}`
31
+ unless $?.success?
32
+ abort "There is no tag for v#{version}"
33
+ end
34
+
35
+ `git show-ref heads/releasing`
36
+ if $?.success?
37
+ abort "Remove the releasing branch, we need it!"
38
+ end
39
+
40
+ puts "Checking out to the releasing branch as the tag"
41
+ system("git", "checkout", "-b", "releasing", "tags/v#{version}")
42
+
43
+ puts "Reseting back to master"
44
+ system("git", "checkout", "master")
45
+ system("git", "branch", "-d", "releasing")
46
+
47
+ current = @spec.version.to_s + ".0"
48
+ next_version = Gem::Version.new(current).bump
49
+
50
+ puts "Changing the version to #{next_version}."
51
+
52
+ version_file = File.dirname(__FILE__)+"/lib/#{GEM}/version.rb"
53
+ File.open(version_file, "w") do |f|
54
+ f.puts <<-EOT
55
+ module SalesforceAdapter
56
+ VERSION = "#{next_version}"
57
+ end
58
+ EOT
59
+ end
60
+
61
+ puts "Committing the version change"
62
+ system("git", "commit", version_file, "-m", "Next version: #{next_version}")
63
+
64
+ puts "Push the commit up! if you don't, you'll be hunted down"
65
+ end
@@ -0,0 +1,29 @@
1
+ require 'data_objects'
2
+ require 'dm-core'
3
+ require 'dm-types'
4
+ require 'dm-validations'
5
+
6
+ require 'soap/wsdlDriver'
7
+ require 'soap/header/simplehandler'
8
+ require 'rexml/element'
9
+
10
+ class SalesforceAdapter < ::DataMapper::Adapters::AbstractAdapter
11
+ Inflector = ::DataMapper.const_defined?(:Inflector) ? ::DataMapper::Inflector : ::Extlib::Inflection
12
+ end
13
+
14
+ require 'dm-salesforce-adapter/resource'
15
+ require 'dm-salesforce-adapter/connection'
16
+ require 'dm-salesforce-adapter/connection/errors'
17
+ require 'dm-salesforce-adapter/soap_wrapper'
18
+ require 'dm-salesforce-adapter/sql'
19
+ require 'dm-salesforce-adapter/version'
20
+ require 'dm-salesforce-adapter/adapter'
21
+ require 'dm-salesforce-adapter/property'
22
+
23
+ # For convenience (WRT the examples)
24
+ module DataMapper::Salesforce
25
+ Resource = SalesforceAdapter::Resource
26
+ end
27
+
28
+ ::DataMapper::Adapters::SalesforceAdapter = SalesforceAdapter
29
+ ::DataMapper::Adapters.const_added(:SalesforceAdapter)
@@ -0,0 +1,172 @@
1
+
2
+ class SalesforceAdapter
3
+ include SQL
4
+
5
+ def initialize(name, uri_or_options)
6
+ super
7
+ @resource_naming_convention = proc do |value|
8
+ klass = Inflector.constantize(value)
9
+ if klass.respond_to?(:salesforce_class)
10
+ klass.salesforce_class
11
+ else
12
+ value.split("::").last
13
+ end
14
+ end
15
+ @field_naming_convention = proc do |property|
16
+ connection.field_name_for(property.model.storage_name(name), property.name.to_s)
17
+ end
18
+ end
19
+
20
+ def connection
21
+ @connection ||= Connection.new(options["username"], options["password"], options["path"], options["apidir"])
22
+ end
23
+
24
+ # FIXME: DM Adapters customarily throw exceptions when they experience errors,
25
+ # otherwise failed operations (e.g. Resource#save) still return true and thus
26
+ # confuse the caller.
27
+ #
28
+ # Someone needs to make a decision about legacy support and the consequences
29
+ # of changing the behaviour from broken-but-typical to
30
+ # correct-but-maybe-unexpected. Maybe a config file option about whether to
31
+ # raise exceptions or for the user to always check Model#valid? +
32
+ # Model#salesforce_errors?
33
+ #
34
+ # Needs to be applied to all CRUD operations.
35
+ def create(resources)
36
+ arr = resources.map do |resource|
37
+ make_salesforce_obj(resource, resource.dirty_attributes)
38
+ end
39
+
40
+ result = connection.create(arr)
41
+ result.each_with_index do |record, i|
42
+ resource = resources[i]
43
+ if id_field = resource.class.key.find {|p| p.serial?}
44
+ normalized_value = normalize_id_value(resource.class, id_field, record.id)
45
+ id_field.set!(resource, normalized_value)
46
+ end
47
+ end
48
+
49
+ result.size
50
+
51
+ rescue Connection::SOAPError => e
52
+ handle_server_outage(e)
53
+ end
54
+
55
+ def update(attributes, collection)
56
+ query = collection.query
57
+ arr = collection.map { |obj| make_salesforce_obj(query, attributes) }
58
+
59
+ connection.update(arr).size
60
+
61
+ rescue Connection::SOAPError => e
62
+ handle_server_outage(e)
63
+ end
64
+
65
+ def delete(collection)
66
+ query = collection.query
67
+ keys = collection.map { |r| r.key }.flatten.uniq
68
+
69
+ connection.delete(keys).size
70
+
71
+ rescue Connection::SOAPError => e
72
+ handle_server_outage(e)
73
+ end
74
+
75
+ def handle_server_outage(error)
76
+ if error.server_unavailable?
77
+ raise Connection::ServerUnavailable, "The salesforce server is currently unavailable"
78
+ else
79
+ raise error
80
+ end
81
+ end
82
+
83
+ # Reading responses back from SELECTS:
84
+ # In the typical case, response.size reflects the # of records returned.
85
+ # In the aggregation case, response.size reflects the count.
86
+ #
87
+ # Interpretation of this field requires knowledge of whether we are expecting
88
+ # an aggregate result, thus the response from execute_select() is processed
89
+ # differently depending on invocation (read vs. aggregate).
90
+ def read(query)
91
+ properties = query.fields
92
+ repository = query.repository
93
+
94
+ response = execute_select(query)
95
+ return [] unless response.records
96
+
97
+ rows = response.records.inject([]) do |results, record|
98
+ results << properties.inject({}) do |result, property|
99
+ meth = connection.field_name_for(property.model.storage_name(repository.name), property.field)
100
+ result[property] = normalize_id_value(query.model, property, record.send(meth))
101
+ result
102
+ end
103
+ end
104
+
105
+ query.model.load(rows, query)
106
+ end
107
+
108
+ # http://www.salesforce.com/us/developer/docs/api90/Content/sforce_api_calls_soql.htm
109
+ # SOQL doesn't support anything but count(), so we catch it here and interpret
110
+ # the result. Requires 'dm-aggregates' to be loaded.
111
+ def aggregate(query)
112
+ query.fields.each do |f|
113
+ unless f.target == :all && f.operator == :count
114
+ raise ArgumentError, %{Aggregate function #{f.operator} not supported in SOQL}
115
+ end
116
+ end
117
+
118
+ [ execute_select(query).size ]
119
+ end
120
+
121
+ private
122
+ def execute_select(query)
123
+ repository = query.repository
124
+ conditions = query.conditions.map {|c| conditions_statement(c, repository)}.compact.join(") AND (")
125
+
126
+ fields = query.fields.map do |f|
127
+ case f
128
+ when DataMapper::Property
129
+ f.field
130
+ when DataMapper::Query::Operator
131
+ %{#{f.operator}()}
132
+ else
133
+ raise ArgumentError, "Unknown query field #{f.class}: #{f.inspect}"
134
+ end
135
+ end.join(", ")
136
+
137
+ sql = "SELECT #{fields} from #{query.model.storage_name(repository.name)}"
138
+ sql << " WHERE (#{conditions})" unless conditions.empty?
139
+ sql << " ORDER BY #{order(query.order[0])}" unless query.order.empty?
140
+ sql << " LIMIT #{query.limit}" if query.limit
141
+
142
+ DataMapper.logger.debug sql if DataMapper.logger
143
+
144
+ connection.query(sql)
145
+ end
146
+
147
+ def make_salesforce_obj(from, with_attrs)
148
+ klass_name = from.model.storage_name(from.repository.name)
149
+ values = {}
150
+
151
+ # FIXME: query.conditions is potentially a tree now
152
+ if from.is_a?(::DataMapper::Query)
153
+ key_value = from.conditions.find { |c| c.subject.key? }.value
154
+ values["id"] = normalize_id_value(from.model, from.model.properties[:id], key_value)
155
+ end
156
+
157
+ with_attrs.each do |property, value|
158
+ next if property.serial? || property.key? and value.nil?
159
+ values[property.field] = normalize_id_value(from.model, property, value)
160
+ end
161
+
162
+ connection.make_object(klass_name, values)
163
+ end
164
+
165
+ def normalize_id_value(klass, property, value)
166
+ return nil unless value
167
+ properties = Array(klass.send(:salesforce_id_properties)).map { |p| p.to_sym } rescue []
168
+ return properties.include?(property.name) ? value[0..14] : value
169
+ end
170
+ end
171
+
172
+
@@ -0,0 +1,144 @@
1
+ require 'dm-salesforce-adapter/connection/errors'
2
+
3
+ class SalesforceAdapter
4
+ class Connection
5
+ include Errors
6
+
7
+ class HeaderHandler < SOAP::Header::SimpleHandler
8
+ def initialize(tag, value)
9
+ super(XSD::QName.new('urn:enterprise.soap.sforce.com', tag))
10
+ @tag = tag
11
+ @value = value
12
+ end
13
+ def on_simple_outbound
14
+ @value
15
+ end
16
+ end
17
+
18
+ def initialize(username, password, wsdl_path, api_dir, organization_id = nil)
19
+ @wrapper = SoapWrapper.new("SalesforceAPI", "Soap", wsdl_path, api_dir)
20
+ @username, @password, @organization_id = URI.unescape(username), password, organization_id
21
+ login
22
+ end
23
+ attr_reader :user_id, :user_details
24
+
25
+ def wsdl_path
26
+ @wrapper.wsdl_path
27
+ end
28
+
29
+ def api_dir
30
+ @wrapper.api_dir
31
+ end
32
+
33
+ def organization_id
34
+ @user_details && @user_details.organizationId
35
+ end
36
+
37
+ def make_object(klass_name, values)
38
+ obj = SalesforceAPI.const_get(klass_name).new
39
+ values.each do |property, value|
40
+ field = field_name_for(klass_name, property)
41
+ if value.nil? or value == ""
42
+ obj.fieldsToNull.push(field)
43
+ else
44
+ obj.send("#{field}=", value)
45
+ end
46
+ end
47
+ obj
48
+ end
49
+
50
+ def field_name_for(klass_name, column)
51
+ klass = SalesforceAPI.const_get(klass_name)
52
+ # extlib vs. activesupport
53
+ fields = [column, (column.camel_case rescue column.camelcase), "#{column}__c".downcase]
54
+ options = /^(#{fields.join("|")})$/i
55
+ matches = klass.instance_methods(false).grep(options)
56
+ if matches.any?
57
+ matches.first
58
+ else
59
+ raise FieldNotFound,
60
+ "You specified #{column} as a field, but neither #{fields.join(" or ")} exist. " \
61
+ "Either manually specify the field name with :field, or check to make sure you have " \
62
+ "provided a correct field name."
63
+ end
64
+ end
65
+
66
+ def query(string)
67
+ with_reconnection do
68
+ driver.query(:queryString => string).result
69
+ end
70
+ rescue SOAP::FaultError => e
71
+ raise QueryError.new(e.message, [])
72
+ end
73
+
74
+ def create(objects)
75
+ call_api(:create, CreateError, "creating", objects)
76
+ end
77
+
78
+ def update(objects)
79
+ call_api(:update, UpdateError, "updating", objects)
80
+ end
81
+
82
+ def delete(keys)
83
+ call_api(:delete, DeleteError, "deleting", keys)
84
+ end
85
+
86
+ private
87
+
88
+ def driver
89
+ @wrapper.driver
90
+ end
91
+
92
+ def login
93
+ driver
94
+ if @organization_id
95
+ driver.headerhandler << HeaderHandler.new("LoginScopeHeader", :organizationId => @organization_id)
96
+ end
97
+
98
+ begin
99
+ result = driver.login(:username => @username, :password => @password).result
100
+ rescue SOAP::FaultError => error
101
+ if error.faultcode.to_s =~ /INVALID_LOGIN/
102
+ raise LoginFailed, error.faultstring.to_s
103
+ else
104
+ raise error
105
+ end
106
+ end
107
+ driver.endpoint_url = result.serverUrl
108
+ driver.headerhandler << HeaderHandler.new("SessionHeader", "sessionId" => result.sessionId)
109
+ driver.headerhandler << HeaderHandler.new("CallOptions", "client" => "client")
110
+ @user_id = result.userId
111
+ @user_details = result.userInfo
112
+ driver
113
+ end
114
+
115
+ def call_api(method, exception_class, message, args)
116
+ with_reconnection do
117
+ result = driver.send(method, args)
118
+ if result.all? {|r| r.success}
119
+ result
120
+ else
121
+ # TODO: be smarter about exceptions here
122
+ raise exception_class.new("Got some errors while #{message} Salesforce objects", result)
123
+ end
124
+ end
125
+ end
126
+
127
+ def with_reconnection(&block)
128
+ yield
129
+ rescue SOAP::FaultError => error
130
+ retry_count ||= 0
131
+ if error.faultcode.to_s =~ "INVALID_SESSION_ID"
132
+ DataMapper.logger.debug "Got a invalid session id; reconnecting" if DataMapper.logger
133
+ @driver = nil
134
+ login
135
+ retry_count += 1
136
+ retry unless retry_count > 5
137
+ else
138
+ raise error
139
+ end
140
+
141
+ raise SessionTimeout, "The Salesforce session could not be established"
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,50 @@
1
+ class SalesforceAdapter::Connection
2
+ module Errors
3
+ class Error < StandardError; end
4
+ class FieldNotFound < Error; end
5
+ class LoginFailed < Error; end
6
+ class SessionTimeout < Error; end
7
+ class UnknownStatusCode < Error; end
8
+ class ServerUnavailable < Error; end
9
+
10
+
11
+ class SOAPError < Error
12
+ def initialize(message, result)
13
+ @result = result
14
+ super("#{message}: #{result_message}")
15
+ end
16
+
17
+ def records
18
+ @result.to_a
19
+ end
20
+
21
+ def failed_records
22
+ @result.reject {|r| r.success}
23
+ end
24
+
25
+ def successful_records
26
+ @result.select {|r| r.success}
27
+ end
28
+
29
+ def result_message
30
+ failed_records.map do |r|
31
+ message_for_record(r)
32
+ end.join("; ")
33
+ end
34
+
35
+ def message_for_record(record)
36
+ record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
37
+ end
38
+
39
+ def server_unavailable?
40
+ failed_records.any? do |record|
41
+ record.errors.any? {|e| e.statusCode == "SERVER_UNAVAILABLE"}
42
+ end
43
+ end
44
+ end
45
+ class CreateError < SOAPError; end
46
+ class QueryError < SOAPError; end
47
+ class DeleteError < SOAPError; end
48
+ class UpdateError < SOAPError; end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ class SalesforceAdapter
2
+ module Property
3
+ end
4
+ end
5
+
6
+ require 'dm-salesforce-adapter/property/serial'
7
+ require 'dm-salesforce-adapter/property/boolean'
@@ -0,0 +1,17 @@
1
+ module SalesforceAdapter::Property
2
+ class Boolean < ::DataMapper::Property::Integer
3
+ FALSE = 0
4
+ TRUE = 1
5
+
6
+ def self.dump(value, property)
7
+ case value
8
+ when nil, false then FALSE
9
+ else TRUE
10
+ end
11
+ end
12
+
13
+ def self.load(value, property)
14
+ [true, 1, '1', 'true', 'TRUE', TRUE].include?(value)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module SalesforceAdapter::Property
2
+ class Serial < ::DataMapper::Property::String
3
+ accept_options :serial
4
+ serial true
5
+
6
+ length 15
7
+
8
+ def self.dump(value, property)
9
+ value[0..14] unless value.blank?
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ class SalesforceAdapter
2
+ module Resource
3
+ def self.included(model)
4
+ model.send :include, DataMapper::Resource
5
+ model.send :include, SalesforceAdapter::Property
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,82 @@
1
+ class SalesforceAdapter
2
+ class SoapWrapper
3
+ class ClassesFailedToGenerate < StandardError; end
4
+
5
+ attr_reader :module_name, :driver_name, :wsdl_path, :api_dir
6
+
7
+ def initialize(module_name, driver_name, wsdl_path, api_dir)
8
+ @module_name, @driver_name, @wsdl_path, @api_dir = module_name, driver_name, File.expand_path(wsdl_path), File.expand_path(api_dir)
9
+ generate_soap_classes
10
+ driver
11
+ end
12
+
13
+ def driver
14
+ @driver ||= Object.const_get(module_name).const_get(driver_name).new
15
+ end
16
+
17
+ =begin
18
+ # Attempt at a run-time equivalent. Works except for the API responses are
19
+ # in SOAP objects, not native ruby objects. Haven't figured that one out..
20
+ def driver
21
+ return @driver if @driver
22
+
23
+ require 'wsdl/soap/wsdl2ruby'
24
+
25
+ factory = SOAP::WSDLDriverFactory.new(wsdl_path)
26
+ class_name_creator = WSDL::SOAP::ClassNameCreator.new
27
+
28
+ eval(WSDL::SOAP::ClassDefCreator.new(factory.wsdl, class_name_creator, @module_name).dump, TOPLEVEL_BINDING)
29
+ eval(WSDL::SOAP::MappingRegistryCreator.new(factory.wsdl, class_name_creator, @module_name).dump, TOPLEVEL_BINDING)
30
+
31
+ @driver ||= factory.create_rpc_driver
32
+ end
33
+ =end
34
+
35
+ def generate_soap_classes
36
+ unless File.file?(wsdl_path)
37
+ raise Errno::ENOENT, "Could not find the WSDL at #{wsdl_path}"
38
+ end
39
+
40
+ FileUtils.mkdir_p(wsdl_api_dir)
41
+
42
+ generate_files unless files_exist?
43
+
44
+ $:.push wsdl_api_dir
45
+ require "#{module_name}Driver"
46
+ $:.delete wsdl_api_dir
47
+ end
48
+
49
+ # Good candidate for shipping out into a Rakefile.
50
+ def generate_files
51
+ require 'wsdl/soap/wsdl2ruby'
52
+
53
+ wsdl2ruby = WSDL::SOAP::WSDL2Ruby.new
54
+ wsdl2ruby.logger = $LOG if $LOG
55
+ wsdl2ruby.location = wsdl_path
56
+ wsdl2ruby.basedir = wsdl_api_dir
57
+
58
+ wsdl2ruby.opt.merge!({
59
+ 'classdef' => module_name,
60
+ 'module_path' => module_name,
61
+ 'mapping_registry' => nil,
62
+ 'driver' => nil,
63
+ 'client_skelton' => nil,
64
+ })
65
+
66
+ wsdl2ruby.run
67
+
68
+ raise ClassesFailedToGenerate unless files_exist?
69
+ end
70
+
71
+ def files_exist?
72
+ ["#{module_name}.rb", "#{module_name}MappingRegistry.rb", "#{module_name}Driver.rb"].all? do |name|
73
+ File.exist?("#{wsdl_api_dir}/#{name}")
74
+ end
75
+ end
76
+
77
+ def wsdl_api_dir
78
+ "#{api_dir}/#{File.basename(wsdl_path)}"
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,99 @@
1
+ class SalesforceAdapter
2
+ module SQL
3
+ def conditions_statement(conditions, repository)
4
+ case conditions
5
+ when DataMapper::Query::Conditions::NotOperation then negate_operation(conditions.operand, repository)
6
+ when DataMapper::Query::Conditions::AbstractOperation then conditions.operands.first # ignores AND/OR grouping for now.
7
+ when DataMapper::Query::Conditions::AbstractComparison then comparison_statement(conditions, repository)
8
+ else raise("Unkown condition type #{conditions.class}: #{conditions.inspect}")
9
+ end
10
+ end
11
+
12
+ def comparison_statement(comparison, repository)
13
+ subject = comparison.subject
14
+ value = comparison.value
15
+
16
+ if comparison.relationship?
17
+ return conditions_statement(comparison.foreign_key_mapping, repository)
18
+ elsif comparison.slug == :in && value.empty?
19
+ return [] # match everything
20
+ end
21
+
22
+ operator = comparison_operator(comparison)
23
+ column_name = property_to_column_name(subject, repository)
24
+
25
+ "#{column_name} #{operator} #{quote_value(value,subject)}"
26
+ end
27
+
28
+ def comparison_operator(comparison)
29
+ subject = comparison.subject
30
+ value = comparison.value
31
+
32
+ case comparison.slug
33
+ when :eql then equality_operator(subject, value)
34
+ when :in then include_operator(subject, value)
35
+ when :not then inequality_operator(subject, value)
36
+ when :regexp then regexp_operator(value)
37
+ when :like then like_operator(value)
38
+ when :gt then '>'
39
+ when :lt then '<'
40
+ when :gte then '>='
41
+ when :lte then '<='
42
+ end
43
+ end
44
+
45
+ def negate_operation(operand, repository)
46
+ statement = conditions_statement(operand, repository)
47
+ statement = "NOT(#{statement})" unless statement.nil?
48
+ statement
49
+ end
50
+
51
+ def property_to_column_name(prop, repository)
52
+ case prop
53
+ when DataMapper::Property
54
+ prop.field
55
+ when DataMapper::Query::Path
56
+ rels = prop.relationships
57
+ names = rels.map {|r| storage_name(r, repository) }.join(".")
58
+ "#{names}.#{prop.field}"
59
+ end
60
+ end
61
+
62
+ def storage_name(rel, repository)
63
+ rel.parent_model.storage_name(repository.name)
64
+ end
65
+
66
+ def order(direction)
67
+ "#{direction.target.field} #{direction.operator.to_s.upcase}"
68
+ end
69
+
70
+ def equality_operator(property, operand)
71
+ operand.nil? ? 'IS' : '='
72
+ end
73
+
74
+ def include_operator(property, operand)
75
+ case operand
76
+ when Array then 'IN'
77
+ when Range then 'BETWEEN'
78
+ end
79
+ end
80
+
81
+ def like_operator(operand)
82
+ "LIKE"
83
+ end
84
+
85
+ def quote_value(value, property)
86
+ if property.type == Property::Boolean
87
+ # True on salesforce needs to be TRUE/FALSE for WHERE clauses but not for inserts.
88
+ return value == Property::Boolean::TRUE ? 'TRUE' : 'FALSE'
89
+ end
90
+
91
+ case value
92
+ when Array then "(#{value.map {|v| quote_value(v, property)}.join(", ")})"
93
+ when NilClass then "NULL"
94
+ when String then "'#{value.gsub(/'/, "\\'").gsub(/\\/, %{\\\\})}'"
95
+ else "#{value}"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,3 @@
1
+ class SalesforceAdapter
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-salesforce-adapter
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Yehuda Katz
14
+ - Tim Carey-Smith
15
+ - Andy Delcambre
16
+ - Jordan Ritter
17
+ autorequire:
18
+ bindir: bin
19
+ cert_chain: []
20
+
21
+ date: 2010-12-23 00:00:00 -05:00
22
+ default_executable:
23
+ dependencies:
24
+ - !ruby/object:Gem::Dependency
25
+ name: httpclient
26
+ prerelease: false
27
+ requirement: &id001 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - "="
31
+ - !ruby/object:Gem::Version
32
+ hash: 119
33
+ segments:
34
+ - 2
35
+ - 1
36
+ - 5
37
+ - 2
38
+ version: 2.1.5.2
39
+ type: :runtime
40
+ version_requirements: *id001
41
+ - !ruby/object:Gem::Dependency
42
+ name: extlib
43
+ prerelease: false
44
+ requirement: &id002 !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">"
48
+ - !ruby/object:Gem::Version
49
+ hash: 37
50
+ segments:
51
+ - 0
52
+ - 9
53
+ - 15
54
+ version: 0.9.15
55
+ type: :runtime
56
+ version_requirements: *id002
57
+ - !ruby/object:Gem::Dependency
58
+ name: dm-core
59
+ prerelease: false
60
+ requirement: &id003 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ hash: 19
66
+ segments:
67
+ - 1
68
+ - 0
69
+ - 2
70
+ version: 1.0.2
71
+ type: :runtime
72
+ version_requirements: *id003
73
+ - !ruby/object:Gem::Dependency
74
+ name: dm-validations
75
+ prerelease: false
76
+ requirement: &id004 !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ hash: 19
82
+ segments:
83
+ - 1
84
+ - 0
85
+ - 2
86
+ version: 1.0.2
87
+ type: :runtime
88
+ version_requirements: *id004
89
+ - !ruby/object:Gem::Dependency
90
+ name: dm-types
91
+ prerelease: false
92
+ requirement: &id005 !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ~>
96
+ - !ruby/object:Gem::Version
97
+ hash: 19
98
+ segments:
99
+ - 1
100
+ - 0
101
+ - 2
102
+ version: 1.0.2
103
+ type: :runtime
104
+ version_requirements: *id005
105
+ - !ruby/object:Gem::Dependency
106
+ name: soap4r
107
+ prerelease: false
108
+ requirement: &id006 !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ~>
112
+ - !ruby/object:Gem::Version
113
+ hash: 19
114
+ segments:
115
+ - 1
116
+ - 5
117
+ - 8
118
+ version: 1.5.8
119
+ type: :runtime
120
+ version_requirements: *id006
121
+ description: A DataMapper 1.0x adapter to the Salesforce API
122
+ email: jpr5@cloudcrowd.com
123
+ executables: []
124
+
125
+ extensions: []
126
+
127
+ extra_rdoc_files:
128
+ - README.markdown
129
+ - LICENSE
130
+ files:
131
+ - LICENSE
132
+ - README.markdown
133
+ - Rakefile
134
+ - lib/dm-salesforce-adapter/adapter.rb
135
+ - lib/dm-salesforce-adapter/connection/errors.rb
136
+ - lib/dm-salesforce-adapter/connection.rb
137
+ - lib/dm-salesforce-adapter/property/boolean.rb
138
+ - lib/dm-salesforce-adapter/property/serial.rb
139
+ - lib/dm-salesforce-adapter/property.rb
140
+ - lib/dm-salesforce-adapter/resource.rb
141
+ - lib/dm-salesforce-adapter/soap_wrapper.rb
142
+ - lib/dm-salesforce-adapter/sql.rb
143
+ - lib/dm-salesforce-adapter/version.rb
144
+ - lib/dm-salesforce-adapter.rb
145
+ has_rdoc: true
146
+ homepage: http://github.com/cloudcrowd/dm-salesforce-adapter
147
+ licenses: []
148
+
149
+ post_install_message:
150
+ rdoc_options: []
151
+
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ hash: 3
160
+ segments:
161
+ - 0
162
+ version: "0"
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ hash: 3
169
+ segments:
170
+ - 0
171
+ version: "0"
172
+ requirements: []
173
+
174
+ rubyforge_project:
175
+ rubygems_version: 1.3.7
176
+ signing_key:
177
+ specification_version: 3
178
+ summary: A DataMapper 1.0x adapter to the Salesforce API
179
+ test_files: []
180
+