table_sync 2.3.0 → 4.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 (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