dm-salesforce-adapter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+