synchronisable 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # 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
- }
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
- 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
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.blank?
169
- raise MissedRemoteIdError, I18n.t(
170
- 'errors.missed_remote_id',
171
- remote_id: remote_id
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
@@ -2,7 +2,7 @@ module Synchronisable
2
2
  module VERSION
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- PATCH = 3
5
+ PATCH = 4
6
6
  SUFFIX = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, SUFFIX].compact.join('.')
@@ -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
- 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
-
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
- @logger.info 'starting'
74
+ log_info('STARTING', :yellow, true)
75
75
 
76
76
  context = Context.new(@model, @parent.try(:model))
77
77
  yield context
78
78
 
79
- @logger.info 'done'
80
- @logger.info(context.summary_message)
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.build
93
+ source.prepare
97
94
 
98
- @logger.info(source.dump_message) if verbose_logging?
95
+ log_info(source.dump_message, :green)
99
96
 
100
97
  if source.updatable?
101
- update_record(source)
98
+ log_info("updating #{@model}: #{source.local_record.id}", :blue)
99
+ source.update_record
102
100
  else
103
- create_record_pair(source)
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
- if verbose_logging? && source.associations.present?
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
- if verbose_logging?
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 verbose_logging?
188
- Synchronisable.logging[:verbose]
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
@@ -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
  #
@@ -1,6 +1,8 @@
1
1
  class BreakConventionTeamSynchronizer < Synchronisable::Synchronizer
2
2
  @gateway = TeamGateway.new
3
3
 
4
+ has_many :players
5
+
4
6
  remote_id :maet_id
5
7
  mappings(
6
8
  :eman => :name,
@@ -4,8 +4,6 @@ class MatchSynchronizer < Synchronisable::Synchronizer
4
4
  has_one :team, key: 'home_team_id'
5
5
  has_one :team, key: 'away_team_id'
6
6
 
7
- has_many :players
8
-
9
7
  remote_id :match_id
10
8
 
11
9
  mappings(
@@ -6,7 +6,6 @@ require 'action_mailer/railtie'
6
6
  require 'action_view/railtie'
7
7
  require 'sprockets/railtie'
8
8
 
9
- Bundler.require(*Rails.groups)
10
9
  require 'synchronisable'
11
10
 
12
11
  module Dummy
@@ -1,5 +1,5 @@
1
1
  FactoryGirl.define do
2
2
  factory :import, class: Synchronisable::Import do
3
- remote_id { generate :integer }
3
+ remote_id { generate :remote_id }
4
4
  end
5
5
  end
@@ -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
- context 'when remote id is not specified' do
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 |model|
68
- sequence(:"#{model}_id") { |n| "#{model}_#{n}" }
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
@@ -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"