synchronisable 0.0.2

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.
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