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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.md +5 -4
  3. data/lib/synchronisable/controller.rb +105 -0
  4. data/lib/synchronisable/dsl/associations/association.rb +7 -4
  5. data/lib/synchronisable/dsl/associations/belongs_to.rb +16 -0
  6. data/lib/synchronisable/dsl/associations/has_many.rb +4 -0
  7. data/lib/synchronisable/dsl/associations/has_one.rb +4 -0
  8. data/lib/synchronisable/dsl/associations.rb +4 -5
  9. data/lib/synchronisable/error_handler.rb +3 -0
  10. data/lib/synchronisable/helper/logging.rb +23 -0
  11. data/lib/synchronisable/input_descriptor.rb +5 -0
  12. data/lib/synchronisable/{input_dispatcher.rb → input_parser.rb} +10 -8
  13. data/lib/synchronisable/locale/en.yml +1 -0
  14. data/lib/synchronisable/locale/ru.yml +1 -0
  15. data/lib/synchronisable/model/methods.rb +6 -6
  16. data/lib/synchronisable/source.rb +38 -34
  17. data/lib/synchronisable/synchronizer.rb +1 -1
  18. data/lib/synchronisable/version.rb +2 -2
  19. data/lib/synchronisable/worker/associations.rb +56 -0
  20. data/lib/synchronisable/worker/base.rb +23 -0
  21. data/lib/synchronisable/worker/record.rb +44 -0
  22. data/spec/dummy/app/gateways/team_group_statistic_gateway.rb +22 -0
  23. data/spec/dummy/app/models/stadium.rb +1 -1
  24. data/spec/dummy/app/models/team.rb +1 -0
  25. data/spec/dummy/app/models/team_group_statistic.rb +6 -0
  26. data/spec/dummy/app/synchronizers/team_group_statistic_synchronizer.rb +5 -0
  27. data/spec/dummy/db/migrate/20140609133855_create_team_group_statistic.rb +13 -0
  28. data/spec/dummy/db/schema.rb +13 -1
  29. data/spec/factories/remote/team_group_statistic.rb +13 -0
  30. data/spec/models/team_group_statistic_spec.rb +15 -0
  31. metadata +21 -4
  32. data/lib/synchronisable/worker.rb +0 -171
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 38932ee4a35a464734cae4beacbef691ffab0de5
4
- data.tar.gz: 28c7843ec8b46d09ec7208bb40f161d13b240203
3
+ metadata.gz: 9ce97779ff8534be6e3b14f5e872e8788638badd
4
+ data.tar.gz: 9d5796423c0e433d71103d972a1a070d2d690bb0
5
5
  SHA512:
6
- metadata.gz: 67242c4c189ae6bd9ed7023e86b07a1384162858d778dde9bcd222d4848d7bd006b9047a35dc37ba0bd3a7e91827ee0e327cf6e858b8c0dc8a370f7a1aada696
7
- data.tar.gz: ec7082b706d7ab227e6f1d31839a7cd158243b00ded14de8d90c45028f434b43f561c0b98aefd0b1743a01ea8bae97c5d22403acdc6109243691dfbb360acfb5
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
- - [ ] worker.rb refactoring
11
+ - [x] worker.rb refactoring
11
12
  - [x] integrate with travis, stillmaintained, gemnasium,
12
- codeclimate, coveralls, inch-pages, codersclan
13
+ codeclimate, coveralls, inch-pages, codersclan
13
14
  - [ ] write a good README
14
15
  - [ ] extended interface
15
- - [ ] sync with include
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 type)
21
+ self.valid_options = %i(key class_name required)
21
22
 
22
- attr_reader :name, :model, :key, :required, :type
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
@@ -6,6 +6,10 @@ module Synchronisable
6
6
  # `has_many` association builder.
7
7
  class HasMany < Association
8
8
  key_suffix 'ids'
9
+
10
+ def macro
11
+ :has_many
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -6,6 +6,10 @@ module Synchronisable
6
6
  # `has_one` association builder.
7
7
  class HasOne < Association
8
8
  key_suffix 'id'
9
+
10
+ def macro
11
+ :has_one
12
+ end
9
13
  end
10
14
  end
11
15
  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
- Hash[intersection.map { |key|
40
- [self.associations[key], [*attrs[key]]]
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,5 +1,10 @@
1
1
  module Synchronisable
2
+ # Provides a set of helper methods
3
+ # to describe user input.
4
+ #
2
5
  # @api private
6
+ #
7
+ # @see Synchronisable::InputParser
3
8
  class InputDescriptor
4
9
  attr_reader :data
5
10
 
@@ -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 InputDispatcher
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
- def dispatch(data)
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
@@ -5,6 +5,7 @@ en:
5
5
  missed_associations: Couldn't find associations %{keys} in %{attrs}
6
6
  import_error: |
7
7
  %{model} import error: %{error}.
8
+ backtrace: %{backtrace},
8
9
  remote attrs: %{remote_attrs},
9
10
  local attrs: %{local_attrs},
10
11
  import record: %{import_record},
@@ -5,6 +5,7 @@ ru:
5
5
  missed_associations: В хэше %{attrs} не указаны ассоциации %{keys}
6
6
  import_error: >
7
7
  Ошибка при импорте %{model}: %{error},
8
+ backtrace: %{backtrace},
8
9
  remote attrs: %{remote_attrs},
9
10
  local attrs: %{local_attrs},
10
11
  import record: %{import_record},
@@ -1,16 +1,17 @@
1
- require 'synchronisable/worker'
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 worker, that initiates synchronization
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::Worker
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
- Worker.run(self, *args)
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 :model, :remote_attrs,
6
- :remote_id, :local_attrs,
7
- :associations, :import_ids
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
- # TODO: implement destroy_missed, somehow pass @import_ids,
26
- # get all import records if nil
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
- # remove associations keys from local attributes
29
- @local_attrs.delete_if do |key, _|
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
- set_foreign_key
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
- def update_record
46
- local_record.update_attributes!(@local_attrs)
46
+ # TODO: This should be in Synchronisable::RecordWorker
47
+ set_belongs_to_parent_foreign_key
47
48
  end
48
49
 
49
- def create_record_pair
50
- record = @model.create!(@local_attrs)
51
- @import_record = Import.create!(
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 set_foreign_key
74
- return unless @parent
75
- name = foreign_key_name
76
- @local_attrs[name] = @parent.local_record.id if name
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 foreign_key_name
80
- return nil unless parent_has_model_as_reflection?
81
- parent_name = @parent.model.table_name.singularize
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 parent_has_model_as_reflection?
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
- [:has_one, :has_many].include?(reflection.macro)
92
+ %i(has_one has_many).include?(reflection.macro)
89
93
  end
90
94
  end
91
95
  end
@@ -169,7 +169,7 @@ module Synchronisable
169
169
  end
170
170
 
171
171
  def gateway_instance
172
- @gateway ||= gateway.try(:new)
172
+ @gateway_instance ||= gateway.try(:new)
173
173
  end
174
174
  end
175
175
  end
@@ -1,8 +1,8 @@
1
1
  module Synchronisable
2
2
  module VERSION
3
- MAJOR = 0
3
+ MAJOR = 1
4
4
  MINOR = 0
5
- PATCH = 9
5
+ PATCH = 0
6
6
  SUFFIX = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, SUFFIX].compact.join('.')
@@ -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
@@ -1,6 +1,6 @@
1
1
  class Stadium < ActiveRecord::Base
2
2
  # There is no synchronizer defined for this model,
3
- # so synchronization worker is gonna try
3
+ # so synchronization controller is gonna try
4
4
  # to use remote attributes as is to create a record
5
5
 
6
6
  synchronisable
@@ -1,5 +1,6 @@
1
1
  class Team < ActiveRecord::Base
2
2
  has_many :players
3
+ has_one :team_group_statistic
3
4
 
4
5
  synchronisable BreakConventionTeamSynchronizer
5
6
  end
@@ -0,0 +1,6 @@
1
+ class TeamGroupStatistic < ActiveRecord::Base
2
+ belongs_to :team
3
+ validates :team, presence: true
4
+
5
+ synchronisable
6
+ end
@@ -0,0 +1,5 @@
1
+ class TeamGroupStatisticSynchronizer < Synchronisable::Synchronizer
2
+ belongs_to :team
3
+
4
+ gateway TeamGroupStatisticGateway
5
+ 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
@@ -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: 20140507140039) do
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.9
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-05 00:00:00.000000000 Z
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/input_dispatcher.rb
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