table_sync 2.3.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +21 -0
  3. data/CHANGELOG.md +40 -0
  4. data/Gemfile.lock +82 -77
  5. data/README.md +4 -2
  6. data/docs/message_protocol.md +24 -0
  7. data/docs/notifications.md +45 -0
  8. data/docs/publishing.md +147 -0
  9. data/docs/receiving.md +341 -0
  10. data/lib/table_sync.rb +16 -31
  11. data/lib/table_sync/errors.rb +39 -23
  12. data/lib/table_sync/publishing.rb +11 -0
  13. data/lib/table_sync/{base_publisher.rb → publishing/base_publisher.rb} +1 -1
  14. data/lib/table_sync/{batch_publisher.rb → publishing/batch_publisher.rb} +4 -4
  15. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/active_record.rb +3 -7
  16. data/lib/table_sync/{orm_adapter → publishing/orm_adapter}/sequel.rb +2 -6
  17. data/lib/table_sync/{publisher.rb → publishing/publisher.rb} +4 -4
  18. data/lib/table_sync/receiving.rb +14 -0
  19. data/lib/table_sync/receiving/config.rb +218 -0
  20. data/lib/table_sync/receiving/config_decorator.rb +27 -0
  21. data/lib/table_sync/receiving/dsl.rb +28 -0
  22. data/lib/table_sync/receiving/handler.rb +131 -0
  23. data/lib/table_sync/{model → receiving/model}/active_record.rb +36 -22
  24. data/lib/table_sync/{model → receiving/model}/sequel.rb +13 -8
  25. data/lib/table_sync/utils.rb +9 -0
  26. data/lib/table_sync/utils/interface_checker.rb +97 -0
  27. data/lib/table_sync/utils/proc_array.rb +17 -0
  28. data/lib/table_sync/utils/proc_keywords_resolver.rb +46 -0
  29. data/lib/table_sync/version.rb +1 -1
  30. data/table_sync.gemspec +2 -1
  31. metadata +42 -30
  32. data/docs/development.md +0 -43
  33. data/docs/synopsis.md +0 -336
  34. data/lib/table_sync/config.rb +0 -105
  35. data/lib/table_sync/config/callback_registry.rb +0 -53
  36. data/lib/table_sync/config_decorator.rb +0 -38
  37. data/lib/table_sync/dsl.rb +0 -25
  38. data/lib/table_sync/event_actions.rb +0 -96
  39. data/lib/table_sync/event_actions/data_wrapper.rb +0 -7
  40. data/lib/table_sync/event_actions/data_wrapper/base.rb +0 -23
  41. data/lib/table_sync/event_actions/data_wrapper/destroy.rb +0 -19
  42. data/lib/table_sync/event_actions/data_wrapper/update.rb +0 -21
  43. data/lib/table_sync/plugins.rb +0 -72
  44. data/lib/table_sync/plugins/abstract.rb +0 -55
  45. data/lib/table_sync/plugins/access_mixin.rb +0 -49
  46. data/lib/table_sync/plugins/registry.rb +0 -153
  47. data/lib/table_sync/receiving_handler.rb +0 -76
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync
4
+ module Publishing
5
+ require_relative "publishing/base_publisher"
6
+ require_relative "publishing/publisher"
7
+ require_relative "publishing/batch_publisher"
8
+ require_relative "publishing/orm_adapter/active_record"
9
+ require_relative "publishing/orm_adapter/sequel"
10
+ end
11
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TableSync::BasePublisher
3
+ class TableSync::Publishing::BasePublisher
4
4
  include Memery
5
5
 
6
6
  BASE_SAFE_JSON_TYPES = [NilClass, String, TrueClass, FalseClass, Numeric, Symbol].freeze
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TableSync::BatchPublisher < TableSync::BasePublisher
3
+ class TableSync::Publishing::BatchPublisher < TableSync::Publishing::BasePublisher
4
4
  def initialize(object_class, original_attributes_array, **options)
5
5
  @original_attributes_array = original_attributes_array.map do |hash|
6
6
  filter_safe_for_serialization(hash.deep_symbolize_keys)
@@ -22,7 +22,7 @@ class TableSync::BatchPublisher < TableSync::BasePublisher
22
22
  return unless need_publish?
23
23
  Rabbit.publish(params)
24
24
 
25
- model_naming = TableSync.orm.model_naming(object_class)
25
+ model_naming = TableSync.publishing_adapter.model_naming(object_class)
26
26
  TableSync::Instrument.notify table: model_naming.table, schema: model_naming.schema,
27
27
  event: event,
28
28
  count: publishing_data[:attributes].size, direction: :publish
@@ -41,7 +41,7 @@ class TableSync::BatchPublisher < TableSync::BasePublisher
41
41
  end
42
42
 
43
43
  memoize def objects
44
- needles.map { |needle| TableSync.orm.find(object_class, needle) }.compact
44
+ needles.map { |needle| TableSync.publishing_adapter.find(object_class, needle) }.compact
45
45
  end
46
46
 
47
47
  def job_callable
@@ -90,7 +90,7 @@ class TableSync::BatchPublisher < TableSync::BasePublisher
90
90
  if attributes_for_sync_defined?
91
91
  object.attributes_for_sync
92
92
  else
93
- TableSync.orm.attributes(object)
93
+ TableSync.publishing_adapter.attributes(object)
94
94
  end
95
95
  end
96
96
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TableSync::ORMAdapter
3
+ module TableSync::Publishing::ORMAdapter
4
4
  module ActiveRecord
5
5
  module_function
6
6
 
7
- def model
8
- ::TableSync::Model::ActiveRecord
9
- end
10
-
11
7
  def model_naming(object)
12
8
  ::TableSync::NamingResolver::ActiveRecord.new(table_name: object.table_name)
13
9
  end
@@ -26,8 +22,8 @@ module TableSync::ORMAdapter
26
22
  klass.instance_exec do
27
23
  { create: :created, update: :updated, destroy: :destroyed }.each do |event, state|
28
24
  after_commit(on: event, **opts) do
29
- TableSync::Publisher.new(self.class.name, attributes,
30
- state: state, debounce_time: debounce_time).publish
25
+ TableSync::Publishing::Publisher.new(self.class.name, attributes,
26
+ state: state, debounce_time: debounce_time).publish
31
27
  end
32
28
  end
33
29
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TableSync::ORMAdapter
3
+ module TableSync::Publishing::ORMAdapter
4
4
  module Sequel
5
5
  module_function
6
6
 
7
- def model
8
- ::TableSync::Model::Sequel
9
- end
10
-
11
7
  def model_naming(object)
12
8
  ::TableSync::NamingResolver::Sequel.new(table_name: object.table_name, db: object.db)
13
9
  end
@@ -44,7 +40,7 @@ module TableSync::ORMAdapter
44
40
  klass.send(:define_method, :"after_#{event}") do
45
41
  if instance_eval(&if_predicate) && !instance_eval(&unless_predicate)
46
42
  db.after_commit do
47
- TableSync::Publisher.new(
43
+ TableSync::Publishing::Publisher.new(
48
44
  self.class.name,
49
45
  values,
50
46
  state: state,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class TableSync::Publisher < TableSync::BasePublisher
3
+ class TableSync::Publishing::Publisher < TableSync::Publishing::BasePublisher
4
4
  DEBOUNCE_TIME = 1.minute
5
5
 
6
6
  # 'original_attributes' are not published, they are used to resolve the routing key
@@ -34,7 +34,7 @@ class TableSync::Publisher < TableSync::BasePublisher
34
34
  return if !object && !destroyed?
35
35
 
36
36
  Rabbit.publish(params)
37
- model_naming = TableSync.orm.model_naming(object_class)
37
+ model_naming = TableSync.publishing_adapter.model_naming(object_class)
38
38
  TableSync::Instrument.notify table: model_naming.table, schema: model_naming.schema,
39
39
  event: event, direction: :publish
40
40
  end
@@ -95,12 +95,12 @@ class TableSync::Publisher < TableSync::BasePublisher
95
95
  elsif attributes_for_sync_defined?
96
96
  object.attributes_for_sync
97
97
  else
98
- TableSync.orm.attributes(object)
98
+ TableSync.publishing_adapter.attributes(object)
99
99
  end
100
100
  end
101
101
 
102
102
  memoize def object
103
- TableSync.orm.find(object_class, needle)
103
+ TableSync.publishing_adapter.find(object_class, needle)
104
104
  end
105
105
 
106
106
  def event
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync
4
+ module Receiving
5
+ AVAILABLE_EVENTS = [:update, :destroy].freeze
6
+
7
+ require_relative "receiving/config"
8
+ require_relative "receiving/config_decorator"
9
+ require_relative "receiving/dsl"
10
+ require_relative "receiving/handler"
11
+ require_relative "receiving/model/active_record"
12
+ require_relative "receiving/model/sequel"
13
+ end
14
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving
4
+ class Config
5
+ attr_reader :model, :events
6
+
7
+ def initialize(model:, events: AVAILABLE_EVENTS)
8
+ @model = model
9
+
10
+ @events = [events].flatten.map(&:to_sym)
11
+
12
+ unless @events.all? { |event| AVAILABLE_EVENTS.include?(event) }
13
+ raise TableSync::UndefinedEvent.new(events)
14
+ end
15
+
16
+ self.class.default_values_for_options.each do |ivar, default_value_generator|
17
+ instance_variable_set(ivar, default_value_generator.call(self))
18
+ end
19
+ end
20
+
21
+ class << self
22
+ attr_reader :default_values_for_options
23
+
24
+ # In a configs this options are requested as they are
25
+ # config.option - get value
26
+ # config.option(args) - set static value
27
+ # config.option { ... } - set proc as value
28
+ #
29
+ # In `Receiving::Handler` or `Receiving::EventActions` this options are requested
30
+ # through `Receiving::ConfigDecorator#method_missing` which always executes `config.option`
31
+
32
+ def add_option(name, value_setter_wrapper:, value_as_proc_setter_wrapper:, default:)
33
+ ivar = "@#{name}".to_sym
34
+
35
+ @default_values_for_options ||= {}
36
+ @default_values_for_options[ivar] = default
37
+
38
+ define_method(name) do |*value, &value_as_proc|
39
+ return instance_variable_get(ivar) if value.empty? && value_as_proc.nil?
40
+
41
+ value = value.first if value.size == 1
42
+
43
+ if value_as_proc.present?
44
+ new_value = TableSync::Utils.proc_keywords_resolver(&value_as_proc)
45
+ setter_wrapper = value_as_proc_setter_wrapper
46
+ else
47
+ new_value = value
48
+ setter_wrapper = value_setter_wrapper
49
+ end
50
+
51
+ old_value = instance_variable_get(ivar)
52
+ result_value = instance_exec(name, new_value, old_value, &setter_wrapper)
53
+ instance_variable_set(ivar, result_value)
54
+ end
55
+ end
56
+ end
57
+
58
+ def allow_event?(name)
59
+ events.include?(name)
60
+ end
61
+ end
62
+ end
63
+
64
+ # Helpers:
65
+
66
+ column_key = proc do |option_name, new_value|
67
+ unless model.columns.include?(new_value)
68
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
69
+ end
70
+ new_value
71
+ end
72
+
73
+ exactly_symbol = proc do |option_name, new_value|
74
+ unless new_value.is_a? Symbol
75
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
76
+ end
77
+ new_value
78
+ end
79
+
80
+ to_array = proc do |block|
81
+ proc do |option_name, new_value|
82
+ new_value = [new_value].flatten
83
+ new_value.map { |value| instance_exec(option_name, value, &block) }
84
+ end
85
+ end
86
+
87
+ exactly_not_array = proc do |block|
88
+ proc do |option_name, new_value|
89
+ if new_value.is_a? Array
90
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
91
+ end
92
+ instance_exec(option_name, new_value, &block)
93
+ end
94
+ end
95
+
96
+ exactly_hash = proc do |block_for_keys, block_for_values|
97
+ proc do |option_name, new_value|
98
+ unless new_value.is_a? Hash
99
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
100
+ end
101
+
102
+ new_value.to_a.map do |key, value|
103
+ [
104
+ instance_exec("#{option_name} keys", key, &block_for_keys),
105
+ instance_exec("#{option_name} values", value, &block_for_values),
106
+ ]
107
+ end.to_h
108
+ end
109
+ end
110
+
111
+ any_value = proc do |_option_name, new_value|
112
+ new_value
113
+ end
114
+
115
+ raise_error = proc do |option_name, new_value|
116
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
117
+ end
118
+
119
+ exactly_boolean = proc do |option_name, new_value|
120
+ unless new_value.is_a?(TrueClass) || new_value.is_a?(FalseClass)
121
+ raise TableSync::WrongOptionValue.new(model, option_name, new_value)
122
+ end
123
+ new_value
124
+ end
125
+
126
+ allow_false = proc do |block|
127
+ proc do |option_name, new_value|
128
+ next false if new_value.is_a? FalseClass
129
+ instance_exec(option_name, new_value, &block)
130
+ end
131
+ end
132
+
133
+ proc_result = proc do |block|
134
+ proc do |option_name, new_value|
135
+ proc do |*args, &b|
136
+ result = new_value.call(*args, &b)
137
+ instance_exec(option_name, result, &block)
138
+ end
139
+ end
140
+ end
141
+
142
+ accumulate_procs = proc do |block|
143
+ proc do |option_name, new_value, old_value|
144
+ old_value.push(&instance_exec(option_name, new_value, &block))
145
+ end
146
+ end
147
+
148
+ # Options:
149
+
150
+ # rubocop:disable Layout/ArgumentAlignment
151
+
152
+ TableSync::Receiving::Config.add_option :only,
153
+ value_setter_wrapper: to_array[column_key],
154
+ value_as_proc_setter_wrapper: proc_result[to_array[column_key]],
155
+ default: proc { |config| config.model.columns }
156
+
157
+ TableSync::Receiving::Config.add_option :target_keys,
158
+ value_setter_wrapper: to_array[column_key],
159
+ value_as_proc_setter_wrapper: proc_result[to_array[column_key]],
160
+ default: proc { |config| Array.wrap(config.model.primary_keys) }
161
+
162
+ TableSync::Receiving::Config.add_option :rest_key,
163
+ value_setter_wrapper: exactly_not_array[allow_false[column_key]],
164
+ value_as_proc_setter_wrapper: proc_result[exactly_not_array[allow_false[column_key]]],
165
+ default: proc { :rest }
166
+
167
+ TableSync::Receiving::Config.add_option :version_key,
168
+ value_setter_wrapper: exactly_not_array[column_key],
169
+ value_as_proc_setter_wrapper: proc_result[exactly_not_array[column_key]],
170
+ default: proc { :version }
171
+
172
+ TableSync::Receiving::Config.add_option :except,
173
+ value_setter_wrapper: to_array[exactly_symbol],
174
+ value_as_proc_setter_wrapper: proc_result[to_array[exactly_symbol]],
175
+ default: proc { [] }
176
+
177
+ TableSync::Receiving::Config.add_option :mapping_overrides,
178
+ value_setter_wrapper: exactly_hash[exactly_symbol, exactly_symbol],
179
+ value_as_proc_setter_wrapper: proc_result[exactly_hash[exactly_symbol, exactly_symbol]],
180
+ default: proc { {} }
181
+
182
+ TableSync::Receiving::Config.add_option :additional_data,
183
+ value_setter_wrapper: exactly_hash[exactly_symbol, any_value],
184
+ value_as_proc_setter_wrapper: proc_result[exactly_hash[exactly_symbol, any_value]],
185
+ default: proc { {} }
186
+
187
+ TableSync::Receiving::Config.add_option :default_values,
188
+ value_setter_wrapper: exactly_hash[exactly_symbol, any_value],
189
+ value_as_proc_setter_wrapper: proc_result[exactly_hash[exactly_symbol, any_value]],
190
+ default: proc { {} }
191
+
192
+ TableSync::Receiving::Config.add_option :skip,
193
+ value_setter_wrapper: exactly_boolean,
194
+ value_as_proc_setter_wrapper: proc_result[exactly_boolean],
195
+ default: proc { false }
196
+
197
+ TableSync::Receiving::Config.add_option :wrap_receiving,
198
+ value_setter_wrapper: raise_error,
199
+ value_as_proc_setter_wrapper: any_value,
200
+ default: proc { proc { |&block| block.call } }
201
+
202
+ %i[
203
+ before_update
204
+ after_commit_on_update
205
+ before_destroy
206
+ after_commit_on_destroy
207
+ ].each do |option|
208
+ TableSync::Receiving::Config.add_option option,
209
+ value_setter_wrapper: raise_error,
210
+ value_as_proc_setter_wrapper: accumulate_procs[any_value],
211
+ default: (proc do |_config|
212
+ TableSync::Utils::ProcArray.new do |array, args, &block|
213
+ array.map { |pr| pr.call(*args, &block) }
214
+ end
215
+ end)
216
+ end
217
+
218
+ # rubocop:enable Layout/ArgumentAlignment
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving
4
+ class ConfigDecorator
5
+ extend Forwardable
6
+
7
+ def_delegators :@config, :allow_event?
8
+ # rubocop:disable Metrics/ParameterLists
9
+ def initialize(config:, event:, model:, version:, project_id:, raw_data:)
10
+ @config = config
11
+
12
+ @default_params = {
13
+ event: event,
14
+ model: model,
15
+ version: version,
16
+ project_id: project_id,
17
+ raw_data: raw_data,
18
+ }
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def method_missing(name, **additional_params, &block)
23
+ value = @config.send(name)
24
+ value.is_a?(Proc) ? value.call(@default_params.merge(additional_params), &block) : value
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TableSync::Receiving
4
+ module DSL
5
+ def inherited(klass)
6
+ klass.instance_variable_set(:@configs, configs.deep_dup)
7
+ super
8
+ end
9
+
10
+ def configs
11
+ @configs ||= Hash.new { |hash, key| hash[key] = [] }
12
+ end
13
+
14
+ def receive(source, to_table: nil, to_model: nil, events: [:update, :destroy], &block)
15
+ model = to_table ? TableSync.receiving_model.new(to_table) : to_model
16
+
17
+ TableSync::Utils::InterfaceChecker.new(model).implements(:receiving_model)
18
+
19
+ config = ::TableSync::Receiving::Config.new(model: model, events: events)
20
+
21
+ config.instance_exec(&block) if block
22
+
23
+ configs[source.to_s] << config
24
+
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TableSync::Receiving::Handler < Rabbit::EventHandler
4
+ extend TableSync::Receiving::DSL
5
+
6
+ # Rabbit::EventHandler uses Tainbox and performs `handler.new(message).call`
7
+ attribute :event
8
+ attribute :model
9
+ attribute :version
10
+
11
+ def call
12
+ configs.each do |config|
13
+ next unless config.allow_event?(event)
14
+
15
+ data = processed_data(config)
16
+
17
+ next if data.empty?
18
+
19
+ version_key = config.version_key(data: data)
20
+ data.each { |row| row[version_key] = version }
21
+
22
+ target_keys = config.target_keys(data: data)
23
+
24
+ validate_data(data, target_keys: target_keys)
25
+
26
+ params = { data: data, target_keys: target_keys, version_key: version_key }
27
+
28
+ if event == :update
29
+ params[:default_values] = config.default_values(data: data)
30
+ end
31
+
32
+ config.wrap_receiving(**params) do
33
+ perform(config, params)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # redefine setter from Rabbit::EventHandler
41
+ def data=(data)
42
+ super(Array.wrap(data[:attributes]))
43
+ end
44
+
45
+ def event=(name)
46
+ name = name.to_sym
47
+ raise TableSync::UndefinedEvent.new(event) unless %i[update destroy].include?(name)
48
+ super(name)
49
+ end
50
+
51
+ def model=(name)
52
+ super(name.to_s)
53
+ end
54
+
55
+ def configs
56
+ @configs ||= self.class.configs[model]&.map do |config|
57
+ ::TableSync::Receiving::ConfigDecorator.new(
58
+ config: config,
59
+ # next parameters will be send to each proc-options from config
60
+ event: event,
61
+ model: model,
62
+ version: version,
63
+ project_id: project_id,
64
+ raw_data: data,
65
+ )
66
+ end
67
+ end
68
+
69
+ def processed_data(config)
70
+ data.map do |row|
71
+ next if config.skip(row: row)
72
+
73
+ row = row.dup
74
+
75
+ config.mapping_overrides(row: row).each do |before, after|
76
+ row[after] = row.delete(before)
77
+ end
78
+
79
+ config.except(row: row).each(&row.method(:delete))
80
+
81
+ row.merge!(config.additional_data(row: row))
82
+
83
+ only = config.only(row: row)
84
+ row, rest = row.partition { |key, _| key.in?(only) }.map(&:to_h)
85
+
86
+ rest_key = config.rest_key(row: row, rest: rest)
87
+ (row[rest_key] ||= {}).merge!(rest) if rest_key
88
+
89
+ row
90
+ end.compact
91
+ end
92
+
93
+ def validate_data(data, target_keys:)
94
+ data.each do |row|
95
+ next if target_keys.all?(&row.keys.method(:include?))
96
+ raise TableSync::DataError.new(
97
+ data, target_keys, "Some target keys not found in received attributes!"
98
+ )
99
+ end
100
+
101
+ if data.uniq { |row| row.slice(*target_keys) }.size != data.size
102
+ raise TableSync::DataError.new(
103
+ data, target_keys, "Duplicate rows found!"
104
+ )
105
+ end
106
+ end
107
+
108
+ def perform(config, params)
109
+ model = config.model
110
+
111
+ model.transaction do
112
+ if event == :update
113
+ config.before_update(**params)
114
+
115
+ results = model.upsert(**params)
116
+
117
+ model.after_commit do
118
+ config.after_commit_on_update(**params.merge(results: results))
119
+ end
120
+ else
121
+ config.before_destroy(**params)
122
+
123
+ results = model.destroy(**params)
124
+
125
+ model.after_commit do
126
+ config.after_commit_on_destroy(**params.merge(results: results))
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end