synchronisable 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.ruby-version +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +9 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +22 -0
- data/Rakefile +7 -0
- data/TODO.md +20 -0
- data/bin/autospec +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/lib/generators/synchronizable/USAGE +2 -0
- data/lib/generators/synchronizable/install_generator.rb +24 -0
- data/lib/generators/synchronizable/templates/create_imports_migration.rb +21 -0
- data/lib/generators/synchronizable/templates/initializer.rb +14 -0
- data/lib/synchronizable.rb +92 -0
- data/lib/synchronizable/context.rb +25 -0
- data/lib/synchronizable/dsl/associations.rb +57 -0
- data/lib/synchronizable/dsl/associations/association.rb +59 -0
- data/lib/synchronizable/dsl/associations/has_many.rb +12 -0
- data/lib/synchronizable/dsl/associations/has_one.rb +13 -0
- data/lib/synchronizable/dsl/macro.rb +100 -0
- data/lib/synchronizable/dsl/macro/attribute.rb +41 -0
- data/lib/synchronizable/dsl/macro/expression.rb +51 -0
- data/lib/synchronizable/dsl/macro/method.rb +15 -0
- data/lib/synchronizable/error_handler.rb +50 -0
- data/lib/synchronizable/exceptions.rb +8 -0
- data/lib/synchronizable/locale/en.yml +17 -0
- data/lib/synchronizable/locale/ru.yml +17 -0
- data/lib/synchronizable/model.rb +54 -0
- data/lib/synchronizable/model/methods.rb +55 -0
- data/lib/synchronizable/models/import.rb +24 -0
- data/lib/synchronizable/source.rb +72 -0
- data/lib/synchronizable/synchronizer.rb +177 -0
- data/lib/synchronizable/synchronizers/synchronizer_default.rb +10 -0
- data/lib/synchronizable/version.rb +10 -0
- data/lib/synchronizable/worker.rb +191 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/gateways/gateway_base.rb +19 -0
- data/spec/dummy/app/gateways/match_gateway.rb +15 -0
- data/spec/dummy/app/gateways/player_gateway.rb +26 -0
- data/spec/dummy/app/gateways/stage_gateway.rb +18 -0
- data/spec/dummy/app/gateways/team_gateway.rb +18 -0
- data/spec/dummy/app/gateways/tournament_gateway.rb +15 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/match.rb +10 -0
- data/spec/dummy/app/models/match_player.rb +15 -0
- data/spec/dummy/app/models/player.rb +5 -0
- data/spec/dummy/app/models/stadium.rb +7 -0
- data/spec/dummy/app/models/stage.rb +6 -0
- data/spec/dummy/app/models/team.rb +5 -0
- data/spec/dummy/app/models/tournament.rb +7 -0
- data/spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb +14 -0
- data/spec/dummy/app/synchronizers/match_synchronizer.rb +71 -0
- data/spec/dummy/app/synchronizers/player_synchronizer.rb +19 -0
- data/spec/dummy/app/synchronizers/stage_synchronizer.rb +20 -0
- data/spec/dummy/app/synchronizers/tournament_synchronizer.rb +20 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +33 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +20 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/synchronizable.rb +2 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/db/migrate/20140422132431_create_teams.rb +11 -0
- data/spec/dummy/db/migrate/20140422132544_create_matches.rb +12 -0
- data/spec/dummy/db/migrate/20140422132708_create_players.rb +15 -0
- data/spec/dummy/db/migrate/20140422133122_create_match_players.rb +12 -0
- data/spec/dummy/db/migrate/20140422135244_create_imports.rb +21 -0
- data/spec/dummy/db/migrate/20140422140817_create_stadiums.rb +10 -0
- data/spec/dummy/db/migrate/20140507135800_create_tournaments.rb +13 -0
- data/spec/dummy/db/migrate/20140507135837_create_stages.rb +13 -0
- data/spec/dummy/db/migrate/20140507140039_add_stage_id_to_matches.rb +6 -0
- data/spec/dummy/db/schema.rb +103 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
- data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
- data/spec/factories/import.rb +5 -0
- data/spec/factories/match.rb +9 -0
- data/spec/factories/match_player.rb +9 -0
- data/spec/factories/player.rb +10 -0
- data/spec/factories/remote/match.rb +21 -0
- data/spec/factories/remote/player.rb +18 -0
- data/spec/factories/remote/stage.rb +26 -0
- data/spec/factories/remote/team.rb +21 -0
- data/spec/factories/remote/tournament.rb +18 -0
- data/spec/factories/stadium.rb +7 -0
- data/spec/factories/team.rb +16 -0
- data/spec/models/match_spec.rb +41 -0
- data/spec/models/team_spec.rb +85 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/synchronizable/dsl/macro_spec.rb +59 -0
- data/spec/synchronizable/models/import_spec.rb +10 -0
- data/spec/synchronizable/support/has_macro.rb +12 -0
- data/spec/synchronizable/support/has_macro_subclass.rb +9 -0
- data/spec/synchronizable/synchronizable_spec.rb +60 -0
- data/synchronizable.gemspec +39 -0
- 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,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
|