synchronisable 0.0.9 → 1.0.0
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/TODO.md +5 -4
- data/lib/synchronisable/controller.rb +105 -0
- data/lib/synchronisable/dsl/associations/association.rb +7 -4
- data/lib/synchronisable/dsl/associations/belongs_to.rb +16 -0
- data/lib/synchronisable/dsl/associations/has_many.rb +4 -0
- data/lib/synchronisable/dsl/associations/has_one.rb +4 -0
- data/lib/synchronisable/dsl/associations.rb +4 -5
- data/lib/synchronisable/error_handler.rb +3 -0
- data/lib/synchronisable/helper/logging.rb +23 -0
- data/lib/synchronisable/input_descriptor.rb +5 -0
- data/lib/synchronisable/{input_dispatcher.rb → input_parser.rb} +10 -8
- data/lib/synchronisable/locale/en.yml +1 -0
- data/lib/synchronisable/locale/ru.yml +1 -0
- data/lib/synchronisable/model/methods.rb +6 -6
- data/lib/synchronisable/source.rb +38 -34
- data/lib/synchronisable/synchronizer.rb +1 -1
- data/lib/synchronisable/version.rb +2 -2
- data/lib/synchronisable/worker/associations.rb +56 -0
- data/lib/synchronisable/worker/base.rb +23 -0
- data/lib/synchronisable/worker/record.rb +44 -0
- data/spec/dummy/app/gateways/team_group_statistic_gateway.rb +22 -0
- data/spec/dummy/app/models/stadium.rb +1 -1
- data/spec/dummy/app/models/team.rb +1 -0
- data/spec/dummy/app/models/team_group_statistic.rb +6 -0
- data/spec/dummy/app/synchronizers/team_group_statistic_synchronizer.rb +5 -0
- data/spec/dummy/db/migrate/20140609133855_create_team_group_statistic.rb +13 -0
- data/spec/dummy/db/schema.rb +13 -1
- data/spec/factories/remote/team_group_statistic.rb +13 -0
- data/spec/models/team_group_statistic_spec.rb +15 -0
- metadata +21 -4
- data/lib/synchronisable/worker.rb +0 -171
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ce97779ff8534be6e3b14f5e872e8788638badd
|
4
|
+
data.tar.gz: 9d5796423c0e433d71103d972a1a070d2d690bb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f2d9314d4e4e4fa31c4061038051531279311b0e25bd33e15293dd29304c86f86eee1f9d44223f73029ffbc864df789f0759e2ab150f0dc9361350ae898528f
|
7
|
+
data.tar.gz: f43e651e7528bd8e6ce9b7a09dba7ed6cb156e4f5ba4e66d5b1a8c906d877b78af7063c6fe8af44715392c27eaf63ca8199892cb44f078ba02fd24201f85cbde
|
data/TODO.md
CHANGED
@@ -5,14 +5,15 @@ Primary objectives
|
|
5
5
|
- [x] sync method
|
6
6
|
- [x] dependent syncronization & mapping
|
7
7
|
- [x] tests for Synchronisable.sync
|
8
|
-
- [ ] fix a mess with Context
|
8
|
+
- [ ] fix a mess with Context: return an array of sync contexts,
|
9
|
+
or aggregate all syncronization contexts into just one
|
9
10
|
- [ ] destroy_missed
|
10
|
-
- [
|
11
|
+
- [x] worker.rb refactoring
|
11
12
|
- [x] integrate with travis, stillmaintained, gemnasium,
|
12
|
-
|
13
|
+
codeclimate, coveralls, inch-pages, codersclan
|
13
14
|
- [ ] write a good README
|
14
15
|
- [ ] extended interface
|
15
|
-
-
|
16
|
+
- [ ] sync with include
|
16
17
|
- [x] sync with ids array
|
17
18
|
- [ ] handle case when association type is a :hash
|
18
19
|
- [ ] sync method for collection proxy (Model.where(condition).sync)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'pry-byebug'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
require 'synchronisable/error_handler'
|
5
|
+
require 'synchronisable/context'
|
6
|
+
require 'synchronisable/input_parser'
|
7
|
+
require 'synchronisable/source'
|
8
|
+
require 'synchronisable/models/import'
|
9
|
+
require 'synchronisable/helper/logging'
|
10
|
+
require 'synchronisable/worker/record'
|
11
|
+
require 'synchronisable/worker/associations'
|
12
|
+
|
13
|
+
module Synchronisable
|
14
|
+
# Responsible for model synchronization.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
class Controller
|
18
|
+
include Helper::Logging
|
19
|
+
|
20
|
+
attr_reader :logger
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Creates a new instance of controller and initiates model synchronization.
|
24
|
+
#
|
25
|
+
# @overload call(model, data, options)
|
26
|
+
# @param model [Class] model class to be synchronized
|
27
|
+
# @param options [Hash] synchronization options
|
28
|
+
# @option options [Hash] :include assocations to be synchronized.
|
29
|
+
# Use this option to override `has_one` & `has_many` assocations
|
30
|
+
# defined in model synchronizer.
|
31
|
+
# @overload call(model, data)
|
32
|
+
# @overload call(model)
|
33
|
+
#
|
34
|
+
# @return [Synchronisable::Context] synchronization context
|
35
|
+
def call(model, *args)
|
36
|
+
options = args.extract_options!
|
37
|
+
data = args.first
|
38
|
+
|
39
|
+
new(model, options).call(data)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Initiates model synchronization.
|
44
|
+
#
|
45
|
+
# @param data [Array<Hash>, Array<String>, Array<Integer>, String, Integer]
|
46
|
+
# synchronization data.
|
47
|
+
# If not specified, it will try to get array of hashes to sync with
|
48
|
+
# using defined gateway class or `fetch` lambda/proc
|
49
|
+
# defined in corresponding synchronizer
|
50
|
+
#
|
51
|
+
# @return [Synchronisable::Context] synchronization context
|
52
|
+
#
|
53
|
+
# @see Synchronisable::InputParser
|
54
|
+
def call(data)
|
55
|
+
sync do |context|
|
56
|
+
error_handler = ErrorHandler.new(logger, context)
|
57
|
+
context.before = @model.imports_count
|
58
|
+
|
59
|
+
hashes = @input.parse(data)
|
60
|
+
hashes.each do |attrs|
|
61
|
+
source = Source.new(@model, @parent, attrs)
|
62
|
+
|
63
|
+
error_handler.handle(source) do
|
64
|
+
source.prepare
|
65
|
+
|
66
|
+
record_worker = Worker::Record.new(@synchronizer, source)
|
67
|
+
associations_worker = Worker::Associations.new(@synchronizer, source)
|
68
|
+
|
69
|
+
@synchronizer.with_sync_callbacks(source) do
|
70
|
+
associations_worker.sync_parent_associations
|
71
|
+
record_worker.sync_record
|
72
|
+
associations_worker.sync_child_associations
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context.after = @model.imports_count
|
78
|
+
context.deleted = 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def initialize(model, options)
|
85
|
+
@model, @synchronizer = model, model.synchronizer
|
86
|
+
@logger = @synchronizer.logger
|
87
|
+
@parent = options[:parent]
|
88
|
+
|
89
|
+
@input = InputParser.new(@model, @synchronizer)
|
90
|
+
end
|
91
|
+
|
92
|
+
def sync
|
93
|
+
@logger.progname = "#{@model} synchronization"
|
94
|
+
log_info('STARTING', :yellow, true)
|
95
|
+
|
96
|
+
context = Context.new(@model, @parent.try(:model))
|
97
|
+
yield context
|
98
|
+
|
99
|
+
log_info('DONE', :yellow, true)
|
100
|
+
log_info(context.summary_message, :cyan, true)
|
101
|
+
|
102
|
+
context
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -4,6 +4,7 @@ module Synchronisable
|
|
4
4
|
module DSL
|
5
5
|
module Associations
|
6
6
|
# Association builder.
|
7
|
+
# Subclasses must implement #macro method.
|
7
8
|
class Association
|
8
9
|
include Synchronisable::DSL::Macro
|
9
10
|
|
@@ -17,9 +18,9 @@ module Synchronisable
|
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
|
-
self.valid_options = %i(key class_name required
|
21
|
+
self.valid_options = %i(key class_name required)
|
21
22
|
|
22
|
-
attr_reader :name, :model, :key, :required
|
23
|
+
attr_reader :name, :model, :key, :required
|
23
24
|
|
24
25
|
def initialize(synchronizer, name)
|
25
26
|
@synchronizer, @name = synchronizer, name.to_sym
|
@@ -30,7 +31,6 @@ module Synchronisable
|
|
30
31
|
|
31
32
|
@key = options[:key]
|
32
33
|
@required = options[:required]
|
33
|
-
@type = options[:type]
|
34
34
|
|
35
35
|
if options[:class_name].present?
|
36
36
|
@model = options[:class_name].constantize
|
@@ -41,11 +41,14 @@ module Synchronisable
|
|
41
41
|
@synchronizer.associations[@key] = self
|
42
42
|
end
|
43
43
|
|
44
|
+
def macro
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
44
48
|
protected
|
45
49
|
|
46
50
|
def set_defaults
|
47
51
|
@required ||= false
|
48
|
-
@type ||= :ids
|
49
52
|
|
50
53
|
@model ||= @name.to_s.classify.constantize
|
51
54
|
@key = "#{@name}_#{self.class.key_suffix}" unless @key.present?
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'synchronisable/dsl/associations/association'
|
2
|
+
|
3
|
+
module Synchronisable
|
4
|
+
module DSL
|
5
|
+
module Associations
|
6
|
+
# `belongs_to` association builder.
|
7
|
+
class BelongsTo < Association
|
8
|
+
key_suffix 'id'
|
9
|
+
|
10
|
+
def macro
|
11
|
+
:belongs_to
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'synchronisable/dsl/associations/has_one'
|
2
2
|
require 'synchronisable/dsl/associations/has_many'
|
3
|
+
require 'synchronisable/dsl/associations/belongs_to'
|
3
4
|
|
4
5
|
module Synchronisable
|
5
6
|
module DSL
|
@@ -17,7 +18,7 @@ module Synchronisable
|
|
17
18
|
subclass.associations = {}
|
18
19
|
end
|
19
20
|
|
20
|
-
[HasOne, HasMany].each do |klass|
|
21
|
+
[HasOne, HasMany, BelongsTo].each do |klass|
|
21
22
|
macro = klass.to_s.demodulize.underscore.to_sym
|
22
23
|
define_method(macro) do |name, options = {}|
|
23
24
|
klass.create(self, name, options)
|
@@ -34,11 +35,9 @@ module Synchronisable
|
|
34
35
|
# attributes hash doesn't required associations
|
35
36
|
def associations_for(attrs)
|
36
37
|
ensure_required_associations(attrs)
|
37
|
-
intersection = self.associations.map { |key, _| key } & attrs.keys
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
}]
|
39
|
+
intersection = self.associations.map { |key, _| key } & attrs.keys
|
40
|
+
Hash[intersection.map { |key| [self.associations[key], [*attrs[key]]] }]
|
42
41
|
end
|
43
42
|
|
44
43
|
private
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'pry-byebug'
|
2
|
+
|
1
3
|
module Synchronisable
|
2
4
|
# Helper class for synchronization errors handling.
|
3
5
|
#
|
@@ -40,6 +42,7 @@ module Synchronisable
|
|
40
42
|
I18n.t('errors.import_error',
|
41
43
|
:model => @context.model.to_s,
|
42
44
|
:error => e.message,
|
45
|
+
:backtrace => e.backtrace.join("\n"),
|
43
46
|
:remote_attrs => source.remote_attrs,
|
44
47
|
:local_attrs => source.local_attrs,
|
45
48
|
:import_record => source.import_record.inspect,
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Synchronisable
|
2
|
+
module Helper
|
3
|
+
# Provides logging helper methods.
|
4
|
+
# Class or module that includes this one
|
5
|
+
# should implement `logger` method.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
module Logging
|
9
|
+
protected
|
10
|
+
|
11
|
+
%i(verbose colorize).each do |name|
|
12
|
+
define_method("#{name}_logging?".to_sym) do
|
13
|
+
Synchronisable.logging[name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_info(msg, color = :white, force = true)
|
18
|
+
text = msg.colorize(color) if colorize_logging?
|
19
|
+
logger.info(text) if force || verbose_logging?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,20 +1,22 @@
|
|
1
1
|
require 'synchronisable/input_descriptor'
|
2
2
|
|
3
3
|
module Synchronisable
|
4
|
+
# Responsible for guessing the user input format.
|
5
|
+
#
|
4
6
|
# @api private
|
5
|
-
class
|
6
|
-
class << self
|
7
|
-
def dispatch(model, synchronizer, data)
|
8
|
-
new(model, synchronizer).dispatch(data)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
7
|
+
class InputParser
|
12
8
|
def initialize(model, synchronizer)
|
13
9
|
@model = model
|
14
10
|
@synchronizer = synchronizer
|
15
11
|
end
|
16
12
|
|
17
|
-
|
13
|
+
# Parses the user input.
|
14
|
+
#
|
15
|
+
# @param data [Array<Hash>, Array<String>, Array<Integer>, String, Integer]
|
16
|
+
# synchronization data to handle.
|
17
|
+
#
|
18
|
+
# @return [Array<Hash>] array of hashes with remote attributes
|
19
|
+
def parse(data)
|
18
20
|
input = InputDescriptor.new(data)
|
19
21
|
|
20
22
|
result = case
|
@@ -1,16 +1,17 @@
|
|
1
|
-
require 'synchronisable/
|
1
|
+
require 'synchronisable/controller'
|
2
2
|
require 'synchronisable/models/import'
|
3
3
|
|
4
4
|
module Synchronisable
|
5
5
|
module Model
|
6
6
|
# Methods that will be attached to synchronisable model class.
|
7
7
|
module Methods
|
8
|
-
# Creates a new
|
9
|
-
# for this particular model.
|
8
|
+
# Creates a new controller, that initiates synchronization
|
9
|
+
# for this particular model and its associations.
|
10
10
|
# If you have implemented `fetch` & `find` methods
|
11
11
|
# in your model synchronizer, than it will be used if no data supplied.
|
12
12
|
#
|
13
13
|
# @overload sync(data, options)
|
14
|
+
# @param data [Array<Hash>, Array<String>, Array<Integer>, String, Integer] synchronization data
|
14
15
|
# @param options [Hash] synchronization options
|
15
16
|
# @option options [Hash] :include assocations to be synchronized.
|
16
17
|
# Use this option to override `has_one` & `has_many` assocations
|
@@ -19,9 +20,8 @@ module Synchronisable
|
|
19
20
|
# @overload sync(data)
|
20
21
|
# @overload sync
|
21
22
|
#
|
22
|
-
# @param data [Array<Hash>] array of hashes with remote attributes.
|
23
23
|
#
|
24
|
-
# @see Synchronisable::
|
24
|
+
# @see Synchronisable::Controller
|
25
25
|
#
|
26
26
|
# @example Supplying array of hashes with remote attributes
|
27
27
|
# FooModel.sync([
|
@@ -43,7 +43,7 @@ module Synchronisable
|
|
43
43
|
# :match_players => :player
|
44
44
|
# })
|
45
45
|
def sync(*args)
|
46
|
-
|
46
|
+
Controller.call(self, *args)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Count of import records for this model.
|
@@ -1,10 +1,15 @@
|
|
1
1
|
module Synchronisable
|
2
|
+
# TODO: Massive refactoring needed
|
3
|
+
|
2
4
|
# Synchronization source.
|
3
5
|
class Source
|
6
|
+
CHILD_ASSOCIATION_KEYS = %i(has_one has_many)
|
7
|
+
PARENT_ASSOCIATION_KEYS = %i(belongs_to)
|
8
|
+
|
4
9
|
attr_accessor :import_record
|
5
|
-
attr_reader :
|
6
|
-
|
7
|
-
:
|
10
|
+
attr_reader :parent_associations, :child_associations
|
11
|
+
attr_reader :model, :remote_attrs, :remote_id,
|
12
|
+
:local_attrs, :import_ids
|
8
13
|
|
9
14
|
def initialize(model, parent, remote_attrs)
|
10
15
|
@model, @parent, @synchronizer = model, parent, model.synchronizer
|
@@ -13,6 +18,7 @@ module Synchronisable
|
|
13
18
|
|
14
19
|
# Prepares synchronization source:
|
15
20
|
# `remote_id`, `local_attributes`, `import_record` and `associations`.
|
21
|
+
#
|
16
22
|
# Sets foreign key if current model is specified as `has_one` or `has_many`
|
17
23
|
# association of parent model.
|
18
24
|
#
|
@@ -20,14 +26,14 @@ module Synchronisable
|
|
20
26
|
def prepare
|
21
27
|
@remote_id = @synchronizer.extract_remote_id(@remote_attrs)
|
22
28
|
@local_attrs = @synchronizer.map_attributes(@remote_attrs)
|
23
|
-
@associations = @synchronizer.associations_for(@local_attrs)
|
24
29
|
|
25
|
-
|
26
|
-
|
30
|
+
@associations = @synchronizer.associations_for(@local_attrs)
|
31
|
+
@parent_associations = @associations.select do |association|
|
32
|
+
PARENT_ASSOCIATION_KEYS.include? association.macro
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
@associations.keys.any? { |a| a.key == key }
|
35
|
+
@child_associations = @associations.select do |association|
|
36
|
+
CHILD_ASSOCIATION_KEYS.include? association.macro
|
31
37
|
end
|
32
38
|
|
33
39
|
@import_record = Import.find_by(
|
@@ -35,25 +41,15 @@ module Synchronisable
|
|
35
41
|
:synchronisable_type => @model
|
36
42
|
)
|
37
43
|
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
def updatable?
|
42
|
-
@import_record.present? && local_record.present?
|
43
|
-
end
|
44
|
+
remove_association_keys_from_local_attrs
|
44
45
|
|
45
|
-
|
46
|
-
|
46
|
+
# TODO: This should be in Synchronisable::RecordWorker
|
47
|
+
set_belongs_to_parent_foreign_key
|
47
48
|
end
|
48
49
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
:synchronisable_id => record.id,
|
53
|
-
:synchronisable_type => @model.to_s,
|
54
|
-
:remote_id => @remote_id.to_s,
|
55
|
-
:attrs => @local_attrs
|
56
|
-
)
|
50
|
+
def updatable?
|
51
|
+
import_record.present? &&
|
52
|
+
local_record.present?
|
57
53
|
end
|
58
54
|
|
59
55
|
def local_record
|
@@ -70,22 +66,30 @@ module Synchronisable
|
|
70
66
|
|
71
67
|
private
|
72
68
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
def remove_association_keys_from_local_attrs
|
70
|
+
@local_attrs.delete_if do |key, _|
|
71
|
+
@associations.keys.any? { |a| a.key == key }
|
72
|
+
end
|
77
73
|
end
|
78
74
|
|
79
|
-
def
|
80
|
-
return
|
81
|
-
|
75
|
+
def set_belongs_to_parent_foreign_key
|
76
|
+
return unless @parent && parent_has_current_model_as_reflection?
|
77
|
+
@local_attrs[parent_foreign_key_name] = @parent.local_record.id
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def parent_foreign_key_name
|
82
82
|
"#{parent_name}_id"
|
83
83
|
end
|
84
84
|
|
85
|
-
def
|
85
|
+
def parent_name
|
86
|
+
@parent.model.table_name.singularize
|
87
|
+
end
|
88
|
+
|
89
|
+
def parent_has_current_model_as_reflection?
|
86
90
|
@parent.model.reflections.values.any? do |reflection|
|
87
91
|
reflection.plural_name == @model.table_name &&
|
88
|
-
|
92
|
+
%i(has_one has_many).include?(reflection.macro)
|
89
93
|
end
|
90
94
|
end
|
91
95
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'synchronisable/worker/base'
|
2
|
+
|
3
|
+
module Synchronisable
|
4
|
+
module Worker
|
5
|
+
# Responsible for associations synchronization.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Associations < Base
|
9
|
+
# Synchronizes associations.
|
10
|
+
#
|
11
|
+
# @see Synchronisable::Source
|
12
|
+
# @see Synchronisable::DSL::Associations
|
13
|
+
# @see Synchronisable::DSL::Associations::Association
|
14
|
+
%w(child parent).each do |type|
|
15
|
+
define_method(:"sync_#{type}_associations") do
|
16
|
+
associations = @source.send(:"#{type}_associations")
|
17
|
+
log_info("starting #{type} associations sync", :blue) if associations.present?
|
18
|
+
|
19
|
+
associations.each do |association, ids|
|
20
|
+
ids.each do |id|
|
21
|
+
send(:"sync_#{type}_association", id, association)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
log_info("done #{type} associations sync", :blue) if associations.present?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def sync_parent_association(id, association)
|
32
|
+
log_info("synchronizing parent association with id: #{id}", :blue)
|
33
|
+
|
34
|
+
@synchronizer.with_association_sync_callbacks(@source, id, association) do
|
35
|
+
attrs = association.model.synchronizer.find(id)
|
36
|
+
Controller.call(association.model, [attrs])
|
37
|
+
|
38
|
+
import_record = Import.find_by(
|
39
|
+
:remote_id => id,
|
40
|
+
:synchronisable_type => association.model.to_s
|
41
|
+
)
|
42
|
+
@source.local_attrs[association.key] = import_record.synchronisable.id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def sync_child_association(id, association)
|
47
|
+
log_info("synchronizing child association with id: #{id}", :blue)
|
48
|
+
|
49
|
+
@synchronizer.with_association_sync_callbacks(@source, id, association) do
|
50
|
+
attrs = association.model.synchronizer.find(id)
|
51
|
+
Controller.call(association.model, [attrs], { :parent => @source })
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'synchronisable/helper/logging'
|
2
|
+
|
3
|
+
module Synchronisable
|
4
|
+
module Worker
|
5
|
+
# Base class for synchronization workers.
|
6
|
+
#
|
7
|
+
# @see Synchronisable::Worker::Record
|
8
|
+
# @see Synchronisable::Worker::Associations
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Base
|
12
|
+
include Helper::Logging
|
13
|
+
|
14
|
+
def initialize(synchronizer, source)
|
15
|
+
@synchronizer, @source = synchronizer, source
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger
|
19
|
+
@synchronizer.logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'synchronisable/worker/base'
|
2
|
+
|
3
|
+
module Synchronisable
|
4
|
+
module Worker
|
5
|
+
# Responsible for record synchronization.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Record < Base
|
9
|
+
# Synchronizes record.
|
10
|
+
def sync_record
|
11
|
+
@synchronizer.with_record_sync_callbacks(@source) do
|
12
|
+
log_info(@source.dump_message, :green)
|
13
|
+
|
14
|
+
if @source.updatable?
|
15
|
+
log_info("updating #{@source.model}: #{@source.local_record.id}", :blue)
|
16
|
+
|
17
|
+
update_record
|
18
|
+
else
|
19
|
+
create_record_pair
|
20
|
+
|
21
|
+
log_info("#{@source.model} (id: #{@source.local_record.id}) was created", :blue)
|
22
|
+
log_info("#{@source.import_record.class}: #{@source.import_record.id} was created", :blue)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def update_record
|
30
|
+
@source.local_record.update_attributes!(@source.local_attrs)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_record_pair
|
34
|
+
record = @source.model.create!(@source.local_attrs)
|
35
|
+
@source.import_record = Import.create!(
|
36
|
+
:synchronisable_id => record.id,
|
37
|
+
:synchronisable_type => @source.model.to_s,
|
38
|
+
:remote_id => @source.remote_id.to_s,
|
39
|
+
:attrs => @source.local_attrs
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class TeamGroupStatisticGateway < GatewayBase
|
2
|
+
def id_key
|
3
|
+
:id
|
4
|
+
end
|
5
|
+
|
6
|
+
def find(id)
|
7
|
+
raise NotImplementedError, 'Life is a bitch'
|
8
|
+
end
|
9
|
+
|
10
|
+
def source
|
11
|
+
@source ||= [
|
12
|
+
FactoryGirl.build(:remote_team_group_statistic,
|
13
|
+
:id => 'team_0',
|
14
|
+
:team_id => 'team_0'
|
15
|
+
),
|
16
|
+
FactoryGirl.build(:remote_team_group_statistic,
|
17
|
+
:id => 'team_1',
|
18
|
+
:team_id => 'team_1'
|
19
|
+
),
|
20
|
+
]
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateTeamGroupStatistic < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :team_group_statistics do |t|
|
4
|
+
t.references :team, index: true
|
5
|
+
t.integer :games_played
|
6
|
+
t.integer :games_won
|
7
|
+
t.integer :games_lost
|
8
|
+
t.integer :games_draw
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
#
|
12
12
|
# It's strongly recommended that you check this file into your version control system.
|
13
13
|
|
14
|
-
ActiveRecord::Schema.define(version:
|
14
|
+
ActiveRecord::Schema.define(version: 20140609133855) do
|
15
15
|
|
16
16
|
create_table "imports", force: true do |t|
|
17
17
|
t.string "synchronisable_type", null: false
|
@@ -82,6 +82,18 @@ ActiveRecord::Schema.define(version: 20140507140039) do
|
|
82
82
|
|
83
83
|
add_index "stages", ["tournament_id"], name: "index_stages_on_tournament_id"
|
84
84
|
|
85
|
+
create_table "team_group_statistics", force: true do |t|
|
86
|
+
t.integer "team_id"
|
87
|
+
t.integer "games_played"
|
88
|
+
t.integer "games_won"
|
89
|
+
t.integer "games_lost"
|
90
|
+
t.integer "games_draw"
|
91
|
+
t.datetime "created_at"
|
92
|
+
t.datetime "updated_at"
|
93
|
+
end
|
94
|
+
|
95
|
+
add_index "team_group_statistics", ["team_id"], name: "index_team_group_statistics_on_team_id"
|
96
|
+
|
85
97
|
create_table "teams", force: true do |t|
|
86
98
|
t.string "name"
|
87
99
|
t.string "country"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :remote_team_group_statistic, class: Hash do
|
3
|
+
id { generate :team_id }
|
4
|
+
team_id { id }
|
5
|
+
|
6
|
+
games_played { generate :integer }
|
7
|
+
games_won { generate :integer }
|
8
|
+
games_lost { generate :integer }
|
9
|
+
games_draw { generate :integer }
|
10
|
+
|
11
|
+
initialize_with { attributes }
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeamGroupStatistic do
|
4
|
+
describe 'synchronization' do
|
5
|
+
context 'sync with no data specified' do
|
6
|
+
subject { -> { TeamGroupStatistic.sync } }
|
7
|
+
|
8
|
+
it { is_expected.to change { TeamGroupStatistic.count }.by(2) }
|
9
|
+
it { is_expected.to change { Team.count }.by(2) }
|
10
|
+
it { is_expected.to change { Player.count }.by(4) }
|
11
|
+
|
12
|
+
it { is_expected.to change { Synchronisable::Import.count }.by(8) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: synchronisable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vasiliy Yorkin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -302,8 +302,10 @@ files:
|
|
302
302
|
- lib/synchronisable.rb
|
303
303
|
- lib/synchronisable/attribute_mapper.rb
|
304
304
|
- lib/synchronisable/context.rb
|
305
|
+
- lib/synchronisable/controller.rb
|
305
306
|
- lib/synchronisable/dsl/associations.rb
|
306
307
|
- lib/synchronisable/dsl/associations/association.rb
|
308
|
+
- lib/synchronisable/dsl/associations/belongs_to.rb
|
307
309
|
- lib/synchronisable/dsl/associations/has_many.rb
|
308
310
|
- lib/synchronisable/dsl/associations/has_one.rb
|
309
311
|
- lib/synchronisable/dsl/macro.rb
|
@@ -313,8 +315,9 @@ files:
|
|
313
315
|
- lib/synchronisable/error_handler.rb
|
314
316
|
- lib/synchronisable/exceptions.rb
|
315
317
|
- lib/synchronisable/gateway.rb
|
318
|
+
- lib/synchronisable/helper/logging.rb
|
316
319
|
- lib/synchronisable/input_descriptor.rb
|
317
|
-
- lib/synchronisable/
|
320
|
+
- lib/synchronisable/input_parser.rb
|
318
321
|
- lib/synchronisable/locale/en.yml
|
319
322
|
- lib/synchronisable/locale/ru.yml
|
320
323
|
- lib/synchronisable/model.rb
|
@@ -324,7 +327,9 @@ files:
|
|
324
327
|
- lib/synchronisable/synchronizer.rb
|
325
328
|
- lib/synchronisable/synchronizers/synchronizer_default.rb
|
326
329
|
- lib/synchronisable/version.rb
|
327
|
-
- lib/synchronisable/worker.rb
|
330
|
+
- lib/synchronisable/worker/associations.rb
|
331
|
+
- lib/synchronisable/worker/base.rb
|
332
|
+
- lib/synchronisable/worker/record.rb
|
328
333
|
- spec/dummy/.gitignore
|
329
334
|
- spec/dummy/Rakefile
|
330
335
|
- spec/dummy/app/assets/images/.keep
|
@@ -336,6 +341,7 @@ files:
|
|
336
341
|
- spec/dummy/app/gateways/player_gateway.rb
|
337
342
|
- spec/dummy/app/gateways/stage_gateway.rb
|
338
343
|
- spec/dummy/app/gateways/team_gateway.rb
|
344
|
+
- spec/dummy/app/gateways/team_group_statistic_gateway.rb
|
339
345
|
- spec/dummy/app/gateways/tournament_gateway.rb
|
340
346
|
- spec/dummy/app/helpers/application_helper.rb
|
341
347
|
- spec/dummy/app/mailers/.keep
|
@@ -346,11 +352,13 @@ files:
|
|
346
352
|
- spec/dummy/app/models/stadium.rb
|
347
353
|
- spec/dummy/app/models/stage.rb
|
348
354
|
- spec/dummy/app/models/team.rb
|
355
|
+
- spec/dummy/app/models/team_group_statistic.rb
|
349
356
|
- spec/dummy/app/models/tournament.rb
|
350
357
|
- spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb
|
351
358
|
- spec/dummy/app/synchronizers/match_synchronizer.rb
|
352
359
|
- spec/dummy/app/synchronizers/player_synchronizer.rb
|
353
360
|
- spec/dummy/app/synchronizers/stage_synchronizer.rb
|
361
|
+
- spec/dummy/app/synchronizers/team_group_statistic_synchronizer.rb
|
354
362
|
- spec/dummy/app/synchronizers/tournament_synchronizer.rb
|
355
363
|
- spec/dummy/app/views/layouts/application.html.erb
|
356
364
|
- spec/dummy/bin/bundle
|
@@ -383,6 +391,7 @@ files:
|
|
383
391
|
- spec/dummy/db/migrate/20140507135800_create_tournaments.rb
|
384
392
|
- spec/dummy/db/migrate/20140507135837_create_stages.rb
|
385
393
|
- spec/dummy/db/migrate/20140507140039_add_stage_id_to_matches.rb
|
394
|
+
- spec/dummy/db/migrate/20140609133855_create_team_group_statistic.rb
|
386
395
|
- spec/dummy/db/schema.rb
|
387
396
|
- spec/dummy/db/seeds.rb
|
388
397
|
- spec/dummy/lib/assets/.keep
|
@@ -403,10 +412,12 @@ files:
|
|
403
412
|
- spec/factories/remote/player.rb
|
404
413
|
- spec/factories/remote/stage.rb
|
405
414
|
- spec/factories/remote/team.rb
|
415
|
+
- spec/factories/remote/team_group_statistic.rb
|
406
416
|
- spec/factories/remote/tournament.rb
|
407
417
|
- spec/factories/stadium.rb
|
408
418
|
- spec/factories/team.rb
|
409
419
|
- spec/models/match_spec.rb
|
420
|
+
- spec/models/team_group_statistic_spec.rb
|
410
421
|
- spec/models/team_spec.rb
|
411
422
|
- spec/spec_helper.rb
|
412
423
|
- spec/synchronisable/dsl/macro_spec.rb
|
@@ -455,6 +466,7 @@ test_files:
|
|
455
466
|
- spec/dummy/app/gateways/player_gateway.rb
|
456
467
|
- spec/dummy/app/gateways/stage_gateway.rb
|
457
468
|
- spec/dummy/app/gateways/team_gateway.rb
|
469
|
+
- spec/dummy/app/gateways/team_group_statistic_gateway.rb
|
458
470
|
- spec/dummy/app/gateways/tournament_gateway.rb
|
459
471
|
- spec/dummy/app/helpers/application_helper.rb
|
460
472
|
- spec/dummy/app/mailers/.keep
|
@@ -465,11 +477,13 @@ test_files:
|
|
465
477
|
- spec/dummy/app/models/stadium.rb
|
466
478
|
- spec/dummy/app/models/stage.rb
|
467
479
|
- spec/dummy/app/models/team.rb
|
480
|
+
- spec/dummy/app/models/team_group_statistic.rb
|
468
481
|
- spec/dummy/app/models/tournament.rb
|
469
482
|
- spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb
|
470
483
|
- spec/dummy/app/synchronizers/match_synchronizer.rb
|
471
484
|
- spec/dummy/app/synchronizers/player_synchronizer.rb
|
472
485
|
- spec/dummy/app/synchronizers/stage_synchronizer.rb
|
486
|
+
- spec/dummy/app/synchronizers/team_group_statistic_synchronizer.rb
|
473
487
|
- spec/dummy/app/synchronizers/tournament_synchronizer.rb
|
474
488
|
- spec/dummy/app/views/layouts/application.html.erb
|
475
489
|
- spec/dummy/bin/bundle
|
@@ -502,6 +516,7 @@ test_files:
|
|
502
516
|
- spec/dummy/db/migrate/20140507135800_create_tournaments.rb
|
503
517
|
- spec/dummy/db/migrate/20140507135837_create_stages.rb
|
504
518
|
- spec/dummy/db/migrate/20140507140039_add_stage_id_to_matches.rb
|
519
|
+
- spec/dummy/db/migrate/20140609133855_create_team_group_statistic.rb
|
505
520
|
- spec/dummy/db/schema.rb
|
506
521
|
- spec/dummy/db/seeds.rb
|
507
522
|
- spec/dummy/lib/assets/.keep
|
@@ -522,10 +537,12 @@ test_files:
|
|
522
537
|
- spec/factories/remote/player.rb
|
523
538
|
- spec/factories/remote/stage.rb
|
524
539
|
- spec/factories/remote/team.rb
|
540
|
+
- spec/factories/remote/team_group_statistic.rb
|
525
541
|
- spec/factories/remote/tournament.rb
|
526
542
|
- spec/factories/stadium.rb
|
527
543
|
- spec/factories/team.rb
|
528
544
|
- spec/models/match_spec.rb
|
545
|
+
- spec/models/team_group_statistic_spec.rb
|
529
546
|
- spec/models/team_spec.rb
|
530
547
|
- spec/spec_helper.rb
|
531
548
|
- spec/synchronisable/dsl/macro_spec.rb
|
@@ -1,171 +0,0 @@
|
|
1
|
-
require 'colorize'
|
2
|
-
|
3
|
-
require 'synchronisable/error_handler'
|
4
|
-
require 'synchronisable/context'
|
5
|
-
require 'synchronisable/input_dispatcher'
|
6
|
-
require 'synchronisable/source'
|
7
|
-
require 'synchronisable/models/import'
|
8
|
-
|
9
|
-
module Synchronisable
|
10
|
-
# Responsible for model synchronization.
|
11
|
-
#
|
12
|
-
# @api private
|
13
|
-
class Worker
|
14
|
-
class << self
|
15
|
-
# Creates a new instance of worker and initiates model synchronization.
|
16
|
-
#
|
17
|
-
# @overload run(model, data, options)
|
18
|
-
# @param model [Class] model class to be synchronized
|
19
|
-
# @param options [Hash] synchronization options
|
20
|
-
# @option options [Hash] :include assocations to be synchronized.
|
21
|
-
# Use this option to override `has_one` & `has_many` assocations
|
22
|
-
# defined in model synchronizer.
|
23
|
-
# @overload run(model, data)
|
24
|
-
# @overload run(model)
|
25
|
-
#
|
26
|
-
# @return [Synchronisable::Context] synchronization context
|
27
|
-
def run(model, *args)
|
28
|
-
options = args.extract_options!
|
29
|
-
data = args.first
|
30
|
-
|
31
|
-
new(model, options).run(data)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# Initiates model synchronization.
|
36
|
-
#
|
37
|
-
# @param data [Array<Hash>] array of hashes with remote attriutes.
|
38
|
-
# If not specified worker will try to get the data
|
39
|
-
# using defined gateway class or `fetch` lambda/proc
|
40
|
-
# defined in corresponding synchronizer
|
41
|
-
#
|
42
|
-
# @return [Synchronisable::Context] synchronization context
|
43
|
-
#
|
44
|
-
# @see Synchronisable::InputDispatcher
|
45
|
-
def run(data)
|
46
|
-
sync do |context|
|
47
|
-
error_handler = ErrorHandler.new(@logger, context)
|
48
|
-
context.before = @model.imports_count
|
49
|
-
|
50
|
-
hashes = InputDispatcher.dispatch(@model, @synchronizer, data)
|
51
|
-
hashes.each do |attrs|
|
52
|
-
source = Source.new(@model, @parent, attrs)
|
53
|
-
error_handler.handle(source) do
|
54
|
-
@synchronizer.with_sync_callbacks(source) do
|
55
|
-
sync_record(source)
|
56
|
-
sync_associations(source)
|
57
|
-
set_record_foreign_keys(source)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
context.after = @model.imports_count
|
63
|
-
context.deleted = 0
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
private
|
68
|
-
|
69
|
-
def initialize(model, options)
|
70
|
-
@model, @synchronizer = model, model.synchronizer
|
71
|
-
@logger = @synchronizer.logger
|
72
|
-
@parent = options[:parent]
|
73
|
-
end
|
74
|
-
|
75
|
-
def sync
|
76
|
-
@logger.progname = "#{@model} synchronization"
|
77
|
-
log_info('STARTING', :yellow, true)
|
78
|
-
|
79
|
-
context = Context.new(@model, @parent.try(:model))
|
80
|
-
yield context
|
81
|
-
|
82
|
-
log_info('DONE', :yellow, true)
|
83
|
-
log_info(context.summary_message, :cyan, true)
|
84
|
-
|
85
|
-
context
|
86
|
-
end
|
87
|
-
|
88
|
-
# Method called by {#run} for each remote model attribute hash
|
89
|
-
#
|
90
|
-
# @param source [Synchronisable::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.prepare
|
97
|
-
|
98
|
-
log_info(source.dump_message, :green)
|
99
|
-
|
100
|
-
if source.updatable?
|
101
|
-
log_info("updating #{@model}: #{source.local_record.id}", :blue)
|
102
|
-
source.update_record
|
103
|
-
else
|
104
|
-
source.create_record_pair
|
105
|
-
log_info("#{@model} (id: #{source.local_record.id}) was created", :blue)
|
106
|
-
log_info("#{source.import_record.class}: #{source.import_record.id} was created", :blue)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def set_record_foreign_keys(source)
|
112
|
-
reflection = belongs_to_parent_reflection
|
113
|
-
return unless reflection
|
114
|
-
|
115
|
-
belongs_to_key = "#{reflection.plural_name.singularize}_id"
|
116
|
-
source.local_record.update_attributes!(
|
117
|
-
belongs_to_key => @parent.local_record.id
|
118
|
-
)
|
119
|
-
end
|
120
|
-
|
121
|
-
# Synchronizes associations.
|
122
|
-
#
|
123
|
-
# @param source [Synchronisable::Source] synchronization source
|
124
|
-
#
|
125
|
-
# @see Synchronisable::DSL::Associations
|
126
|
-
# @see Synchronisable::DSL::Associations::Association
|
127
|
-
def sync_associations(source)
|
128
|
-
log_info("starting associations sync", :blue) if source.associations.present?
|
129
|
-
|
130
|
-
source.associations.each do |association, ids|
|
131
|
-
ids.each { |id| sync_association(source, id, association) }
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def sync_association(source, id, association)
|
136
|
-
log_info("synchronizing association with id: #{id}", :blue)
|
137
|
-
|
138
|
-
@synchronizer.with_association_sync_callbacks(source, id, association) do
|
139
|
-
attrs = association.model.synchronizer.find(id)
|
140
|
-
Worker.run(association.model, [attrs], { :parent => source })
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# Finds a `belongs_to` reflection to the parent model.
|
145
|
-
#
|
146
|
-
# @see ActiveRecord::Reflection::AssociationReflection
|
147
|
-
def belongs_to_parent_reflection
|
148
|
-
return unless @parent
|
149
|
-
|
150
|
-
model_reflections.find do |r|
|
151
|
-
r.macro == :belongs_to &&
|
152
|
-
r.plural_name == @parent.model.table_name
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def model_reflections
|
157
|
-
@model.reflections.values
|
158
|
-
end
|
159
|
-
|
160
|
-
def log_info(msg, color = :white, force = true)
|
161
|
-
text = msg.colorize(color) if colorize_logging?
|
162
|
-
@logger.info(text) if force || verbose_logging?
|
163
|
-
end
|
164
|
-
|
165
|
-
%i(verbose colorize).each do |name|
|
166
|
-
define_method("#{name}_logging?".to_sym) do
|
167
|
-
Synchronisable.logging[name]
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|