synchronisable 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +2 -0
  5. data/.travis.yml +4 -0
  6. data/Gemfile +9 -0
  7. data/Guardfile +14 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +22 -0
  10. data/Rakefile +7 -0
  11. data/TODO.md +20 -0
  12. data/bin/autospec +16 -0
  13. data/bin/rake +16 -0
  14. data/bin/rspec +16 -0
  15. data/lib/generators/synchronizable/USAGE +2 -0
  16. data/lib/generators/synchronizable/install_generator.rb +24 -0
  17. data/lib/generators/synchronizable/templates/create_imports_migration.rb +21 -0
  18. data/lib/generators/synchronizable/templates/initializer.rb +14 -0
  19. data/lib/synchronizable.rb +92 -0
  20. data/lib/synchronizable/context.rb +25 -0
  21. data/lib/synchronizable/dsl/associations.rb +57 -0
  22. data/lib/synchronizable/dsl/associations/association.rb +59 -0
  23. data/lib/synchronizable/dsl/associations/has_many.rb +12 -0
  24. data/lib/synchronizable/dsl/associations/has_one.rb +13 -0
  25. data/lib/synchronizable/dsl/macro.rb +100 -0
  26. data/lib/synchronizable/dsl/macro/attribute.rb +41 -0
  27. data/lib/synchronizable/dsl/macro/expression.rb +51 -0
  28. data/lib/synchronizable/dsl/macro/method.rb +15 -0
  29. data/lib/synchronizable/error_handler.rb +50 -0
  30. data/lib/synchronizable/exceptions.rb +8 -0
  31. data/lib/synchronizable/locale/en.yml +17 -0
  32. data/lib/synchronizable/locale/ru.yml +17 -0
  33. data/lib/synchronizable/model.rb +54 -0
  34. data/lib/synchronizable/model/methods.rb +55 -0
  35. data/lib/synchronizable/models/import.rb +24 -0
  36. data/lib/synchronizable/source.rb +72 -0
  37. data/lib/synchronizable/synchronizer.rb +177 -0
  38. data/lib/synchronizable/synchronizers/synchronizer_default.rb +10 -0
  39. data/lib/synchronizable/version.rb +10 -0
  40. data/lib/synchronizable/worker.rb +191 -0
  41. data/spec/dummy/.gitignore +16 -0
  42. data/spec/dummy/Rakefile +6 -0
  43. data/spec/dummy/app/assets/images/.keep +0 -0
  44. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  45. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  46. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  47. data/spec/dummy/app/gateways/gateway_base.rb +19 -0
  48. data/spec/dummy/app/gateways/match_gateway.rb +15 -0
  49. data/spec/dummy/app/gateways/player_gateway.rb +26 -0
  50. data/spec/dummy/app/gateways/stage_gateway.rb +18 -0
  51. data/spec/dummy/app/gateways/team_gateway.rb +18 -0
  52. data/spec/dummy/app/gateways/tournament_gateway.rb +15 -0
  53. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  54. data/spec/dummy/app/mailers/.keep +0 -0
  55. data/spec/dummy/app/models/.keep +0 -0
  56. data/spec/dummy/app/models/match.rb +10 -0
  57. data/spec/dummy/app/models/match_player.rb +15 -0
  58. data/spec/dummy/app/models/player.rb +5 -0
  59. data/spec/dummy/app/models/stadium.rb +7 -0
  60. data/spec/dummy/app/models/stage.rb +6 -0
  61. data/spec/dummy/app/models/team.rb +5 -0
  62. data/spec/dummy/app/models/tournament.rb +7 -0
  63. data/spec/dummy/app/synchronizers/break_convention_team_synchronizer.rb +14 -0
  64. data/spec/dummy/app/synchronizers/match_synchronizer.rb +71 -0
  65. data/spec/dummy/app/synchronizers/player_synchronizer.rb +19 -0
  66. data/spec/dummy/app/synchronizers/stage_synchronizer.rb +20 -0
  67. data/spec/dummy/app/synchronizers/tournament_synchronizer.rb +20 -0
  68. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  69. data/spec/dummy/bin/bundle +3 -0
  70. data/spec/dummy/bin/rails +4 -0
  71. data/spec/dummy/bin/rake +4 -0
  72. data/spec/dummy/config.ru +4 -0
  73. data/spec/dummy/config/application.rb +26 -0
  74. data/spec/dummy/config/boot.rb +6 -0
  75. data/spec/dummy/config/database.yml +25 -0
  76. data/spec/dummy/config/environment.rb +5 -0
  77. data/spec/dummy/config/environments/development.rb +33 -0
  78. data/spec/dummy/config/environments/production.rb +80 -0
  79. data/spec/dummy/config/environments/test.rb +36 -0
  80. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  81. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/spec/dummy/config/initializers/inflections.rb +20 -0
  83. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  84. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  85. data/spec/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/dummy/config/initializers/synchronizable.rb +2 -0
  87. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  88. data/spec/dummy/config/locales/en.yml +23 -0
  89. data/spec/dummy/config/routes.rb +56 -0
  90. data/spec/dummy/db/migrate/20140422132431_create_teams.rb +11 -0
  91. data/spec/dummy/db/migrate/20140422132544_create_matches.rb +12 -0
  92. data/spec/dummy/db/migrate/20140422132708_create_players.rb +15 -0
  93. data/spec/dummy/db/migrate/20140422133122_create_match_players.rb +12 -0
  94. data/spec/dummy/db/migrate/20140422135244_create_imports.rb +21 -0
  95. data/spec/dummy/db/migrate/20140422140817_create_stadiums.rb +10 -0
  96. data/spec/dummy/db/migrate/20140507135800_create_tournaments.rb +13 -0
  97. data/spec/dummy/db/migrate/20140507135837_create_stages.rb +13 -0
  98. data/spec/dummy/db/migrate/20140507140039_add_stage_id_to_matches.rb +6 -0
  99. data/spec/dummy/db/schema.rb +103 -0
  100. data/spec/dummy/db/seeds.rb +7 -0
  101. data/spec/dummy/lib/assets/.keep +0 -0
  102. data/spec/dummy/lib/tasks/.keep +0 -0
  103. data/spec/dummy/log/.keep +0 -0
  104. data/spec/dummy/public/404.html +58 -0
  105. data/spec/dummy/public/422.html +58 -0
  106. data/spec/dummy/public/500.html +57 -0
  107. data/spec/dummy/public/favicon.ico +0 -0
  108. data/spec/dummy/public/robots.txt +5 -0
  109. data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
  110. data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
  111. data/spec/factories/import.rb +5 -0
  112. data/spec/factories/match.rb +9 -0
  113. data/spec/factories/match_player.rb +9 -0
  114. data/spec/factories/player.rb +10 -0
  115. data/spec/factories/remote/match.rb +21 -0
  116. data/spec/factories/remote/player.rb +18 -0
  117. data/spec/factories/remote/stage.rb +26 -0
  118. data/spec/factories/remote/team.rb +21 -0
  119. data/spec/factories/remote/tournament.rb +18 -0
  120. data/spec/factories/stadium.rb +7 -0
  121. data/spec/factories/team.rb +16 -0
  122. data/spec/models/match_spec.rb +41 -0
  123. data/spec/models/team_spec.rb +85 -0
  124. data/spec/spec_helper.rb +64 -0
  125. data/spec/synchronizable/dsl/macro_spec.rb +59 -0
  126. data/spec/synchronizable/models/import_spec.rb +10 -0
  127. data/spec/synchronizable/support/has_macro.rb +12 -0
  128. data/spec/synchronizable/support/has_macro_subclass.rb +9 -0
  129. data/spec/synchronizable/synchronizable_spec.rb +60 -0
  130. data/synchronizable.gemspec +39 -0
  131. metadata +506 -0
@@ -0,0 +1,59 @@
1
+ require 'synchronizable/dsl/macro'
2
+
3
+ module Synchronizable
4
+ module DSL
5
+ module Associations
6
+ # Association builder.
7
+ class Association
8
+ include Synchronizable::DSL::Macro
9
+
10
+ attribute :key_suffix, default: -> { raise NotImplementedError }
11
+
12
+ class << self
13
+ attr_accessor :valid_options
14
+
15
+ def create(synchronizer, name, options)
16
+ new(synchronizer, name).create(options)
17
+ end
18
+ end
19
+
20
+ self.valid_options = %i(key class_name required)
21
+
22
+ attr_reader :name, :model, :key, :required
23
+
24
+ def initialize(synchronizer, name)
25
+ @synchronizer, @name = synchronizer, name.to_sym
26
+ end
27
+
28
+ def create(options)
29
+ validate_options(options)
30
+
31
+ @key = options[:key]
32
+ @required = options[:required]
33
+
34
+ if options[:class_name].present?
35
+ @model = options[:class_name].constantize
36
+ end
37
+
38
+ set_defaults
39
+
40
+ @synchronizer.associations[@key] = self
41
+ end
42
+
43
+ protected
44
+
45
+ def set_defaults
46
+ @required ||= false
47
+ @model ||= @name.to_s.classify.constantize
48
+ @key = "#{@name}_#{self.class.key_suffix}" unless @key.present?
49
+ end
50
+
51
+ private
52
+
53
+ def validate_options(options)
54
+ options.assert_valid_keys(Association.valid_options)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ require 'synchronizable/dsl/associations/association'
2
+
3
+ module Synchronizable
4
+ module DSL
5
+ module Associations
6
+ # `has_many` association builder.
7
+ class HasMany < Association
8
+ key_suffix 'ids'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require 'synchronizable/dsl/associations/association'
2
+
3
+ module Synchronizable
4
+ module DSL
5
+ module Associations
6
+ # `has_one` association builder.
7
+ class HasOne < Association
8
+ key_suffix 'id'
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,100 @@
1
+ require 'synchronizable/dsl/macro/method'
2
+ require 'synchronizable/dsl/macro/attribute'
3
+
4
+ module Synchronizable
5
+ module DSL
6
+ # Allows to define DSL-like attributes and methods.
7
+ # to be used in target class/module.
8
+ #
9
+ # @example Common use cases
10
+ # class Foo
11
+ # include Synchronizable::DSL::Macro
12
+ #
13
+ # attribute :bar, default: 1
14
+ # attribute :blah, default: -> { bar * 2 }
15
+ # attribute :x, :y, :z
16
+ # attribute :f, -> {
17
+ # ...
18
+ # }
19
+ # attribute :w, converter: ->(h) { x.with_indifferent_access }
20
+ # method :sqr, default: ->(x) { x * x }
21
+ # method :sqrt, :sin, :cos, default: -> { raise NotImplementedError }
22
+ # end
23
+ #
24
+ # class Bar < Foo
25
+ # bar 4
26
+ # blah "blah blah"
27
+ #
28
+ # sqr do |x|
29
+ # x * x
30
+ # end
31
+ # end
32
+ #
33
+ # @api private
34
+ #
35
+ # @see Synchronizable::Synchronizer
36
+ # @see Synchronizable::DSL::Macro::Expression
37
+ # @see Synchronizable::DSL::Macro::Attribute
38
+ # @see Synchronizable::DSL::Macro::Method
39
+ module Macro
40
+ extend ActiveSupport::Concern
41
+
42
+ included do |base|
43
+ class_attribute :class_attributes
44
+ self.class_attributes = {
45
+ base => {}
46
+ }
47
+ end
48
+
49
+ module ClassMethods
50
+ def inherited(subclass)
51
+ class_attributes[subclass] = class_attributes[self].deep_dup
52
+ end
53
+
54
+ # Defines a new attribute.
55
+ #
56
+ # @overload attribute(*attrs, options)
57
+ # @param attrs [Array] attributes
58
+ # @param options [Hash]
59
+ # @option options :default default value,
60
+ # procs & lambdas are lazily evaluated
61
+ # @option options [Lambda] :converter method that
62
+ # will be lazily applied to the source value in order to convert it
63
+ # to the desired type
64
+ # @overload attribute(*attrs)
65
+ #
66
+ # @api private
67
+ def attribute(*args)
68
+ define_expressions(Attribute, args)
69
+ end
70
+
71
+ # Defines a new method.
72
+ #
73
+ # @overload method(*attrs, options)
74
+ # @param attrs [Array] attributes
75
+ # @param options [Hash]
76
+ # @option options :default default value,
77
+ # procs & lambdas are lazily evaluated
78
+ # @overload method(*attrs)
79
+ #
80
+ # @api private
81
+ def method(*args)
82
+ define_expressions(Method, args)
83
+ end
84
+
85
+ private
86
+
87
+ def define_expressions(klass, args)
88
+ options = args.extract_options!
89
+ args.each do |attr|
90
+ name = attr.to_sym
91
+ class_attributes[self][name] = klass.new(options)
92
+ define_singleton_method(name) do |*params, &block|
93
+ class_attributes[self][name].handle(params, &block)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,41 @@
1
+ require 'synchronizable/dsl/macro/expression'
2
+
3
+ module Synchronizable
4
+ module DSL
5
+ module Macro
6
+ # Expression for an attribute definition.
7
+ #
8
+ # @api private
9
+ #
10
+ # @see Synchronizable::DSL::Macro
11
+ # @see Synchronizable::DSL::Expression
12
+ class Attribute < Expression
13
+ def initialize(options)
14
+ @converter = options[:converter]
15
+ super
16
+ end
17
+
18
+ def source
19
+ transform(@source)
20
+ end
21
+
22
+ def default
23
+ transform(@default)
24
+ end
25
+
26
+ protected
27
+
28
+ def transform(arg)
29
+ convert(super)
30
+ end
31
+
32
+ private
33
+
34
+ def convert(arg)
35
+ @converter.try(:call, arg) || arg
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,51 @@
1
+ module Synchronizable
2
+ module DSL
3
+ module Macro
4
+ # Serves as storage and provides lazy evaluation for expression.
5
+ # You can override attributes in subclasses,
6
+ # for example you can change the default value.
7
+ #
8
+ # @api private
9
+ #
10
+ # @see Synchronizable::DSL::Macro
11
+ # @see Synchronizable::DSL::Attribute
12
+ # @see Synchronizable::DSL::Method
13
+ class Expression
14
+ attr_reader :source, :default
15
+
16
+ def initialize(options)
17
+ @default = options[:default]
18
+ end
19
+
20
+ def handle(args, &block)
21
+ has_value = block || args.present?
22
+ has_value ? save(args, block) : value
23
+ end
24
+
25
+ protected
26
+
27
+ def transform(arg)
28
+ evaluate(arg)
29
+ end
30
+
31
+ private
32
+
33
+ def save(args, block)
34
+ if args.present?
35
+ @source = args.count > 1 ? args : args.first
36
+ elsif block
37
+ @source = block
38
+ end
39
+ end
40
+
41
+ def value
42
+ source || default
43
+ end
44
+
45
+ def evaluate(arg)
46
+ arg.try(:call) || arg
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ require 'synchronizable/dsl/macro/expression'
2
+
3
+ module Synchronizable
4
+ module DSL
5
+ module Macro
6
+ # Expression for a method definition.
7
+ #
8
+ # @api private
9
+ #
10
+ # @see Synchronizable::DSL::Macro
11
+ # @see Synchronizable::DSL::Expression
12
+ class Method < Expression; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module Synchronizable
2
+ # Helper class for synchronization errors handling.
3
+ #
4
+ # @see Synchronizable::Context
5
+ #
6
+ # @api private
7
+ class ErrorHandler
8
+ # @param logger [Logger] logger to used to log errors
9
+ # @param context [Synchronizable::Context] synchronization context
10
+ def initialize(logger, context)
11
+ @logger, @context = logger, context
12
+ end
13
+
14
+ # Wraps the given block in transaction.
15
+ # Rescued exceptions are written to log and saved to errors array.
16
+ #
17
+ # @param source [Synchronizable::Source] synchronization source
18
+ #
19
+ # @return [Boolean] `true` if syncronization was completed
20
+ # without errors, `false` otherwise
21
+ def handle(source)
22
+ ActiveRecord::Base.transaction do
23
+ yield
24
+ return true
25
+ end
26
+ rescue Exception => e
27
+ @context.errors << e
28
+ log(e, source)
29
+ return false
30
+ end
31
+
32
+ private
33
+
34
+ def log(e, source)
35
+ msg = error_message(e, source)
36
+ @logger.error msg
37
+ end
38
+
39
+ def error_message(e, source)
40
+ I18n.t('errors.import_error',
41
+ :model => @context.model.to_s,
42
+ :error => e.message,
43
+ :remote_attrs => source.remote_attrs,
44
+ :local_attrs => source.local_attrs,
45
+ :import_record => source.import_record.inspect,
46
+ :local_record => source.local_record.inspect
47
+ )
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,8 @@
1
+ module Synchronizable
2
+ # Error is thrown when remote id isn't supplied
3
+ # with remote attibutes hash.
4
+ class MissedRemoteIdError < StandardError; end
5
+ # Error is thrown when required associations isn't supplied
6
+ # with remote attributes hash.
7
+ class MissedAssociationsError < StandardError; end
8
+ end
@@ -0,0 +1,17 @@
1
+ ---
2
+ en:
3
+ errors:
4
+ missed_remote_id: Couldn't find remote id '%{remote_id}' in given data hash
5
+ missed_associations: Couldn't find associations %{keys} in %{attrs}
6
+ import_error: |
7
+ %{model} import error: %{error}.
8
+ remote attrs: %{remote_attrs},
9
+ local attrs: %{local_attrs},
10
+ import record: %{import_record},
11
+ local record: %{local_record}
12
+ gateway_method_missing: Method %{method} is not implemented for gateway
13
+ messages:
14
+ result: |
15
+ %{model} synchronization, parent: %{parent}.
16
+ Record count before / after: %{before} / %{after},
17
+ deleted record count: %{deleted}, error count: %{errors}
@@ -0,0 +1,17 @@
1
+ ---
2
+ ru:
3
+ errors:
4
+ missed_remote_id: В хэше не указан remote id '%{remote_id}'
5
+ missed_associations: В хэше %{attrs} не указаны ассоциации %{keys}
6
+ import_error: >
7
+ Ошибка при импорте %{model}: %{error},
8
+ remote attrs: %{remote_attrs},
9
+ local attrs: %{local_attrs},
10
+ import record: %{import_record},
11
+ local record: %{local_record}
12
+ gateway_method_missing: Не реализован метод '%{method}' для Gateway
13
+ messages:
14
+ result: >
15
+ Синхронизация %{model}, родительская запись: %{parent}.
16
+ Количество записей до: %{before}, количество записей после: %{after},
17
+ количество удаленных записей: %{deleted}, количество ошибок: %{errors}
@@ -0,0 +1,54 @@
1
+ require 'synchronizable/model/methods'
2
+ require 'synchronizable/synchronizers/synchronizer_default'
3
+
4
+ module Synchronizable
5
+ module Model
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ SYNCHRONIZER_SUFFIX = 'Synchronizer'
10
+
11
+ # Declare this on your model class to make it synchronizable.
12
+ # After that you can call {Synchronizable::Model::Methods#sync} to
13
+ # start model synchronization.
14
+ #
15
+ # @overload synchronizable(klass, options)
16
+ # @param klass [Class] synchronizer class to be used
17
+ # @param options [Hash] describes behavior of synchronizable model
18
+ # @option options [Class] :synchronizer class that provides
19
+ # synchronization configuration
20
+ # @overload synchronizable(options)
21
+ # @overload synchronizable
22
+ #
23
+ # @see Synchronizable::Synchronizer
24
+ # @see Synchronizable::Model::Methods
25
+ #
26
+ # @example Common usage
27
+ # class FooModel < ActiveRecord::Base
28
+ # synchronizable BarSynchronizer
29
+ # end
30
+ def synchronizable(*args)
31
+ extend Synchronizable::Model::Methods
32
+
33
+ class_attribute :synchronizer
34
+ has_one :import, as: :synchronizable
35
+
36
+ set_defaults(args)
37
+ end
38
+
39
+ private
40
+
41
+ def set_defaults(args)
42
+ options = args.extract_options!
43
+
44
+ self.synchronizer = args.first ||
45
+ options[:synchronizer] || default_synchronizer
46
+ end
47
+
48
+ def default_synchronizer
49
+ const_name = "#{self.name.demodulize}#{SYNCHRONIZER_SUFFIX}"
50
+ const_name.safe_constantize || SynchronizerDefault
51
+ end
52
+ end
53
+ end
54
+ end