synchronisable 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +2 -0
  5. data/.travis.yml +4 -0
  6. data/Gemfile +9 -0
  7. data/Guardfile +14 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +22 -0
  10. data/Rakefile +7 -0
  11. data/TODO.md +20 -0
  12. data/bin/autospec +16 -0
  13. data/bin/rake +16 -0
  14. data/bin/rspec +16 -0
  15. data/lib/generators/synchronizable/USAGE +2 -0
  16. data/lib/generators/synchronizable/install_generator.rb +24 -0
  17. data/lib/generators/synchronizable/templates/create_imports_migration.rb +21 -0
  18. data/lib/generators/synchronizable/templates/initializer.rb +14 -0
  19. data/lib/synchronizable.rb +92 -0
  20. data/lib/synchronizable/context.rb +25 -0
  21. data/lib/synchronizable/dsl/associations.rb +57 -0
  22. data/lib/synchronizable/dsl/associations/association.rb +59 -0
  23. data/lib/synchronizable/dsl/associations/has_many.rb +12 -0
  24. data/lib/synchronizable/dsl/associations/has_one.rb +13 -0
  25. data/lib/synchronizable/dsl/macro.rb +100 -0
  26. data/lib/synchronizable/dsl/macro/attribute.rb +41 -0
  27. data/lib/synchronizable/dsl/macro/expression.rb +51 -0
  28. data/lib/synchronizable/dsl/macro/method.rb +15 -0
  29. data/lib/synchronizable/error_handler.rb +50 -0
  30. data/lib/synchronizable/exceptions.rb +8 -0
  31. data/lib/synchronizable/locale/en.yml +17 -0
  32. data/lib/synchronizable/locale/ru.yml +17 -0
  33. data/lib/synchronizable/model.rb +54 -0
  34. data/lib/synchronizable/model/methods.rb +55 -0
  35. data/lib/synchronizable/models/import.rb +24 -0
  36. data/lib/synchronizable/source.rb +72 -0
  37. data/lib/synchronizable/synchronizer.rb +177 -0
  38. data/lib/synchronizable/synchronizers/synchronizer_default.rb +10 -0
  39. data/lib/synchronizable/version.rb +10 -0
  40. data/lib/synchronizable/worker.rb +191 -0
  41. data/spec/dummy/.gitignore +16 -0
  42. data/spec/dummy/Rakefile +6 -0
  43. data/spec/dummy/app/assets/images/.keep +0 -0
  44. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  45. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  46. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  47. data/spec/dummy/app/gateways/gateway_base.rb +19 -0
  48. data/spec/dummy/app/gateways/match_gateway.rb +15 -0
  49. data/spec/dummy/app/gateways/player_gateway.rb +26 -0
  50. data/spec/dummy/app/gateways/stage_gateway.rb +18 -0
  51. data/spec/dummy/app/gateways/team_gateway.rb +18 -0
  52. data/spec/dummy/app/gateways/tournament_gateway.rb +15 -0
  53. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  54. data/spec/dummy/app/mailers/.keep +0 -0
  55. data/spec/dummy/app/models/.keep +0 -0
  56. data/spec/dummy/app/models/match.rb +10 -0
  57. data/spec/dummy/app/models/match_player.rb +15 -0
  58. data/spec/dummy/app/models/player.rb +5 -0
  59. data/spec/dummy/app/models/stadium.rb +7 -0
  60. data/spec/dummy/app/models/stage.rb +6 -0
  61. data/spec/dummy/app/models/team.rb +5 -0
  62. data/spec/dummy/app/models/tournament.rb +7 -0
  63. data/spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb +14 -0
  64. data/spec/dummy/app/synchronizers/match_synchronizer.rb +71 -0
  65. data/spec/dummy/app/synchronizers/player_synchronizer.rb +19 -0
  66. data/spec/dummy/app/synchronizers/stage_synchronizer.rb +20 -0
  67. data/spec/dummy/app/synchronizers/tournament_synchronizer.rb +20 -0
  68. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  69. data/spec/dummy/bin/bundle +3 -0
  70. data/spec/dummy/bin/rails +4 -0
  71. data/spec/dummy/bin/rake +4 -0
  72. data/spec/dummy/config.ru +4 -0
  73. data/spec/dummy/config/application.rb +26 -0
  74. data/spec/dummy/config/boot.rb +6 -0
  75. data/spec/dummy/config/database.yml +25 -0
  76. data/spec/dummy/config/environment.rb +5 -0
  77. data/spec/dummy/config/environments/development.rb +33 -0
  78. data/spec/dummy/config/environments/production.rb +80 -0
  79. data/spec/dummy/config/environments/test.rb +36 -0
  80. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  81. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/spec/dummy/config/initializers/inflections.rb +20 -0
  83. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  84. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  85. data/spec/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/dummy/config/initializers/synchronizable.rb +2 -0
  87. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  88. data/spec/dummy/config/locales/en.yml +23 -0
  89. data/spec/dummy/config/routes.rb +56 -0
  90. data/spec/dummy/db/migrate/20140422132431_create_teams.rb +11 -0
  91. data/spec/dummy/db/migrate/20140422132544_create_matches.rb +12 -0
  92. data/spec/dummy/db/migrate/20140422132708_create_players.rb +15 -0
  93. data/spec/dummy/db/migrate/20140422133122_create_match_players.rb +12 -0
  94. data/spec/dummy/db/migrate/20140422135244_create_imports.rb +21 -0
  95. data/spec/dummy/db/migrate/20140422140817_create_stadiums.rb +10 -0
  96. data/spec/dummy/db/migrate/20140507135800_create_tournaments.rb +13 -0
  97. data/spec/dummy/db/migrate/20140507135837_create_stages.rb +13 -0
  98. data/spec/dummy/db/migrate/20140507140039_add_stage_id_to_matches.rb +6 -0
  99. data/spec/dummy/db/schema.rb +103 -0
  100. data/spec/dummy/db/seeds.rb +7 -0
  101. data/spec/dummy/lib/assets/.keep +0 -0
  102. data/spec/dummy/lib/tasks/.keep +0 -0
  103. data/spec/dummy/log/.keep +0 -0
  104. data/spec/dummy/public/404.html +58 -0
  105. data/spec/dummy/public/422.html +58 -0
  106. data/spec/dummy/public/500.html +57 -0
  107. data/spec/dummy/public/favicon.ico +0 -0
  108. data/spec/dummy/public/robots.txt +5 -0
  109. data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
  110. data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
  111. data/spec/factories/import.rb +5 -0
  112. data/spec/factories/match.rb +9 -0
  113. data/spec/factories/match_player.rb +9 -0
  114. data/spec/factories/player.rb +10 -0
  115. data/spec/factories/remote/match.rb +21 -0
  116. data/spec/factories/remote/player.rb +18 -0
  117. data/spec/factories/remote/stage.rb +26 -0
  118. data/spec/factories/remote/team.rb +21 -0
  119. data/spec/factories/remote/tournament.rb +18 -0
  120. data/spec/factories/stadium.rb +7 -0
  121. data/spec/factories/team.rb +16 -0
  122. data/spec/models/match_spec.rb +41 -0
  123. data/spec/models/team_spec.rb +85 -0
  124. data/spec/spec_helper.rb +64 -0
  125. data/spec/synchronizable/dsl/macro_spec.rb +59 -0
  126. data/spec/synchronizable/models/import_spec.rb +10 -0
  127. data/spec/synchronizable/support/has_macro.rb +12 -0
  128. data/spec/synchronizable/support/has_macro_subclass.rb +9 -0
  129. data/spec/synchronizable/synchronizable_spec.rb +60 -0
  130. data/synchronizable.gemspec +39 -0
  131. metadata +506 -0
@@ -0,0 +1,55 @@
1
+ require 'synchronizable/worker'
2
+ require 'synchronizable/models/import'
3
+
4
+ module Synchronizable
5
+ module Model
6
+ # Methods that will be attached to synchronizable model class.
7
+ module Methods
8
+ # Creates a new worker, that initiates synchronization
9
+ # for this particular model.
10
+ # If you have implemented `fetch` & `find` methods
11
+ # in your model synchronizer, than it will be used if no data supplied.
12
+ #
13
+ # @overload sync(data, options)
14
+ # @param options [Hash] synchronization options
15
+ # @option options [Hash] :include assocations to be synchronized.
16
+ # Use this option to override `has_one` & `has_many` assocations
17
+ # defined in model synchronizer.
18
+ # @overload sync(options)
19
+ # @overload sync(data)
20
+ # @overload sync
21
+ #
22
+ # @param data [Array<Hash>] array of hashes with remote attributes.
23
+ #
24
+ # @see Synchronizable::Worker
25
+ #
26
+ # @example Supplying array of hashes with remote attributes
27
+ # FooModel.sync([
28
+ # {
29
+ # :id => '123',
30
+ # :attr1 => 4,
31
+ # :attr2 => 'blah'
32
+ # },
33
+ # ...
34
+ # ])
35
+ #
36
+ # @example General usage
37
+ # FooModel.sync(:include => {
38
+ # :assocation_model => :nested_assocaiton_model
39
+ # })
40
+ #
41
+ # @example Football domain use case
42
+ # Match.sync(:include => {
43
+ # :match_players => :player
44
+ # })
45
+ def sync(*args)
46
+ Worker.run(self, *args)
47
+ end
48
+
49
+ # Count of import records for this model.
50
+ def imports_count
51
+ Import.where(synchronizable_type: self).count
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ module Synchronizable
2
+ class Import < ActiveRecord::Base
3
+ belongs_to :synchronizable, polymorphic: true
4
+
5
+ serialize :attrs, Hash
6
+
7
+ scope :with_synchronizable_type, ->(type) { where(synchronizable_type: type) }
8
+ scope :with_synchronizable_ids, ->(ids) { where(synchronizable_id: ids) }
9
+
10
+ scope :with_remote_id, ->(id) { where(remote_id: id.to_s) }
11
+ scope :with_remote_ids, ->(ids) { where(remote_id: ids.map(&:to_s)) }
12
+
13
+ def self.find_by_remote_id(id)
14
+ with_remote_id(id).first
15
+ end
16
+
17
+ def destroy_with_synchronizable
18
+ ActiveRecord::Base.transaction do
19
+ synchronizable.try :destroy
20
+ destroy
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ module Synchronizable
2
+ # Synchronization source.
3
+ class Source
4
+ attr_accessor :import_record
5
+ attr_reader :model, :remote_attrs,
6
+ :remote_id, :local_attrs, :associations
7
+
8
+ def initialize(model, parent, remote_attrs)
9
+ @model, @parent, @synchronizer = model, parent, model.synchronizer
10
+ @remote_attrs = remote_attrs.with_indifferent_access
11
+ end
12
+
13
+ # Extracts the `remote_id` from remote attributes, maps remote attirubtes
14
+ # to local attributes and tries to find import record for given model
15
+ # by extracted remote id.
16
+ #
17
+ # @api private
18
+ def build
19
+ @remote_id = @synchronizer.extract_remote_id(@remote_attrs)
20
+ @local_attrs = @synchronizer.map_attributes(@remote_attrs)
21
+ @associations = @synchronizer.associations_for(@local_attrs)
22
+
23
+ @local_attrs.delete_if do |key, _|
24
+ @associations.keys.any? { |a| a.key == key }
25
+ end
26
+
27
+ @import_record = Import.find_by(
28
+ :remote_id => @remote_id,
29
+ :synchronizable_type => @model
30
+ )
31
+
32
+ set_parent_attribute
33
+ end
34
+
35
+ def updatable?
36
+ @import_record.present? && local_record.present?
37
+ end
38
+
39
+ def local_record
40
+ @import_record.try(:synchronizable)
41
+ end
42
+
43
+ def dump_message
44
+ %Q(
45
+ remote_id: #{remote_id},
46
+ remote attributes: #{remote_attrs},
47
+ local attributes: #{local_attrs}
48
+ )
49
+ end
50
+
51
+ private
52
+
53
+ def set_parent_attribute
54
+ return unless @parent
55
+ name = parent_attribute_name
56
+ @local_attrs[name] = @parent.local_record.id if name
57
+ end
58
+
59
+ def parent_attribute_name
60
+ return nil unless parent_has_model_as_reflection?
61
+ parent_name = @parent.model.table_name.singularize
62
+ "#{parent_name}_id"
63
+ end
64
+
65
+ def parent_has_model_as_reflection?
66
+ @parent.model.reflections.values.any? do |reflection|
67
+ reflection.plural_name == @model.table_name &&
68
+ [:has_one, :has_many].include?(reflection.macro)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,177 @@
1
+ require 'synchronizable/dsl/macro'
2
+ require 'synchronizable/dsl/associations'
3
+ require 'synchronizable/exceptions'
4
+
5
+ module Synchronizable
6
+ # @abstract Subclass to create your model specific synchronizer class to
7
+ # setup synchronization attributes and behavior.
8
+ #
9
+ # @see Synchronizable::DSL::Macro
10
+ # @see Synchronizable::SynchronizerDefault
11
+ class Synchronizer
12
+ include Synchronizable::DSL::Macro
13
+ include Synchronizable::DSL::Associations
14
+
15
+ SYMBOL_ARRAY_CONVERTER = ->(source) { (source || []).map(&:to_s) }
16
+
17
+ # The name of remote `id` attribute.
18
+ attribute :remote_id, default: :id
19
+
20
+ # Mapping configuration between local model attributes and
21
+ # its remote counterpart (including id attribute).
22
+ attribute :mappings, converter: ->(source) {
23
+ source.try(:with_indifferent_access)
24
+ }
25
+
26
+ # Attributes that will be ignored.
27
+ attribute :except, converter: SYMBOL_ARRAY_CONVERTER
28
+
29
+ # The only attributes that will be used.
30
+ attribute :only, converter: SYMBOL_ARRAY_CONVERTER
31
+
32
+ # If set to `true` than all local records that
33
+ # don't have corresponding remote counterpart will be destroyed.
34
+ attribute :destroy_missed, default: false
35
+
36
+ # Logger that will be used during synchronization
37
+ # of this particular model.
38
+ # Fallbacks to `Rails.logger` if available, otherwise
39
+ # `STDOUT` will be used for output.
40
+ attribute :logger, default: -> {
41
+ defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
42
+ }
43
+
44
+ # Lambda that returns array of hashes with remote attributes.
45
+ method :fetch, default: -> { [] }
46
+
47
+ # Lambda that returns a hash with remote attributes by id.
48
+ #
49
+ # @example Common use case
50
+ # class FooSynchronizer < Synchronizable::Synchronizer
51
+ # find do |id|
52
+ # remote_source.find { |h| h[:foo_id] == id } }
53
+ # end
54
+ # end
55
+ method :find
56
+
57
+ # Lambda, that will be called before synchronization
58
+ # of each record and its assocations.
59
+ #
60
+ # @param source [Synchronizable::Source] synchronization source
61
+ #
62
+ # @return [Boolean] `true` to continue sync, `false` to cancel
63
+ method :before_sync
64
+ # Lambda, that will be called every time
65
+ # after record and its associations is synchronized.
66
+ #
67
+ # @param source [Synchronizable::Source] synchronization source
68
+ method :after_sync
69
+
70
+ # Lambda, that will be called before synchronization of each record.
71
+ #
72
+ # @param source [Synchronizable::Source] synchronization source
73
+ #
74
+ # @return [Boolean] `true` to continue sync, `false` to cancel
75
+ method :before_record_sync
76
+
77
+ # Lambda, that will be called every time after record is synchronized.
78
+ #
79
+ # @param source [Synchronizable::Source] synchronization source
80
+ method :after_record_sync
81
+
82
+ # Lambda, that will be called before each association sync.
83
+ #
84
+ # @param source [Synchronizable::Source] synchronization source
85
+ # @param id association remote id
86
+ # @param association [Synchronizable::DSL::Associations::Association]
87
+ # association builder
88
+ #
89
+ # @return [Boolean] `true` to continue sync, `false` to cancel
90
+ method :before_association_sync
91
+
92
+ # Lambda, that will be called every time after association if synchronized.
93
+ #
94
+ # @param source [Synchronizable::Source] synchronization source
95
+ # @param id association remote id
96
+ # @param association [Synchronizable::DSL::Associations::Association]
97
+ # association builder
98
+ method :after_association_sync
99
+
100
+ class << self
101
+ # Extracts remote id from given attribute hash.
102
+ #
103
+ # @param attrs [Hash] remote attributes
104
+ # @return remote id value
105
+ #
106
+ # @raise [MissedRemoteIdError] raised when data doesn't contain remote id
107
+ # @see #ensure_remote_id
108
+ #
109
+ # @api private
110
+ def extract_remote_id(attrs)
111
+ id = attrs.delete(remote_id)
112
+ ensure_remote_id(id)
113
+ id
114
+ end
115
+
116
+ # Maps the remote attributes to local model attributes.
117
+ #
118
+ # @param attrs [Hash] remote attributes
119
+ # @return [Hash] local mapped attributes
120
+ #
121
+ # @api private
122
+ def map_attributes(attrs)
123
+ result = attrs.dup
124
+ apply_mappings(result) if mappings.present?
125
+ apply_only_filter(result) if only.present?
126
+ apply_except_filter(result) if except.present?
127
+ result
128
+ end
129
+
130
+ %w(sync record_sync association_sync).each do |method|
131
+ define_method(:"with_#{method}_callbacks") do |*args, &block|
132
+ run_callbacks(method, args, block)
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def apply_mappings(attrs)
139
+ attrs.transform_keys! { |key| mappings[key] || key }
140
+ end
141
+
142
+ def apply_only_filter(attrs)
143
+ attrs.keep_if do |key|
144
+ only.include?(key) ||
145
+ associations.keys.include?(key)
146
+ end
147
+ end
148
+
149
+ def apply_except_filter(attrs)
150
+ attrs.delete_if { |key| key.nil? || except.include?(key) }
151
+ end
152
+
153
+ def run_callbacks(method, args, block)
154
+ before = send(:"before_#{method}")
155
+ after = send(:"after_#{method}")
156
+
157
+ return unless before.(*args) if before
158
+ block.()
159
+ after.(*args) if after
160
+ end
161
+
162
+ # Throws the {Synchronizable::MissedRemoteIdError} if given id isn't present.
163
+ #
164
+ # @param id id to check
165
+ #
166
+ # @raise [MissedRemoteIdError] raised when data doesn't contain remote id
167
+ def ensure_remote_id(id)
168
+ if id.blank?
169
+ raise MissedRemoteIdError, I18n.t(
170
+ 'errors.missed_remote_id',
171
+ remote_id: remote_id
172
+ )
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,10 @@
1
+ require 'synchronizable/synchronizer'
2
+
3
+ module Synchronizable
4
+ # Default synchronizer to be used when
5
+ # model specific synchronizer is not defined.
6
+ #
7
+ # @api private
8
+ class SynchronizerDefault < Synchronizer
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Synchronizable
2
+ module VERSION
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 2
6
+ SUFFIX = nil
7
+
8
+ STRING = [MAJOR, MINOR, PATCH, SUFFIX].compact.join('.')
9
+ end
10
+ end
@@ -0,0 +1,191 @@
1
+ require 'synchronizable/error_handler'
2
+ require 'synchronizable/context'
3
+ require 'synchronizable/source'
4
+ require 'synchronizable/models/import'
5
+
6
+ module Synchronizable
7
+ # Responsible for model synchronization.
8
+ #
9
+ # @api private
10
+ class Worker
11
+ class << self
12
+ # Creates a new instance of worker and initiates model synchronization.
13
+ #
14
+ # @overload run(model, data, options)
15
+ # @param model [Class] model class to be synchronized
16
+ # @param options [Hash] synchronization options
17
+ # @option options [Hash] :include assocations to be synchronized.
18
+ # Use this option to override `has_one` & `has_many` assocations
19
+ # defined in model synchronizer.
20
+ # @overload run(model, data)
21
+ # @overload run(model)
22
+ #
23
+ # @return [Synchronizable::Context] synchronization context
24
+ def run(model, *args)
25
+ options = args.extract_options!
26
+ data = args.first
27
+
28
+ new(model, options).run(data)
29
+ end
30
+ end
31
+
32
+ # Initiates model synchronization.
33
+ #
34
+ # @param data [Array<Hash>] array of hashes with remote attriutes.
35
+ # If not specified worker will try to get the data
36
+ # using `fetch` lambda/proc defined in corresponding synchronizer
37
+ #
38
+ # @return [Synchronizable::Context] synchronization context
39
+ def run(data)
40
+ sync do |context|
41
+ error_handler = ErrorHandler.new(@logger, context)
42
+ context.before = @model.imports_count
43
+
44
+ data = @synchronizer.fetch.() if data.blank?
45
+ data.each do |attrs|
46
+ # TODO: Handle case when only array of ids is given
47
+ # What to do with associations?
48
+
49
+ source = Source.new(@model, @parent, attrs)
50
+ error_handler.handle(source) do
51
+ @synchronizer.with_sync_callbacks(source) do
52
+ sync_record(source)
53
+ sync_associations(source)
54
+ set_record_foreign_keys(source)
55
+ end
56
+ end
57
+ end
58
+
59
+ context.after = @model.imports_count
60
+ context.deleted = 0
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def initialize(model, options)
67
+ @model, @synchronizer = model, model.synchronizer
68
+ @logger = @synchronizer.logger
69
+ @parent = options[:parent]
70
+ end
71
+
72
+ def sync
73
+ @logger.progname = "#{@model} synchronization"
74
+ @logger.info 'starting'
75
+
76
+ context = Context.new(@model, @parent.try(:model))
77
+ yield context
78
+
79
+ @logger.info 'done'
80
+ @logger.info(context.summary_message)
81
+ @logger.progname = nil
82
+
83
+ context
84
+ end
85
+
86
+ # TODO: Think about how to move it from here to Source or some other place
87
+
88
+ # Method called by {#run} for each remote model attribute hash
89
+ #
90
+ # @param source [Synchronizable::Source] synchronization source
91
+ #
92
+ # @return [Boolean] `true` if synchronization was completed
93
+ # without errors, `false` otherwise
94
+ def sync_record(source)
95
+ @synchronizer.with_record_sync_callbacks(source) do
96
+ source.build
97
+
98
+ @logger.info(source.dump_message) if verbose_logging?
99
+
100
+ if source.updatable?
101
+ update_record(source)
102
+ else
103
+ create_record_pair(source)
104
+ end
105
+ end
106
+ end
107
+
108
+ def update_record(source)
109
+ if verbose_logging?
110
+ @logger.info "updating #{@model}: #{source.local_record.id}"
111
+ end
112
+
113
+ # TODO: Напрашивается, да?
114
+ source.local_record.update_attributes!(source.local_attrs)
115
+ end
116
+
117
+ def create_record_pair(source)
118
+ local_record = @model.create!(source.local_attrs)
119
+ import_record = Import.create!(
120
+ :synchronizable_id => local_record.id,
121
+ :synchronizable_type => @model.to_s,
122
+ :remote_id => source.remote_id,
123
+ :attrs => source.local_attrs
124
+ )
125
+
126
+ source.import_record = import_record
127
+
128
+ if verbose_logging?
129
+ @logger.info "#{@model}: #{local_record.id} was created"
130
+ @logger.info "#{import_record.class}: #{import_record.id} was created"
131
+ end
132
+ end
133
+
134
+ def set_record_foreign_keys(source)
135
+ reflection = belongs_to_parent_reflection
136
+ return unless reflection
137
+
138
+ belongs_to_key = "#{reflection.plural_name.singularize}_id"
139
+ source.local_record.update_attributes!(
140
+ belongs_to_key => @parent.local_record.id
141
+ )
142
+ end
143
+
144
+ # Synchronizes associations.
145
+ #
146
+ # @param source [Synchronizable::Source] synchronization source
147
+ #
148
+ # @see Synchronizable::DSL::Associations
149
+ # @see Synchronizable::DSL::Associations::Association
150
+ def sync_associations(source)
151
+ if verbose_logging? && source.associations.present?
152
+ @logger.info "starting associations sync"
153
+ end
154
+
155
+ source.associations.each do |association, ids|
156
+ ids.each { |id| sync_association(source, id, association) }
157
+ end
158
+ end
159
+
160
+ def sync_association(source, id, association)
161
+ if verbose_logging?
162
+ @logger.info "synchronizing association with id: #{id}"
163
+ end
164
+
165
+ @synchronizer.with_association_sync_callbacks(source, id, association) do
166
+ attrs = association.model.synchronizer.find.(id)
167
+ Worker.run(association.model, [attrs], { :parent => source })
168
+ end
169
+ end
170
+
171
+ # Finds a `belongs_to` reflection to the parent model.
172
+ #
173
+ # @see ActiveRecord::Reflection::AssociationReflection
174
+ def belongs_to_parent_reflection
175
+ return unless @parent
176
+
177
+ model_reflections.find do |r|
178
+ r.macro == :belongs_to &&
179
+ r.plural_name == @parent.model.table_name
180
+ end
181
+ end
182
+
183
+ def model_reflections
184
+ @model.reflections.values
185
+ end
186
+
187
+ def verbose_logging?
188
+ Synchronizable.logging[:verbose]
189
+ end
190
+ end
191
+ end