synchronisable 0.0.3 → 0.0.4
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 +4 -4
- data/README.md +110 -4
- data/TODO.md +23 -15
- data/lib/generators/synchronisable/templates/initializer.rb +4 -0
- data/lib/synchronisable/attribute_mapper.rb +39 -0
- data/lib/synchronisable/context.rb +1 -1
- data/lib/synchronisable/data_builder.rb +58 -0
- data/lib/synchronisable/dsl/associations/association.rb +5 -2
- data/lib/synchronisable/input_descriptor.rb +42 -0
- data/lib/synchronisable/locale/en.yml +3 -3
- data/lib/synchronisable/model.rb +1 -1
- data/lib/synchronisable/source.rb +30 -10
- data/lib/synchronisable/synchronizer.rb +12 -32
- data/lib/synchronisable/version.rb +1 -1
- data/lib/synchronisable/worker.rb +26 -49
- data/lib/synchronisable.rb +7 -1
- data/spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb +2 -0
- data/spec/dummy/app/synchronizers/match_synchronizer.rb +0 -2
- data/spec/dummy/config/application.rb +0 -1
- data/spec/dummy/config/initializers/{synchronizable.rb → synchronisable.rb} +0 -0
- data/spec/factories/import.rb +1 -1
- data/spec/models/team_spec.rb +86 -1
- data/spec/spec_helper.rb +2 -2
- data/spec/synchronisable/support/shared/contexts.rb +33 -0
- data/spec/synchronisable/support/shared/examples.rb +0 -0
- data/synchronisable.gemspec +1 -1
- metadata +68 -47
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'synchronisable/dsl/macro'
|
2
|
+
require 'synchronisable/attribute_mapper'
|
2
3
|
require 'synchronisable/dsl/associations'
|
3
4
|
require 'synchronisable/exceptions'
|
4
5
|
|
@@ -35,11 +36,7 @@ module Synchronisable
|
|
35
36
|
|
36
37
|
# Logger that will be used during synchronization
|
37
38
|
# of this particular model.
|
38
|
-
|
39
|
-
# `STDOUT` will be used for output.
|
40
|
-
attribute :logger, default: -> {
|
41
|
-
defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
42
|
-
}
|
39
|
+
attribute :logger, default: -> { Synchronisable.logging[:logger] }
|
43
40
|
|
44
41
|
# Lambda that returns array of hashes with remote attributes.
|
45
42
|
method :fetch, default: -> { [] }
|
@@ -110,7 +107,6 @@ module Synchronisable
|
|
110
107
|
def extract_remote_id(attrs)
|
111
108
|
id = attrs.delete(remote_id)
|
112
109
|
ensure_remote_id(id)
|
113
|
-
id
|
114
110
|
end
|
115
111
|
|
116
112
|
# Maps the remote attributes to local model attributes.
|
@@ -120,11 +116,11 @@ module Synchronisable
|
|
120
116
|
#
|
121
117
|
# @api private
|
122
118
|
def map_attributes(attrs)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
119
|
+
AttributeMapper.map(attrs, mappings, {
|
120
|
+
:only => only,
|
121
|
+
:except => except,
|
122
|
+
:keep => associations.keys
|
123
|
+
})
|
128
124
|
end
|
129
125
|
|
130
126
|
%w(sync record_sync association_sync).each do |method|
|
@@ -135,21 +131,6 @@ module Synchronisable
|
|
135
131
|
|
136
132
|
private
|
137
133
|
|
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
134
|
def run_callbacks(method, args, block)
|
154
135
|
before = send(:"before_#{method}")
|
155
136
|
after = send(:"after_#{method}")
|
@@ -165,12 +146,11 @@ module Synchronisable
|
|
165
146
|
#
|
166
147
|
# @raise [MissedRemoteIdError] raised when data doesn't contain remote id
|
167
148
|
def ensure_remote_id(id)
|
168
|
-
if id.
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
end
|
149
|
+
return id if id.present?
|
150
|
+
raise MissedRemoteIdError, I18n.t(
|
151
|
+
'errors.missed_remote_id',
|
152
|
+
remote_id: remote_id
|
153
|
+
)
|
174
154
|
end
|
175
155
|
end
|
176
156
|
end
|
@@ -1,5 +1,8 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
1
3
|
require 'synchronisable/error_handler'
|
2
4
|
require 'synchronisable/context'
|
5
|
+
require 'synchronisable/data_builder'
|
3
6
|
require 'synchronisable/source'
|
4
7
|
require 'synchronisable/models/import'
|
5
8
|
|
@@ -41,11 +44,8 @@ module Synchronisable
|
|
41
44
|
error_handler = ErrorHandler.new(@logger, context)
|
42
45
|
context.before = @model.imports_count
|
43
46
|
|
44
|
-
|
45
|
-
|
46
|
-
# TODO: Handle case when only array of ids is given
|
47
|
-
# What to do with associations?
|
48
|
-
|
47
|
+
hashes = DataBuilder.build(@model, @synchronizer, data)
|
48
|
+
hashes.each do |attrs|
|
49
49
|
source = Source.new(@model, @parent, attrs)
|
50
50
|
error_handler.handle(source) do
|
51
51
|
@synchronizer.with_sync_callbacks(source) do
|
@@ -71,20 +71,17 @@ module Synchronisable
|
|
71
71
|
|
72
72
|
def sync
|
73
73
|
@logger.progname = "#{@model} synchronization"
|
74
|
-
|
74
|
+
log_info('STARTING', :yellow, true)
|
75
75
|
|
76
76
|
context = Context.new(@model, @parent.try(:model))
|
77
77
|
yield context
|
78
78
|
|
79
|
-
|
80
|
-
|
81
|
-
@logger.progname = nil
|
79
|
+
log_info('DONE', :yellow, true)
|
80
|
+
log_info(context.summary_message, :cyan, true)
|
82
81
|
|
83
82
|
context
|
84
83
|
end
|
85
84
|
|
86
|
-
# TODO: Think about how to move it from here to Source or some other place
|
87
|
-
|
88
85
|
# Method called by {#run} for each remote model attribute hash
|
89
86
|
#
|
90
87
|
# @param source [Synchronisable::Source] synchronization source
|
@@ -93,44 +90,21 @@ module Synchronisable
|
|
93
90
|
# without errors, `false` otherwise
|
94
91
|
def sync_record(source)
|
95
92
|
@synchronizer.with_record_sync_callbacks(source) do
|
96
|
-
source.
|
93
|
+
source.prepare
|
97
94
|
|
98
|
-
|
95
|
+
log_info(source.dump_message, :green)
|
99
96
|
|
100
97
|
if source.updatable?
|
101
|
-
|
98
|
+
log_info("updating #{@model}: #{source.local_record.id}", :blue)
|
99
|
+
source.update_record
|
102
100
|
else
|
103
|
-
create_record_pair
|
101
|
+
source.create_record_pair
|
102
|
+
log_info("#{@model} (id: #{source.local_record.id}) was created", :blue)
|
103
|
+
log_info("#{source.import_record.class}: #{source.import_record.id} was created", :blue)
|
104
104
|
end
|
105
105
|
end
|
106
106
|
end
|
107
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
|
-
:synchronisable_id => local_record.id,
|
121
|
-
:synchronisable_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
108
|
def set_record_foreign_keys(source)
|
135
109
|
reflection = belongs_to_parent_reflection
|
136
110
|
return unless reflection
|
@@ -148,9 +122,7 @@ module Synchronisable
|
|
148
122
|
# @see Synchronisable::DSL::Associations
|
149
123
|
# @see Synchronisable::DSL::Associations::Association
|
150
124
|
def sync_associations(source)
|
151
|
-
|
152
|
-
@logger.info "starting associations sync"
|
153
|
-
end
|
125
|
+
log_info("starting associations sync", :blue) if source.associations.present?
|
154
126
|
|
155
127
|
source.associations.each do |association, ids|
|
156
128
|
ids.each { |id| sync_association(source, id, association) }
|
@@ -158,9 +130,7 @@ module Synchronisable
|
|
158
130
|
end
|
159
131
|
|
160
132
|
def sync_association(source, id, association)
|
161
|
-
|
162
|
-
@logger.info "synchronizing association with id: #{id}"
|
163
|
-
end
|
133
|
+
log_info("synchronizing association with id: #{id}", :blue)
|
164
134
|
|
165
135
|
@synchronizer.with_association_sync_callbacks(source, id, association) do
|
166
136
|
attrs = association.model.synchronizer.find.(id)
|
@@ -184,8 +154,15 @@ module Synchronisable
|
|
184
154
|
@model.reflections.values
|
185
155
|
end
|
186
156
|
|
187
|
-
def
|
188
|
-
|
157
|
+
def log_info(msg, color = :white, force = true)
|
158
|
+
text = msg.colorize(color) if colorize_logging?
|
159
|
+
@logger.info(text) if force || verbose_logging?
|
160
|
+
end
|
161
|
+
|
162
|
+
%i(verbose colorize).each do |name|
|
163
|
+
define_method("#{name}_logging?".to_sym) do
|
164
|
+
Synchronisable.logging[name]
|
165
|
+
end
|
189
166
|
end
|
190
167
|
end
|
191
168
|
end
|
data/lib/synchronisable.rb
CHANGED
@@ -48,7 +48,13 @@ module Synchronisable
|
|
48
48
|
{}
|
49
49
|
end
|
50
50
|
config_accessor :logging do
|
51
|
+
default_logger = -> { Logger.new(STDOUT) }
|
52
|
+
rails_logger = -> { Rails.logger || default_logger.() }
|
53
|
+
|
54
|
+
logger = defined?(Rails) ? rails_logger.() : default_logger.()
|
55
|
+
|
51
56
|
{
|
57
|
+
:logger => logger,
|
52
58
|
:verbose => true,
|
53
59
|
:colorize => true
|
54
60
|
}
|
@@ -59,7 +65,7 @@ module Synchronisable
|
|
59
65
|
# @param models [Array] array of models that should be synchronized.
|
60
66
|
# This take a precedence over models defined in {Synchronisable#models}.
|
61
67
|
# If this parameter is not specified and {Synchronisable#models} is empty,
|
62
|
-
# than it will try to sync only those models which have a corresponding synchronizers
|
68
|
+
# than it will try to sync only those models which have a corresponding synchronizers
|
63
69
|
#
|
64
70
|
# @return [Array<[Synchronisable::Context]>] array of synchronization contexts
|
65
71
|
#
|
File without changes
|
data/spec/factories/import.rb
CHANGED
data/spec/models/team_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'pry-byebug'
|
2
3
|
|
3
4
|
describe Team do
|
4
5
|
describe 'synchronization' do
|
@@ -36,7 +37,91 @@ describe Team do
|
|
36
37
|
it { is_expected.to change { Synchronisable::Import.count }.by(6) }
|
37
38
|
end
|
38
39
|
|
39
|
-
|
40
|
+
describe 'restrict sync to specific record(s)' do
|
41
|
+
context 'when remote id is specified' do
|
42
|
+
context "when local record doesn't exist" do
|
43
|
+
subject { -> { Team.sync('team_0') } }
|
44
|
+
|
45
|
+
it { is_expected.to change { Team.count }.by(1) }
|
46
|
+
it { is_expected.to change { Synchronisable::Import.count }.by(3) }
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when local record exists' do
|
50
|
+
include_context 'team_0 import'
|
51
|
+
|
52
|
+
subject do
|
53
|
+
-> {
|
54
|
+
Team.sync('team_0')
|
55
|
+
team_0.reload
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
it { is_expected.not_to change { Team.count } }
|
60
|
+
|
61
|
+
it { is_expected.to change { Player.count }.by(2) }
|
62
|
+
it { is_expected.to change { Synchronisable::Import.count }.by(2) }
|
63
|
+
|
64
|
+
it { is_expected.to change { team_0.name } }
|
65
|
+
it { is_expected.to change { team_0.country } }
|
66
|
+
it { is_expected.to change { team_0.city } }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when there is 2 imports with corresponding local records' do
|
71
|
+
include_context 'team imports'
|
72
|
+
|
73
|
+
context 'when local id is specified' do
|
74
|
+
subject do
|
75
|
+
-> {
|
76
|
+
Team.sync(team_0.id)
|
77
|
+
team_0.reload
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
it { is_expected.not_to change { Team.count } }
|
82
|
+
|
83
|
+
it { is_expected.to change { Player.count }.by(2) }
|
84
|
+
it { is_expected.to change { Synchronisable::Import.count }.by(2) }
|
85
|
+
|
86
|
+
it { is_expected.to change { team_0.name } }
|
87
|
+
it { is_expected.to change { team_0.country } }
|
88
|
+
it { is_expected.to change { team_0.city } }
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when array of local ids is specified' do
|
92
|
+
subject do
|
93
|
+
-> {
|
94
|
+
Team.sync([team_0.id, team_1.id])
|
95
|
+
[team_0, team_1].each(&:reload)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
it { is_expected.not_to change { Team.count } }
|
100
|
+
|
101
|
+
it { is_expected.to change { Player.count }.by(4) }
|
102
|
+
it { is_expected.to change { Synchronisable::Import.count }.by(4) }
|
103
|
+
|
104
|
+
it { is_expected.to change { team_0.name } }
|
105
|
+
it { is_expected.to change { team_0.country } }
|
106
|
+
it { is_expected.to change { team_0.city } }
|
107
|
+
|
108
|
+
it { is_expected.to change { team_1.name } }
|
109
|
+
it { is_expected.to change { team_1.country } }
|
110
|
+
it { is_expected.to change { team_1.city } }
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when array of remote ids is specified' do
|
114
|
+
subject do
|
115
|
+
-> {
|
116
|
+
Team.sync([import_0.remote_id, import_1.remote_id])
|
117
|
+
[team_0, team_1].each(&:reload)
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'when remote id is not specified in attributes hash' do
|
40
125
|
subject { Team.sync([remote_attrs.last]) }
|
41
126
|
|
42
127
|
its(:errors) { should have(1).items }
|
data/spec/spec_helper.rb
CHANGED
@@ -64,8 +64,8 @@ Spork.prefork do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
FactoryGirl.define do
|
67
|
-
%w(match team player match_player stage tournament).each do |
|
68
|
-
sequence(:"#{
|
67
|
+
%w(remote match team player match_player stage tournament).each do |prefix|
|
68
|
+
sequence(:"#{prefix}_id") { |n| "#{prefix}_#{n}" }
|
69
69
|
end
|
70
70
|
end
|
71
71
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
shared_context 'team_0 import' do
|
2
|
+
let!(:import_0) do
|
3
|
+
create(:import,
|
4
|
+
:remote_id => 'team_0',
|
5
|
+
:synchronisable => create(:team,
|
6
|
+
:name => 'x',
|
7
|
+
:country => 'Russia',
|
8
|
+
:city => 'Moscow',
|
9
|
+
)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
let!(:team_0) { import_0.synchronisable }
|
13
|
+
end
|
14
|
+
|
15
|
+
shared_context 'team_1 import' do
|
16
|
+
let!(:import_1) do
|
17
|
+
create(:import,
|
18
|
+
:remote_id => 'team_1',
|
19
|
+
:synchronisable => create(:team,
|
20
|
+
:name => 'y',
|
21
|
+
:country => 'France',
|
22
|
+
:city => 'Paris',
|
23
|
+
)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
let!(:team_1) { import_1.synchronisable }
|
27
|
+
end
|
28
|
+
|
29
|
+
shared_context 'team imports' do
|
30
|
+
include_context 'team_0 import'
|
31
|
+
include_context 'team_1 import'
|
32
|
+
end
|
33
|
+
|
File without changes
|
data/synchronisable.gemspec
CHANGED
@@ -21,13 +21,13 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_runtime_dependency "activerecord", ">= 3.0.0"
|
22
22
|
spec.add_runtime_dependency "activesupport", ">= 3.0.0"
|
23
23
|
spec.add_runtime_dependency "i18n"
|
24
|
+
spec.add_runtime_dependency "colorize"
|
24
25
|
|
25
26
|
spec.add_development_dependency "bundler", "~> 1.5"
|
26
27
|
spec.add_development_dependency "rake"
|
27
28
|
spec.add_development_dependency "yard"
|
28
29
|
spec.add_development_dependency "rails", "~> 4.0.0"
|
29
30
|
spec.add_development_dependency "rspec-rails", "~> 3.0.0.beta2"
|
30
|
-
# spec.add_development_dependency "rspec-given"
|
31
31
|
spec.add_development_dependency "rspec-its"
|
32
32
|
spec.add_development_dependency "sqlite3"
|
33
33
|
spec.add_development_dependency "factory_girl"
|