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