restforce-db 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rubocop/custom/method_documentation.rb +65 -0
  4. data/.rubocop.yml +39 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +68 -0
  9. data/Rakefile +13 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/generators/restforce_generator.rb +19 -0
  13. data/lib/generators/templates/config.yml +8 -0
  14. data/lib/generators/templates/script +6 -0
  15. data/lib/restforce/db/command.rb +98 -0
  16. data/lib/restforce/db/configuration.rb +50 -0
  17. data/lib/restforce/db/instances/active_record.rb +48 -0
  18. data/lib/restforce/db/instances/base.rb +66 -0
  19. data/lib/restforce/db/instances/salesforce.rb +46 -0
  20. data/lib/restforce/db/mapping.rb +106 -0
  21. data/lib/restforce/db/model.rb +50 -0
  22. data/lib/restforce/db/record_type.rb +77 -0
  23. data/lib/restforce/db/record_types/active_record.rb +80 -0
  24. data/lib/restforce/db/record_types/base.rb +44 -0
  25. data/lib/restforce/db/record_types/salesforce.rb +94 -0
  26. data/lib/restforce/db/synchronizer.rb +57 -0
  27. data/lib/restforce/db/version.rb +10 -0
  28. data/lib/restforce/db/worker.rb +156 -0
  29. data/lib/restforce/db.rb +77 -0
  30. data/lib/restforce/extensions.rb +19 -0
  31. data/restforce-db.gemspec +41 -0
  32. data/test/cassettes/Restforce_DB/accessing_Salesforce/uses_the_configured_credentials.yml +43 -0
  33. data/test/cassettes/Restforce_DB_Instances_Salesforce/_copy_/updates_the_record_with_the_attributes_from_the_copied_object.yml +193 -0
  34. data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_local_record_with_the_passed_attributes.yml +193 -0
  35. data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_record_in_Salesforce_with_the_passed_attributes.yml +232 -0
  36. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/creates_a_record_in_Salesforce_from_the_passed_database_record_s_attributes.yml +158 -0
  37. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/updates_the_database_record_with_the_Salesforce_record_s_ID.yml +158 -0
  38. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/finds_existing_records_in_Salesforce.yml +157 -0
  39. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/returns_nil_when_no_matching_record_exists.yml +81 -0
  40. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_existing_record_in_the_database/updates_the_database_record.yml +158 -0
  41. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_Salesforce_record/populates_the_database_with_the_new_record.yml +158 -0
  42. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_database_record/populates_Salesforce_with_the_new_record.yml +235 -0
  43. data/test/lib/restforce/db/configuration_test.rb +38 -0
  44. data/test/lib/restforce/db/instances/active_record_test.rb +39 -0
  45. data/test/lib/restforce/db/instances/salesforce_test.rb +51 -0
  46. data/test/lib/restforce/db/mapping_test.rb +70 -0
  47. data/test/lib/restforce/db/model_test.rb +48 -0
  48. data/test/lib/restforce/db/record_type_test.rb +26 -0
  49. data/test/lib/restforce/db/record_types/active_record_test.rb +85 -0
  50. data/test/lib/restforce/db/record_types/salesforce_test.rb +46 -0
  51. data/test/lib/restforce/db/synchronizer_test.rb +84 -0
  52. data/test/lib/restforce/db_test.rb +24 -0
  53. data/test/support/active_record.rb +20 -0
  54. data/test/support/database_cleaner.rb +3 -0
  55. data/test/support/salesforce.rb +48 -0
  56. data/test/support/vcr.rb +23 -0
  57. data/test/test_helper.rb +25 -0
  58. metadata +287 -0
@@ -0,0 +1,106 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Mapping captures a set of mappings between database columns
6
+ # and Salesforce fields, providing utilities to transform hashes of
7
+ # attributes from one to the other.
8
+ class Mapping
9
+
10
+ attr_reader :mappings
11
+
12
+ # Public: Initialize a new Restforce::DB::Mapping.
13
+ #
14
+ # mappings - A Hash, with keys corresponding to the names of Ruby object
15
+ # attributes, and the values of those keys corresponding to the
16
+ # names of the related Salesforce fields.
17
+ def initialize(mappings = {})
18
+ @mappings = mappings
19
+ end
20
+
21
+ # Public: Append a new set of attribute mappings to the current set.
22
+ #
23
+ # mappings - A Hash, with keys corresponding to the names of Ruby object
24
+ # attributes, and the values of those keys corresponding to the
25
+ # names of the related Salesforce fields.
26
+ #
27
+ # Returns nothing.
28
+ def add_mappings(mappings = {})
29
+ @mappings.merge!(mappings)
30
+ end
31
+
32
+ # Public: Get a list of the relevant Salesforce field names for this
33
+ # mapping.
34
+ #
35
+ # Returns an Array.
36
+ def salesforce_fields
37
+ @mappings.values
38
+ end
39
+
40
+ # Public: Get a list of the relevant database column names for this
41
+ # mapping.
42
+ #
43
+ # Returns an Array.
44
+ def database_fields
45
+ @mappings.keys
46
+ end
47
+
48
+ # Public: Build a normalized Hash of attributes from the appropriate set
49
+ # of mappings. The keys of the resulting mapping Hash will correspond to
50
+ # the database column names.
51
+ #
52
+ # in_format - A Symbol reflecting the expected attribute list. Accepted
53
+ # values are :database and :salesforce.
54
+ #
55
+ # Yields the attribute name.
56
+ # Returns a Hash.
57
+ def attributes(from_format)
58
+ use_mappings =
59
+ case from_format
60
+ when :salesforce
61
+ @mappings
62
+ when :database
63
+ # Generate a mapping of database column names to record attributes.
64
+ database_fields.zip(database_fields)
65
+ end
66
+
67
+ use_mappings.each_with_object({}) do |(attribute, mapping), values|
68
+ values[attribute] = yield(mapping)
69
+ end
70
+ end
71
+
72
+ # Public: Convert a Hash of attributes to a format compatible with a
73
+ # specific platform.
74
+ #
75
+ # to_format - A Symbol reflecting the expected format. Accepted values
76
+ # are :database and :salesforce.
77
+ # attributes - A Hash, with keys corresponding to the attribute names in
78
+ # the format to convert away from.
79
+ #
80
+ # Examples
81
+ #
82
+ # mapping = Mapping.new(some_key: "Some_Field__c")
83
+ # mapping.convert(:salesforce, some_key: "some value")
84
+ # # => { "Some_Field__c" => "some value" }
85
+ #
86
+ # mapping.convert(:database, some_key: "some other value")
87
+ # # => { some_key: "some other value" }
88
+ #
89
+ # Returns a Hash.
90
+ def convert(to_format, attributes)
91
+ case to_format
92
+ when :database
93
+ attributes.dup
94
+ when :salesforce
95
+ @mappings.each_with_object({}) do |(attribute, mapping), converted|
96
+ next unless attributes.key?(attribute)
97
+ converted[mapping] = attributes[attribute]
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,50 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Model is a helper module which attaches some special
6
+ # DSL-style methods to an ActiveRecord class, allowing for easier mapping
7
+ # of the ActiveRecord class to an object type in Salesforce.
8
+ module Model
9
+
10
+ # :nodoc:
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ # :nodoc:
16
+ module ClassMethods
17
+
18
+ # Public: Initializes a Restforce::DB::RecordType defining this model's
19
+ # relationship to a Salesforce object type.
20
+ #
21
+ # salesforce_model - A String name of an object type in Salesforce.
22
+ # mappings - A Hash of mappings between database columns and
23
+ # fields in Salesforce.
24
+ #
25
+ # Returns a Restforce::DB::RecordType.
26
+ def map_to(salesforce_model, **mappings)
27
+ RecordType.new(
28
+ self,
29
+ salesforce_model,
30
+ mappings,
31
+ )
32
+ end
33
+
34
+ # Public: Append the passed mappings to this model.
35
+ #
36
+ # mappings - A Hash of database column names mapped to Salesforce
37
+ # fields.
38
+ #
39
+ # Returns nothing.
40
+ def add_mappings(mappings)
41
+ RecordType[self].add_mappings(mappings)
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,77 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::RecordType is an abstraction for a two-way binding between
6
+ # an ActiveRecord class and a Salesforce object type. It provides an
7
+ # interface for mapping database columns to Salesforce fields.
8
+ class RecordType
9
+
10
+ class << self
11
+
12
+ include Enumerable
13
+ attr_accessor :collection
14
+
15
+ # Public: Get the Restforce::DB::RecordType entry for the specified
16
+ # database model.
17
+ #
18
+ # database_model - A Class compatible with ActiveRecord::Base.
19
+ #
20
+ # Returns a Restforce::DB::RecordType.
21
+ def [](database_model)
22
+ collection[database_model]
23
+ end
24
+
25
+ # Public: Iterate through all registered Restforce::DB::RecordTypes.
26
+ #
27
+ # Yields one RecordType for each database-to-Salesforce mapping.
28
+ # Returns nothing.
29
+ def each
30
+ collection.each do |database_model, record_type|
31
+ yield database_model.name, record_type
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ self.collection ||= {}
38
+ attr_reader :mapping, :synchronizer
39
+
40
+ # Public: Initialize and register a Restforce::DB::RecordType.
41
+ #
42
+ # database_model - A Class compatible with ActiveRecord::Base.
43
+ # salesforce_model - A String name of an object type in Salesforce.
44
+ # mappings - A Hash of mappings between database columns and
45
+ # fields in Salesforce.
46
+ def initialize(database_model, salesforce_model, **mappings)
47
+ @mapping = Mapping.new(mappings)
48
+ @database_record_type = RecordTypes::ActiveRecord.new(database_model, @mapping)
49
+ @salesforce_record_type = RecordTypes::Salesforce.new(salesforce_model, @mapping)
50
+ @synchronizer = Synchronizer.new(@database_record_type, @salesforce_record_type)
51
+
52
+ self.class.collection[database_model] = self
53
+ end
54
+
55
+ # Public: Append the passed mappings to this model.
56
+ #
57
+ # mappings - A Hash of database column names mapped to Salesforce fields.
58
+ #
59
+ # Returns nothing.
60
+ def add_mappings(mappings)
61
+ @mapping.add_mappings mappings
62
+ end
63
+
64
+ # Public: Synchronize the records between the database and Salesforce.
65
+ #
66
+ # options - A Hash of options to pass to the synchronizer.
67
+ #
68
+ # Returns nothing.
69
+ def synchronize(options = {})
70
+ @synchronizer.run(options)
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,80 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::ActiveRecord serves as a wrapper for a
8
+ # single ActiveRecord::Base-compatible class, allowing for standard record
9
+ # lookups and attribute mappings.
10
+ class ActiveRecord < Base
11
+
12
+ # Public: Create an instance of this ActiveRecord model for the passed
13
+ # Salesforce instance.
14
+ #
15
+ # from_record - A Restforce::DB::Instances::Salesforce instance.
16
+ #
17
+ # Returns a Restforce::DB::Instances::ActiveRecord instance.
18
+ # Raises on any validation or database error.
19
+ def create!(from_record)
20
+ attributes = @mapping.convert(:database, from_record.attributes)
21
+ record = @record_type.create!(
22
+ attributes.merge(salesforce_id: from_record.id),
23
+ )
24
+
25
+ Instances::ActiveRecord.new(record, @mapping)
26
+ end
27
+
28
+ # Public: Find the instance of this ActiveRecord model corresponding to
29
+ # the passed salesforce_id.
30
+ #
31
+ # salesforce_id - The id of the record in Salesforce.
32
+ #
33
+ # Returns nil or a Restforce::DB::Instances::ActiveRecord instance.
34
+ def find(id)
35
+ record = @record_type.find_by(salesforce_id: id)
36
+ return nil unless record
37
+
38
+ Instances::ActiveRecord.new(record, @mapping)
39
+ end
40
+
41
+ # Public: Iterate through all ActiveRecord records of this type.
42
+ #
43
+ # options - A Hash of options which should be applied to the set of
44
+ # fetched records. Allowed options are:
45
+ # :before - A Time object defining the most recent update
46
+ # timestamp for which records should be returned.
47
+ # :after - A Time object defining the least recent update
48
+ # timestamp for which records should be returned.
49
+ #
50
+ # Yields a series of Restforce::DB::Instances::ActiveRecord instances.
51
+ # Returns nothing.
52
+ def each(options = {})
53
+ scope = @record_type
54
+ scope = scope.where("updated_at > ?", options[:after]) if options[:after]
55
+ scope = scope.where("updated_at < ?", options[:before]) if options[:before]
56
+
57
+ scope.find_each do |record|
58
+ yield Instances::ActiveRecord.new(record, @mapping)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Internal: Has this Salesforce record already been linked to a database
65
+ # record?
66
+ #
67
+ # record - A Restforce::DB::Instances::Salesforce instance.
68
+ #
69
+ # Returns a Boolean.
70
+ def synced?(record)
71
+ @record_type.exists?(salesforce_id: record.id)
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,44 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::Base defines common behavior for the other
8
+ # models defined in the Restforce::DB::RecordTypes namespace.
9
+ class Base
10
+
11
+ # Public: Initialize a new Restforce::DB::RecordTypes::Base.
12
+ #
13
+ # record_type - The name or class of the system record type.
14
+ # mapping - An instance of Restforce::DB::Mapping.
15
+ def initialize(record_type, mapping = Mapping.new)
16
+ @record_type = record_type
17
+ @mapping = mapping
18
+ end
19
+
20
+ # Public: Synchronize the passed record to the record type defined by
21
+ # this class.
22
+ #
23
+ # from_record - A Restforce::DB::Instances::Base instance.
24
+ #
25
+ # Returns a Restforce::DB::Instances::Base instance.
26
+ # Raises on any validation or external error.
27
+ def sync!(from_record)
28
+ if synced?(from_record)
29
+ record = find(from_record.id)
30
+ record.copy!(from_record)
31
+
32
+ record
33
+ else
34
+ create!(from_record)
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,94 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ module RecordTypes
6
+
7
+ # Restforce::DB::RecordTypes::Salesforce serves as a wrapper for a single
8
+ # Salesforce object class, allowing for standard record lookups and
9
+ # attribute mappings.
10
+ class Salesforce < Base
11
+
12
+ # Public: Create an instance of this Salesforce model for the passed
13
+ # database record.
14
+ #
15
+ # from_record - A Restforce::DB::Instances::ActiveRecord instance.
16
+ #
17
+ # Returns a Restforce::DB::Instances::Salesforce instance.
18
+ # Raises on any error from Salesforce.
19
+ def create!(from_record)
20
+ attributes = @mapping.convert(:salesforce, from_record.attributes)
21
+ record_id = DB.client.create!(@record_type, attributes)
22
+ from_record.update!(salesforce_id: record_id)
23
+
24
+ find(record_id)
25
+ end
26
+
27
+ # Public: Find the Salesforce record corresponding to the passed id.
28
+ #
29
+ # id - The id of the record in Salesforce.
30
+ #
31
+ # Returns nil or a Restforce::DB::Instances::Salesforce instance.
32
+ def find(id)
33
+ record = DB.client.query(
34
+ "select #{lookups} from #{@record_type} where Id = '#{id}'",
35
+ ).first
36
+
37
+ return unless record
38
+
39
+ Instances::Salesforce.new(record, @mapping)
40
+ end
41
+
42
+ # Public: Iterate through all Salesforce records of this type.
43
+ #
44
+ # options - A Hash of options which should be applied to the set of
45
+ # fetched records. Allowed options are:
46
+ # :before - A Time object defining the most recent update
47
+ # timestamp for which records should be returned.
48
+ # :after - A Time object defining the least recent update
49
+ # timestamp for which records should be returned.
50
+ #
51
+ # Yields a series of Restforce::DB::Instances::Salesforce instances.
52
+ # Returns nothing.
53
+ def each(options = {})
54
+ constraints = [
55
+ ("SystemModstamp <= #{options[:before].utc.iso8601}" if options[:before]),
56
+ ("SystemModstamp > #{options[:after].utc.iso8601}" if options[:after]),
57
+ ].compact.join(" and ")
58
+ constraints = " where #{constraints}" unless constraints.empty?
59
+
60
+ query = "select #{lookups} from #{@record_type}#{constraints}"
61
+
62
+ DB.client.query(query).each do |record|
63
+ yield Instances::Salesforce.new(record, @mapping)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Internal: Get a String of values to look up when the record is
70
+ # fetched from Salesforce. Includes all configured mappings and a
71
+ # handful of attributes for internal use.
72
+ #
73
+ # Returns a String.
74
+ def lookups
75
+ (Instances::Salesforce::INTERNAL_ATTRIBUTES + @mapping.salesforce_fields).join(", ")
76
+ end
77
+
78
+ # Internal: Has this database record already been linked to a Salesforce
79
+ # record?
80
+ #
81
+ # record - A Restforce::DB::Instances::Salesforce instance.
82
+ #
83
+ # Returns a Boolean.
84
+ def synced?(record)
85
+ record.synced?
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -0,0 +1,57 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Synchronizer is responsible for synchronizing the records
6
+ # in Salesforce with the records in the database. It relies on the mappings
7
+ # configured in instances of Restforce::DB::RecordTypes::Base to create and
8
+ # update records with the appropriate values.
9
+ class Synchronizer
10
+
11
+ attr_reader :last_run
12
+
13
+ # Public: Initialize a new Restforce::DB::Synchronizer.
14
+ #
15
+ # database_record_type - A Restforce::DB::RecordTypes::ActiveRecord
16
+ # instance.
17
+ # salesforce_record_type - A Restforce::DB::RecordTypes::Salesforce
18
+ # instance.
19
+ # last_run_time - A Time object reflecting the time of the most
20
+ # recent synchronization run. Runs will only
21
+ # synchronize data more recent than this stamp.
22
+ def initialize(database_record_type, salesforce_record_type, last_run_time = nil)
23
+ @database_record_type = database_record_type
24
+ @salesforce_record_type = salesforce_record_type
25
+ @last_run = last_run_time
26
+ end
27
+
28
+ # Public: Run the synchronize process, pulling in records from Salesforce
29
+ # and the database to determine which records need to be created and/or
30
+ # updated.
31
+ #
32
+ # NOTE: We bootstrap our record lookups to the exact same timespan, and
33
+ # run the Salesforce sync into the database first. This has the effect of
34
+ # overwriting recent changes to the database, in the event that Salesforce
35
+ # has also been updated since the last sync.
36
+ #
37
+ # options - A Hash of options for configuring the run. Currently unused.
38
+ #
39
+ # Returns the Time the run was performed.
40
+ def run(_options = {})
41
+ run_time = Time.now
42
+
43
+ @salesforce_record_type.each(after: last_run, before: run_time) do |record|
44
+ @database_record_type.sync!(record)
45
+ end
46
+ @database_record_type.each(after: last_run, before: run_time) do |record|
47
+ @salesforce_record_type.sync!(record)
48
+ end
49
+
50
+ @last_run = run_time
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,10 @@
1
+ module Restforce
2
+
3
+ # :nodoc:
4
+ module DB
5
+
6
+ VERSION = "0.1.4"
7
+
8
+ end
9
+
10
+ end
@@ -0,0 +1,156 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Worker represents the primary polling loop through which
6
+ # all record synchronization occurs.
7
+ class Worker
8
+
9
+ DEFAULT_INTERVAL = 5
10
+
11
+ class << self
12
+
13
+ attr_accessor :logger, :interval
14
+
15
+ # Public: Store the list of currently open file descriptors so that they
16
+ # may be reopened when a new process is spawned.
17
+ #
18
+ # Returns nothing.
19
+ def before_fork
20
+ return if @files_to_reopen
21
+
22
+ @files_to_reopen = []
23
+ ObjectSpace.each_object(File) do |file|
24
+ @files_to_reopen << file unless file.closed?
25
+ end
26
+ end
27
+
28
+ # Public: Reopen all file descriptors that have been stored through the
29
+ # before_fork hook.
30
+ #
31
+ # Returns nothing.
32
+ def after_fork
33
+ @files_to_reopen.each do |file|
34
+ begin
35
+ file.reopen file.path, "a+"
36
+ file.sync = true
37
+ rescue ::Exception # rubocop:disable HandleExceptions, RescueException
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ # Public: Initialize a new Restforce::DB::Worker.
45
+ #
46
+ # options - A Hash of options to configure the worker's run. Currently
47
+ # supported options are:
48
+ # interval - The maximum polling loop rest time.
49
+ # config - The path to a client configuration file.
50
+ # verbose - Display command line output? Defaults to false.
51
+ def initialize(options = {})
52
+ @verbose = options.fetch(:verbose) { false }
53
+ self.class.interval = options.fetch(:interval) { DEFAULT_INTERVAL }
54
+
55
+ Restforce::DB.configure { |config| config.parse(options[:config]) }
56
+ end
57
+
58
+ # Public: Start the polling loop for this Worker. Synchronizes all
59
+ # registered record types between the database and Salesforce, looping
60
+ # indefinitely until processing is interrupted by a signal.
61
+ #
62
+ # Returns nothing.
63
+ def start
64
+ trap("TERM") do
65
+ Thread.new { log "Exiting..." }
66
+ stop
67
+ end
68
+
69
+ trap("INT") do
70
+ Thread.new { log "Exiting..." }
71
+ stop
72
+ end
73
+
74
+ log "Starting synchronization..."
75
+
76
+ loop do
77
+ runtime = Benchmark.realtime do
78
+ Restforce::DB::RecordType.each do |name, record_type|
79
+ synchronize name, record_type
80
+ end
81
+ end
82
+
83
+ if runtime < self.class.interval && !stop?
84
+ sleep(self.class.interval - runtime)
85
+ end
86
+
87
+ break if stop?
88
+ end
89
+ end
90
+
91
+ # Public: Instruct the worker to stop running at the end of the current
92
+ # processing loop.
93
+ #
94
+ # Returns nothing.
95
+ def stop
96
+ @exit = true
97
+ end
98
+
99
+ private
100
+
101
+ # Internal: Synchronize the objects in the database and Salesforce
102
+ # corresponding to the passed record type.
103
+ #
104
+ # name - The String name of the record type to synchronize.
105
+ # record_type - A Restforce::DB::RecordType.
106
+ #
107
+ # Returns a Boolean.
108
+ def synchronize(name, record_type)
109
+ log "(#{name}) SYNCHRONIZING"
110
+ runtime = Benchmark.realtime { record_type.synchronize }
111
+ log format("(#{name}) COMPLETED after %.4f", runtime)
112
+
113
+ return true
114
+ rescue => e
115
+ error(e)
116
+
117
+ return false
118
+ end
119
+
120
+ # Internal: Has this worker been instructed to stop?
121
+ #
122
+ # Returns a boolean.
123
+ def stop?
124
+ @exit == true
125
+ end
126
+
127
+ # Internal: Log the passed text at the specified level.
128
+ #
129
+ # text - The piece of text which should be logged for this worker.
130
+ # level - The level at which the text should be logged. Defaults to :info.
131
+ #
132
+ # Returns nothing.
133
+ def log(text, level = :info)
134
+ text = "[Restforce::DB] #{text}"
135
+ puts text if @verbose
136
+
137
+ return unless self.class.logger
138
+
139
+ self.class.logger.send(level, "#{Time.now.strftime('%FT%T%z')}: #{text}")
140
+ end
141
+
142
+ # Internal: Log an error for the worker, outputting the entire error
143
+ # stacktrace and applying the appropriate log level.
144
+ #
145
+ # exception - An Exception object.
146
+ #
147
+ # Returns nothing.
148
+ def error(exception)
149
+ log "#{exception.message}\n#{exception.backtrace.join("\n")}", :error
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ end