acts_as_table 0.0.1

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 (34) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +30 -0
  4. data/README.md +256 -0
  5. data/Rakefile +13 -0
  6. data/app/models/acts_as_table/belongs_to.rb +81 -0
  7. data/app/models/acts_as_table/column_model.rb +89 -0
  8. data/app/models/acts_as_table/foreign_key.rb +299 -0
  9. data/app/models/acts_as_table/foreign_key_map.rb +121 -0
  10. data/app/models/acts_as_table/has_many.rb +90 -0
  11. data/app/models/acts_as_table/has_many_target.rb +40 -0
  12. data/app/models/acts_as_table/lense.rb +131 -0
  13. data/app/models/acts_as_table/primary_key.rb +108 -0
  14. data/app/models/acts_as_table/record.rb +109 -0
  15. data/app/models/acts_as_table/record_error.rb +50 -0
  16. data/app/models/acts_as_table/record_model.rb +194 -0
  17. data/app/models/acts_as_table/row_model.rb +285 -0
  18. data/app/models/acts_as_table/table.rb +41 -0
  19. data/app/models/acts_as_table/value.rb +80 -0
  20. data/app/models/concerns/acts_as_table/record_model_class_methods.rb +554 -0
  21. data/app/models/concerns/acts_as_table/value_provider.rb +223 -0
  22. data/app/models/concerns/acts_as_table/value_provider_association_methods.rb +57 -0
  23. data/config/locales/en.yml +8 -0
  24. data/db/migrate/1_acts_as_table_migration.rb +186 -0
  25. data/lib/acts_as_table.rb +288 -0
  26. data/lib/acts_as_table/adapter.rb +81 -0
  27. data/lib/acts_as_table/engine.rb +14 -0
  28. data/lib/acts_as_table/headers.rb +196 -0
  29. data/lib/acts_as_table/mapper.rb +464 -0
  30. data/lib/acts_as_table/path.rb +157 -0
  31. data/lib/acts_as_table/reader.rb +149 -0
  32. data/lib/acts_as_table/version.rb +4 -0
  33. data/lib/acts_as_table/writer.rb +116 -0
  34. metadata +181 -0
@@ -0,0 +1,285 @@
1
+ module ActsAsTable
2
+ # ActsAsTable row model (value provider).
3
+ #
4
+ # @!attribute [rw] name
5
+ # Returns the name of this ActsAsTable row model.
6
+ #
7
+ # @return [String]
8
+ class RowModel < ::ActiveRecord::Base
9
+ # @!parse
10
+ # include ActsAsTable::ValueProvider
11
+ # include ActsAsTable::ValueProvider::InstanceMethods
12
+ # include ActsAsTable::ValueProviderAssociationMethods
13
+
14
+ self.table_name = ActsAsTable.row_models_table
15
+
16
+ acts_as_table_value_provider
17
+
18
+ # Returns the root ActsAsTable record model for this ActsAsTable row model.
19
+ belongs_to :root_record_model, **{
20
+ class_name: 'ActsAsTable::RecordModel',
21
+ inverse_of: :row_models_as_root,
22
+ required: true,
23
+ }
24
+
25
+ # Returns the ActsAsTable column models for this ActsAsTable row model.
26
+ has_many :column_models, -> { order(position: :asc) }, **{
27
+ autosave: true,
28
+ class_name: 'ActsAsTable::ColumnModel',
29
+ dependent: :destroy,
30
+ foreign_key: 'row_model_id',
31
+ inverse_of: :row_model,
32
+ validate: true,
33
+ }
34
+
35
+ # Returns the ActsAsTable singular macro associations for this ActsAsTable row model.
36
+ has_many :belongs_tos, **{
37
+ autosave: true,
38
+ class_name: 'ActsAsTable::BelongsTo',
39
+ dependent: :destroy,
40
+ foreign_key: 'row_model_id',
41
+ inverse_of: :row_model,
42
+ validate: true,
43
+ }
44
+
45
+ # Returns the ActsAsTable collection macro associations for this ActsAsTable row model.
46
+ has_many :has_manies, **{
47
+ autosave: true,
48
+ class_name: 'ActsAsTable::HasMany',
49
+ dependent: :destroy,
50
+ foreign_key: 'row_model_id',
51
+ inverse_of: :row_model,
52
+ validate: true,
53
+ }
54
+
55
+ # Returns the ActsAsTable record models for this ActsAsTable row model.
56
+ has_many :record_models, **{
57
+ autosave: true,
58
+ class_name: 'ActsAsTable::RecordModel',
59
+ dependent: :destroy,
60
+ foreign_key: 'row_model_id',
61
+ inverse_of: :row_model,
62
+ validate: true,
63
+ }
64
+
65
+ # Returns the ActsAsTable tables that have been provided by this ActsAsTable row model.
66
+ has_many :tables, **{
67
+ class_name: 'ActsAsTable::Table',
68
+ dependent: :restrict_with_exception,
69
+ foreign_key: 'row_model_id',
70
+ inverse_of: :row_model,
71
+ }
72
+
73
+ validates :name, **{
74
+ presence: true,
75
+ }
76
+
77
+ validate :record_models_includes_root_record_model, **{
78
+ if: ::Proc.new { |row_model| row_model.root_record_model.present? },
79
+ }
80
+
81
+ # Draw an ActsAsTable row model.
82
+ #
83
+ # @yieldparam [ActsAsTable::Mapper::RowModel] row_model_mapper
84
+ # @yieldreturn [void]
85
+ # @return [void]
86
+ #
87
+ # @see ActsAsTable::Mapper::RowModel
88
+ def draw(&block)
89
+ ActsAsTable::Mapper::RowModel.new(self, &block)
90
+
91
+ return
92
+ end
93
+
94
+ # Returns the ActsAsTable headers array object for the ActsAsTable column models for this ActsAsTable row model.
95
+ #
96
+ # @return [ActsAsTable::Headers::Array]
97
+ def to_headers
98
+ ActsAsTable::Headers::Array.new(self.column_models)
99
+ end
100
+
101
+ # Returns the ActsAsTable records for the given row.
102
+ #
103
+ # @param [Array<String, nil>, nil] row
104
+ # @param [ActiveRecord::Relation<ActsAsTable::Record>] records
105
+ # @return [Array<ActsAsTable::Record>]
106
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
107
+ def from_row(row = [], records = ActsAsTable::Record.all)
108
+ # @return [Hash<ActsAsTable::RecordModel, Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>>]
109
+ value_by_record_model_and_value_provider = self.record_models.inject({}) { |acc_for_record_model, record_model|
110
+ acc_for_record_model[record_model] = record_model.each_acts_as_table_value_provider(except: [:row_model]).inject({}) { |acc_for_value_provider, value_provider|
111
+ acc_for_value_provider[value_provider] = value_provider.column_model.try { |column_model|
112
+ # @return [Integer]
113
+ index = column_model.position - 1
114
+
115
+ if (index >= 0) && (index < row.size)
116
+ row[index]
117
+ else
118
+ nil
119
+ end
120
+ }
121
+
122
+ acc_for_value_provider
123
+ }
124
+
125
+ acc_for_record_model
126
+ }
127
+
128
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
129
+ hash = ActsAsTable.adapter.set_value_for(self, nil, value_by_record_model_and_value_provider, default: true)
130
+
131
+ hash.target_value.each_pair.collect { |pair|
132
+ record_model, pair = *pair
133
+
134
+ new_record_or_persisted, pair_by_value_provider = *pair
135
+
136
+ records.build(record_model_changed: pair_by_value_provider.changed?) do |record|
137
+ record.base = new_record_or_persisted
138
+
139
+ record.record_model = record_model
140
+
141
+ # @note {ActiveRecord::Validations#validate} is an alias for {ActiveRecord::Validations#valid?} that does not raise an exception when the record is invalid.
142
+ new_record_or_persisted.validate
143
+
144
+ # @return [Array<String>]
145
+ attribute_names = []
146
+
147
+ pair_by_value_provider.target_value.each do |value_provider, target_value|
148
+ # @return [String]
149
+ attribute_name = \
150
+ case value_provider
151
+ when ActsAsTable::ForeignKey
152
+ klass = record_model.class_name.constantize
153
+
154
+ reflection = klass.reflect_on_association(value_provider.method_name)
155
+
156
+ reflection.foreign_key
157
+ else
158
+ value_provider.method_name
159
+ end
160
+
161
+ attribute_names << attribute_name
162
+
163
+ record.values.build(target_value: target_value.target_value, value_provider_changed: target_value.changed?) do |value|
164
+ value.value_provider = value_provider
165
+
166
+ value_provider.column_model.try { |column_model|
167
+ value.column_model = column_model
168
+
169
+ value.position = column_model.position
170
+
171
+ # @return [Integer]
172
+ index = column_model.position - 1
173
+
174
+ value.source_value = \
175
+ if (index >= 0) && (index < row.size)
176
+ row[index]
177
+ else
178
+ nil
179
+ end
180
+ }
181
+
182
+ new_record_or_persisted.errors[attribute_name].each do |message|
183
+ value.record_errors.build(attribute_name: attribute_name, message: message)
184
+ end
185
+ end
186
+ end
187
+
188
+ new_record_or_persisted.errors.each do |attribute_name, message|
189
+ unless attribute_names.include?(attribute_name.to_s)
190
+ record.record_errors.build(attribute_name: attribute_name, message: message)
191
+ end
192
+ end
193
+ end
194
+ }
195
+ end
196
+
197
+ # Returns the row for the given record.
198
+ #
199
+ # @param [ActiveRecord::Base, nil] base
200
+ # @return [Array<String, nil>, nil]
201
+ # @raise [ArgumentError]
202
+ def to_row(base = nil)
203
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
204
+ value_by_record_model_and_value_provider = ActsAsTable.adapter.get_value_for(self, base, default: false)
205
+
206
+ # @return [Integer]
207
+ column_models_maximum_position = (self.persisted? ? self.column_models.maximum(:position) : self.column_models.to_a.collect(&:position).max) || 0
208
+
209
+ # @return [Array<String, nil>]
210
+ row = ::Array.new(column_models_maximum_position) { nil }
211
+
212
+ self.record_models.each do |record_model|
213
+ # @return [ActsAsTable::ValueProvider::WrappedValue, nil]
214
+ value_by_value_provider = value_by_record_model_and_value_provider.target_value.try(:[], record_model)
215
+
216
+ record_model.each_acts_as_table_value_provider(except: [:row_model]) do |value_provider|
217
+ value_provider.column_model.try { |column_model|
218
+ # @return [Integer]
219
+ index = column_model.position - 1
220
+
221
+ if (index >= 0) && (index < column_models_maximum_position)
222
+ # @return [ActsAsTable::ValueProvider::WrappedValue, nil]
223
+ value = value_by_value_provider.try(:target_value).try(:[], value_provider)
224
+
225
+ row[index] = value.try(:target_value)
226
+ end
227
+ }
228
+ end
229
+ end
230
+
231
+ row.all?(&:nil?) ? nil : row
232
+ end
233
+
234
+ include ActsAsTable::RecordModelClassMethods
235
+
236
+ # Returns `true` if the given ActsAsTable record model is reachable from the root ActsAsTable record model for this ActsAsTable row model. Otherwise, returns `false`.
237
+ #
238
+ # @param [ActsAsTable::RecordModel] record_model
239
+ # @return [Boolean]
240
+ def reachable_record_model?(record_model)
241
+ self.class.reachable_record_model_for?(self.root_record_model, record_model)
242
+ end
243
+
244
+ # Returns the ActsAsTable record models that are reachable from the root ActsAsTable record model for this ActsAsTable row model (in topological order).
245
+ #
246
+ # @return [Array<ActsAsTable::RecordModel>]
247
+ def reachable_record_models
248
+ self.class.reachable_record_models_for(self.root_record_model)
249
+ end
250
+
251
+ # Get the value for the given record using the given options.
252
+ #
253
+ # @param [ActiveRecord::Base, nil] base
254
+ # @param [Hash<Symbol, Object>] options
255
+ # @option options [Boolean] :default
256
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
257
+ # @raise [ArgumentError]
258
+ def get_value(base = nil, **options)
259
+ self.class.get_value_for(self.root_record_model, base, **options)
260
+ end
261
+
262
+ # Set the new value for the given record using the given options.
263
+ #
264
+ # @param [ActiveRecord::Base, nil] base
265
+ # @param [Hash<ActsAsTable::RecordModel, Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>>] new_value_by_record_model_and_value_provider
266
+ # @param [Hash<Symbol, Object>] options
267
+ # @option options [Boolean] :default
268
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
269
+ # @raise [ArgumentError]
270
+ def set_value(base = nil, new_value_by_record_model_and_value_provider = {}, **options)
271
+ self.class.set_value_for(self.root_record_model, base, new_value_by_record_model_and_value_provider, **options)
272
+ end
273
+
274
+ private
275
+
276
+ # @return [void]
277
+ def record_models_includes_root_record_model
278
+ unless self.record_models.include?(self.root_record_model)
279
+ self.errors.add('root_record_model_id', :inclusion)
280
+ end
281
+
282
+ return
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,41 @@
1
+ module ActsAsTable
2
+ # ActsAsTable table.
3
+ #
4
+ # @!attribute [r] records_count
5
+ # Returns the number of ActsAsTable records for this ActsAsTable table.
6
+ #
7
+ # @return [Integer]
8
+ class Table < ::ActiveRecord::Base
9
+ # @!parse
10
+ # include ActsAsTable::ValueProvider
11
+ # include ActsAsTable::ValueProviderAssociationMethods
12
+
13
+ self.table_name = ActsAsTable.tables_table
14
+
15
+ # Returns the ActsAsTable row model for this ActsAsTable table.
16
+ belongs_to :row_model, **{
17
+ class_name: 'ActsAsTable::RowModel',
18
+ inverse_of: :tables,
19
+ required: true,
20
+ }
21
+
22
+ # Returns the ActsAsTable records for this ActsAsTable table.
23
+ has_many :records, -> { order(position: :asc) }, **{
24
+ autosave: true,
25
+ class_name: 'ActsAsTable::Record',
26
+ dependent: :destroy,
27
+ foreign_key: 'table_id',
28
+ inverse_of: :table,
29
+ validate: true,
30
+ }
31
+
32
+ # Returns the ActsAsTable records for the given row.
33
+ #
34
+ # @param [Array<String, nil>, nil] row
35
+ # @return [Array<ActsAsTable::Record>]
36
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
37
+ def from_row(row = [])
38
+ self.row_model.from_row(row, self.records)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,80 @@
1
+ module ActsAsTable
2
+ # ActsAsTable value.
3
+ #
4
+ # @!attribute [rw] position
5
+ # Returns the position of this ActsAsTable value or `nil`.
6
+ #
7
+ # @return [Integer, nil]
8
+ # @!attribute [r] record_errors_count
9
+ # Returns the number of ActsAsTable record errors for this ActsAsTable value.
10
+ #
11
+ # @return [Integer]
12
+ # @!attribute [rw] source_value
13
+ # Returns the source value for this ActsAsTable value.
14
+ #
15
+ # @return [String, nil]
16
+ # @!attribute [rw] target_value
17
+ # Returns the target value for this ActsAsTable value.
18
+ #
19
+ # @return [String, nil]
20
+ # @!attribute [rw] value_provider_changed
21
+ # Returns `true` if the ActsAsTable value provider changed the value for this ActsAsTable value. Otherwise, returns `false`.
22
+ #
23
+ # @return [Boolean]
24
+ class Value < ::ActiveRecord::Base
25
+ # @!parse
26
+ # include ActsAsTable::ValueProvider
27
+ # include ActsAsTable::ValueProviderAssociationMethods
28
+
29
+ self.table_name = ActsAsTable.values_table
30
+
31
+ # Returns the ActsAsTable column model that provided this ActsAsTable value or `nil`.
32
+ #
33
+ # @return [ActsAsTable::ColumnModel, nil]
34
+ belongs_to :column_model, **{
35
+ class_name: 'ActsAsTable::ColumnModel',
36
+ inverse_of: :values,
37
+ required: false,
38
+ }
39
+
40
+ # Returns the ActsAsTable record for this ActsAsTable value.
41
+ belongs_to :record, **{
42
+ class_name: 'ActsAsTable::Record',
43
+ counter_cache: 'values_count',
44
+ inverse_of: :values,
45
+ required: true,
46
+ }
47
+
48
+ # Returns the ActsAsTable value provider that provider the value for this ActsAsTable value.
49
+ #
50
+ # @return [ActsAsTable::ValueProvider::InstanceMethods]
51
+ belongs_to :value_provider, **{
52
+ polymorphic: true,
53
+ required: true,
54
+ }
55
+
56
+ # Returns the ActsAsTable record errors for this ActsAsTable value.
57
+ has_many :record_errors, -> { order(attribute_name: :asc, message: :asc) }, **{
58
+ autosave: false,
59
+ class_name: 'ActsAsTable::RecordError',
60
+ dependent: :nullify,
61
+ foreign_key: 'value_id',
62
+ inverse_of: :values,
63
+ validate: false,
64
+ }
65
+
66
+ validates :record_id, **{
67
+ uniqueness: {
68
+ scope: ['value_provider_id', 'value_provider_type'],
69
+ },
70
+ }
71
+
72
+ # validates :position, **{}
73
+
74
+ # validates :source_value, **{}
75
+
76
+ # validates :target_value, **{}
77
+
78
+ # validates :value_provider_changed, **{}
79
+ end
80
+ end
@@ -0,0 +1,554 @@
1
+ require 'singleton'
2
+
3
+ module ActsAsTable
4
+ # ActsAsTable record model class methods (concern).
5
+ module RecordModelClassMethods
6
+ extend ::ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Returns `true` if the target ActsAsTable record model is reachable from the source ActsAsTable record model. Otherwise, returns `false`.
10
+ #
11
+ # @param [ActsAsTable::RecordModel] source_record_model
12
+ # @param [ActsAsTable::RecordModel] target_record_model
13
+ # @return [Boolean]
14
+ def reachable_record_model_for?(source_record_model, target_record_model)
15
+ Inject.instance.inject(false, source_record_model) { |acc, path, &block|
16
+ # @note Continue until the target ActsAsTable record model is reached.
17
+ acc || (path.options.dig(:data, :target_record_model) == target_record_model) || block.call(acc)
18
+ }
19
+ end
20
+
21
+ # Returns the ActsAsTable record models that are reachable from the given ActsAsTable record models (in topological order).
22
+ #
23
+ # @param [ActsAsTable::RecordModel] record_model
24
+ # @return [Array<ActsAsTable::RecordModel>]
25
+ def reachable_record_models_for(record_model)
26
+ Inject.instance.inject([], record_model) { |acc, path, &block|
27
+ # @return [ActsAsTable::RecordModel]
28
+ target_record_model = path.options.dig(:data, :target_record_model)
29
+
30
+ # @note Continue until every target ActsAsTable record is reached one or more times.
31
+ if acc.include?(target_record_model)
32
+ acc
33
+ else
34
+ acc << target_record_model
35
+
36
+ block.call(acc)
37
+ end
38
+ }
39
+ end
40
+
41
+ # Get the value for the given record using the given options.
42
+ #
43
+ # @param [ActsAsTable::RecordModel] record_model
44
+ # @param [ActiveRecord::Base, nil] base
45
+ # @param [Hash<Symbol, Object>] options
46
+ # @option options [Boolean] :default
47
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
48
+ # @raise [ArgumentError]
49
+ def get_value_for(record_model, base = nil, **options)
50
+ GetValue.new(**options).call(record_model, base)
51
+ end
52
+
53
+ # Set the new value for the given record using the given options.
54
+ #
55
+ # @param [ActsAsTable::RecordModel] record_model
56
+ # @param [ActiveRecord::Base, nil] base
57
+ # @param [Hash<ActsAsTable::RecordModel, Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>>] new_value_by_record_model_and_value_provider
58
+ # @param [Hash<Symbol, Object>] options
59
+ # @option options [Boolean] :default
60
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
61
+ # @raise [ArgumentError]
62
+ def set_value_for(record_model, base = nil, new_value_by_record_model_and_value_provider = {}, **options)
63
+ SetValue.new(new_value_by_record_model_and_value_provider, **options).call(record_model, base)
64
+ end
65
+ end
66
+
67
+ # ActsAsTable "non-destructive" traversal (does not create new records if they are not found).
68
+ class Inject
69
+ include ::Singleton
70
+
71
+ # Combine all ActsAsTable paths using an associative binary operation.
72
+ #
73
+ # @param [Object] acc
74
+ # @param [ActsAsTable::RecordModel] record_model
75
+ # @yieldparam [Object] acc
76
+ # @yieldparam [ActsAsTable::Path] path
77
+ # @yieldreturn [Object]
78
+ # @return [Object]
79
+ def inject(acc, record_model, &block)
80
+ # @return [Class]
81
+ klass = record_model.class_name.constantize
82
+
83
+ # @return [ActsAsTable::Path]
84
+ path = ActsAsTable::Path.new(klass, nil, data: {
85
+ source_record_model: nil,
86
+ target_record_model: record_model,
87
+ })
88
+
89
+ _inject(acc, path, &block)
90
+ end
91
+
92
+ private
93
+
94
+ # @param [Object] orig_acc
95
+ # @yieldparam [ActsAsTable::Path] orig_path
96
+ # @yieldparam [Object] acc
97
+ # @yieldparam [ActsAsTable::Path] path
98
+ # @yieldreturn [Object]
99
+ # @return [Object]
100
+ def _inject(orig_acc, orig_path, &block)
101
+ block.call(orig_acc, orig_path) do |new_acc|
102
+ # @return [ActsAsTable::RecordModel]
103
+ source_record_model = orig_path.options.dig(:data, :target_record_model)
104
+
105
+ %i(belongs_to).each do |macro|
106
+ # @return [Array<ActsAsTable::BelongsTo>]
107
+ macro_reflection_models = source_record_model.persisted? ? source_record_model.send(:"#{macro.to_s.pluralize}_as_source").to_a : source_record_model.row_model.send(:"#{macro.to_s.pluralize}").to_a.select { |macro_reflection_model| macro_reflection_model.source_record_model == source_record_model }
108
+
109
+ macro_reflection_models.each do |macro_reflection_model|
110
+ # @return [ActsAsTable::RecordModel]
111
+ target_record_model = macro_reflection_model.target_record_model
112
+
113
+ # @return [ActsAsTable::Path]
114
+ new_path = orig_path.send(macro_reflection_model.macro, macro_reflection_model.method_name, data: {
115
+ source_record_model: source_record_model,
116
+ target_record_model: target_record_model,
117
+ })
118
+
119
+ new_acc = _inject(new_acc, new_path, &block)
120
+ end
121
+ end
122
+
123
+ %i(has_many).each do |macro|
124
+ # @return [Array<ActsAsTable::HasMany>]
125
+ macro_reflection_models = source_record_model.persisted? ? source_record_model.send(:"#{macro.to_s.pluralize}_as_source").to_a : source_record_model.row_model.send(:"#{macro.to_s.pluralize}").to_a.select { |macro_reflection_model| macro_reflection_model.source_record_model == source_record_model }
126
+
127
+ macro_reflection_models.each do |macro_reflection_model|
128
+ # @return [Array<ActsAsTable::HasManyTarget>]
129
+ macro_reflection_model_targets = macro_reflection_model.persisted? ? macro_reflection_model.send(:"#{macro.to_s.singularize}_targets").to_a : macro_reflection_model.send(:"#{macro.to_s.singularize}_targets").to_a.sort_by(&:position)
130
+
131
+ macro_reflection_model_targets.each do |macro_reflection_model_target|
132
+ # @return [ActsAsTable::RecordModel]
133
+ target_record_model = macro_reflection_model_target.record_model
134
+
135
+ # @return [Integer]
136
+ index = macro_reflection_model_target.position - 1
137
+
138
+ # @return [ActsAsTable::Path]
139
+ new_path = orig_path.send(macro_reflection_model.macro, macro_reflection_model.method_name, index, data: {
140
+ source_record_model: source_record_model,
141
+ target_record_model: target_record_model,
142
+ })
143
+
144
+ new_acc = _inject(new_acc, new_path, &block)
145
+ end
146
+ end
147
+ end
148
+
149
+ new_acc
150
+ end
151
+ end
152
+ end
153
+
154
+ # ActsAsTable "destructive" traversal (creates new records if they are not found and updates persisted records).
155
+ class FindOrInitializeBy
156
+ # Returns the number of mandatory arguments.
157
+ #
158
+ # @return [Integer]
159
+ def arity
160
+ 1
161
+ end
162
+
163
+ # Invokes the traversal.
164
+ #
165
+ # @param [ActsAsTable::RecordModel] record_model
166
+ # @param [ActiveRecord::Base, nil] base
167
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
168
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
169
+ def call(record_model, base = nil)
170
+ raise ::NotImplementedError.new("#{self.class}#call")
171
+ end
172
+
173
+ # Combine all ActsAsTable paths using an associative binary operation.
174
+ #
175
+ # @param [Object] acc
176
+ # @param [ActsAsTable::RecordModel] record_model
177
+ # @param [ActiveRecord::Base, nil] base
178
+ # @yieldparam [Object] acc
179
+ # @yieldparam [ActsAsTable::Path] path
180
+ # @yieldreturn [Object]
181
+ # @return [Object]
182
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
183
+ def inject(acc, record_model, base = nil)
184
+ # @return [Class]
185
+ klass = record_model.class_name.constantize
186
+
187
+ # @return [ActsAsTable::Path]
188
+ path = ActsAsTable::Path.new(klass, nil, data: {
189
+ source_record_model: nil,
190
+ source_record: nil,
191
+ target_record_model: record_model,
192
+ target_record: base,
193
+ })
194
+
195
+ _inject(acc, path, **{
196
+ find_or_initialize_by: ::Proc.new { |*args, &block|
197
+ ActsAsTable.adapter.find_or_initialize_by_for(record_model, klass, :find_by!, *args, &block)
198
+ },
199
+ new: ::Proc.new { |*args, &block|
200
+ ActsAsTable.adapter.new_for(record_model, klass, :new, *args, &block)
201
+ },
202
+ })
203
+ end
204
+
205
+ protected
206
+
207
+ # @param [Object] acc
208
+ # @param [ActsAsTable::Path] path
209
+ # @yieldparam [Object] acc
210
+ # @yieldparam [ActsAsTable::Path] path
211
+ # @yieldreturn [Object]
212
+ # @return [Object]
213
+ # @raise [ArgumentError]
214
+ def _around_inject(acc, path, &block)
215
+ block.call(acc)
216
+ end
217
+
218
+ # @param [Object] acc
219
+ # @param [ActsAsTable::RecordModel] record_model
220
+ # @param [ActiveRecord::Base, nil] base
221
+ # @param [Hash<Symbol, Object>] options
222
+ # @option options [#call] :find_or_initialize_by
223
+ # @option options [#call] :new
224
+ # @return [Array<Object>]
225
+ def _find_or_initialize(acc, record_model, base = nil, **options)
226
+ [acc, base]
227
+ end
228
+
229
+ # @param [Object] acc
230
+ # @param [ActsAsTable::RecordModel] record_model
231
+ # @return [ActiveRecord::Base, nil]
232
+ def _at(acc, record_model)
233
+ nil
234
+ end
235
+
236
+ private
237
+
238
+ # @param [Object] orig_acc
239
+ # @param [ActsAsTable::Path] orig_path
240
+ # @param [Hash<Symbol, Object>] options
241
+ # @option options [#call] :find_or_initialize_by
242
+ # @option options [#call] :new
243
+ # @yieldparam [ActsAsTable::Path] orig_path
244
+ # @yieldparam [Object] acc
245
+ # @yieldparam [ActsAsTable::Path] path
246
+ # @yieldreturn [Object]
247
+ # @return [Object]
248
+ # @raise [ArgumentError]
249
+ def _inject(orig_acc, orig_path, **options)
250
+ options.assert_valid_keys(:find_or_initialize_by, :new)
251
+
252
+ _around_inject(orig_acc, orig_path) do |new_acc|
253
+ # @return [ActsAsTable::RecordModel]
254
+ source_record_model = orig_path.options.dig(:data, :target_record_model)
255
+
256
+ # @return [ActiveRecord::Base, nil]
257
+ source_record = orig_path.options.dig(:data, :target_record)
258
+
259
+ unless source_record.nil? || source_record.class.name.eql?(source_record_model.class_name)
260
+ raise ::ArgumentError.new("#{self.name}#_inject - source_record - expected: #{source_record_model.class_name.inspect}, found: #{source_record.inspect}")
261
+ end
262
+
263
+ new_acc, source_record = *_find_or_initialize(new_acc, source_record_model, source_record, **options)
264
+
265
+ unless source_record.nil?
266
+ %i(belongs_to).each do |macro|
267
+ # @return [Array<ActsAsTable::BelongsTo>]
268
+ macro_reflection_models = source_record_model.persisted? ? source_record_model.send(:"#{macro.to_s.pluralize}_as_source").to_a : source_record_model.row_model.send(:"#{macro.to_s.pluralize}").to_a.select { |macro_reflection_model| macro_reflection_model.source_record_model == source_record_model }
269
+
270
+ macro_reflection_models.each do |macro_reflection_model|
271
+ # @return [ActsAsTable::RecordModel]
272
+ target_record_model = macro_reflection_model.target_record_model
273
+
274
+ # @return [ActiveRecord::Reflection::MacroReflection]
275
+ reflection = source_record.class.reflect_on_association(macro_reflection_model.method_name)
276
+
277
+ # @return [ActiveRecord::Base, nil]
278
+ target_record = source_record.send(reflection.name)
279
+
280
+ if target_record.nil?
281
+ target_record = source_record.send(:"#{reflection.name}=", _at(new_acc, target_record_model))
282
+ end
283
+
284
+ # @return [ActsAsTable::Path]
285
+ new_path = orig_path.send(macro_reflection_model.macro, macro_reflection_model.method_name, data: {
286
+ source_record_model: source_record_model,
287
+ source_record: source_record,
288
+ target_record_model: target_record_model,
289
+ target_record: target_record,
290
+ })
291
+
292
+ new_acc = _inject(new_acc, new_path, **options.merge({
293
+ find_or_initialize_by: ::Proc.new { |*args, &block|
294
+ source_record.send(:"#{reflection.name}=", ActsAsTable.adapter.find_or_initialize_by_for(target_record_model, reflection.klass, :find_by!, *args, &block))
295
+ },
296
+ new: ::Proc.new { |*args, &block|
297
+ source_record.send(:"#{reflection.name}=", nil)
298
+
299
+ ActsAsTable.adapter.new_for(target_record_model, source_record, :"build_#{reflection.name}", *args, &block)
300
+ },
301
+ }))
302
+ end
303
+ end
304
+
305
+ %i(has_many).each do |macro|
306
+ # @return [Array<ActsAsTable::HasMany>]
307
+ macro_reflection_models = source_record_model.persisted? ? source_record_model.send(:"#{macro.to_s.pluralize}_as_source").to_a : source_record_model.row_model.send(:"#{macro.to_s.pluralize}").to_a.select { |macro_reflection_model| macro_reflection_model.source_record_model == source_record_model }
308
+
309
+ macro_reflection_models.each do |macro_reflection_model|
310
+ # @return [ActiveRecord::Reflection::MacroReflection]
311
+ reflection = source_record.class.reflect_on_association(macro_reflection_model.method_name)
312
+
313
+ # @return [ActiveRecord::Relation<ActiveRecord::Base>]
314
+ relation = source_record.send(reflection.name)
315
+
316
+ # @return [Array<ActiveRecord::Base>]
317
+ target_records = relation.to_a
318
+
319
+ # @return [Array<ActsAsTable::HasManyTarget>]
320
+ macro_reflection_model_targets = macro_reflection_model.persisted? ? macro_reflection_model.send(:"#{macro.to_s.singularize}_targets").to_a : macro_reflection_model.send(:"#{macro.to_s.singularize}_targets").to_a.sort_by(&:position)
321
+
322
+ macro_reflection_model_targets.each do |macro_reflection_model_target|
323
+ # @return [ActsAsTable::RecordModel]
324
+ target_record_model = macro_reflection_model_target.record_model
325
+
326
+ # @return [Integer]
327
+ index = macro_reflection_model_target.position - 1
328
+
329
+ target_record = \
330
+ if (index >= 0) && (index < target_records.size)
331
+ target_records[index]
332
+ else
333
+ nil
334
+ end
335
+
336
+ if target_record.nil?
337
+ target_record = _at(new_acc, target_record_model)
338
+
339
+ unless target_record.nil? || target_records.include?(target_record)
340
+ relation.proxy_association.add_to_target(target_record)
341
+ end
342
+ end
343
+
344
+ # @return [ActsAsTable::Path]
345
+ new_path = orig_path.send(macro_reflection_model.macro, macro_reflection_model.method_name, index, data: {
346
+ source_record_model: source_record_model,
347
+ source_record: source_record,
348
+ target_record_model: target_record_model,
349
+ target_record: target_record,
350
+ })
351
+
352
+ new_acc = _inject(new_acc, new_path, **options.merge({
353
+ find_or_initialize_by: ::Proc.new { |*args, &block|
354
+ ActsAsTable.adapter.find_or_initialize_by_for(target_record_model, relation, :find_by!, *args, &block)
355
+ },
356
+ new: ::Proc.new { |*args, &block|
357
+ ActsAsTable.adapter.new_for(target_record_model, relation, :build, *args, &block)
358
+ },
359
+ }))
360
+ end
361
+ end
362
+ end
363
+ end
364
+
365
+ new_acc
366
+ end
367
+ end
368
+ end
369
+
370
+ # Get the value for the given record using the given options.
371
+ #
372
+ # @!attribute [r] options
373
+ # The options for this ActsAsTable "destructive" traversal.
374
+ #
375
+ # @return [Hash<Symbol, Object>]
376
+ class GetValue < FindOrInitializeBy
377
+ attr_reader :options
378
+
379
+ # Returns a new ActsAsTable "destructive" traversal.
380
+ #
381
+ # @param [Hash<Symbol, Object>] options
382
+ # @option options [Boolean] :default
383
+ # @return [ActsAsTable::RecordModelClassMethods::GetValue]
384
+ def initialize(**options)
385
+ super()
386
+
387
+ options.assert_valid_keys(:default)
388
+
389
+ @options = options.dup
390
+ end
391
+
392
+ # Invokes the traversal.
393
+ #
394
+ # @param [ActsAsTable::RecordModel] record_model
395
+ # @param [ActiveRecord::Base, nil] base
396
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
397
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
398
+ def call(record_model, base = nil)
399
+ ActsAsTable.adapter.wrap_value_for(record_model, base, nil, self.inject({}, record_model, base))
400
+ end
401
+
402
+ protected
403
+
404
+ # @param [Hash<ActsAsTable::RecordModel, ActsAsTable::ValueProvider::WrappedValue>] acc
405
+ # @param [ActsAsTable::Path] path
406
+ # @yieldparam [Hash<ActsAsTable::RecordModel, ActsAsTable::ValueProvider::WrappedValue>] acc
407
+ # @yieldparam [ActsAsTable::Path] path
408
+ # @yieldreturn [Hash<ActsAsTable::RecordModel, ActsAsTable::ValueProvider::WrappedValue>]
409
+ # @return [Hash<ActsAsTable::RecordModel, ActsAsTable::ValueProvider::WrappedValue>]
410
+ # @raise [ArgumentError]
411
+ def _around_inject(acc, path, &block)
412
+ acc.key?(path.options.dig(:data, :target_record_model)) ? acc : block.call(acc)
413
+ end
414
+
415
+ # @param [Hash<ActsAsTable::RecordModel, ActsAsTable::ValueProvider::WrappedValue>] acc
416
+ # @param [ActiveRecord::Base, nil] base
417
+ # @param [Hash<Symbol, Object>] options
418
+ # @option options [#call] :find_or_initialize_by
419
+ # @option options [#call] :new
420
+ # @return [Array<Object>]
421
+ # @raise [ArgumentError]
422
+ def _find_or_initialize(acc, record_model, base = nil, **options)
423
+ acc[record_model] ||= ActsAsTable.adapter.get_value_for(record_model, base, **@options)
424
+
425
+ [acc, base]
426
+ end
427
+ end
428
+
429
+ # Set the new value for the given record using the given options.
430
+ #
431
+ # @!attribute [r] new_value_by_record_model_and_value_provider
432
+ # The new values by ActsAsTable record model and value provider for this ActsAsTable "destructive" traversal.
433
+ #
434
+ # @return [Hash<ActsAsTable::RecordModel, Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>>]
435
+ # @!attribute [r] options
436
+ # The options for this ActsAsTable "destructive" traversal.
437
+ #
438
+ # @return [Hash<Symbol, Object>]
439
+ class SetValue < FindOrInitializeBy
440
+ attr_reader :new_value_by_record_model_and_value_provider, :options
441
+
442
+ # Returns a new ActsAsTable "destructive" traversal.
443
+ #
444
+ # @param [Hash<ActsAsTable::RecordModel, Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>>] new_value_by_record_model_and_value_provider
445
+ # @param [Hash<Symbol, Object>] options
446
+ # @option options [Boolean] :default
447
+ # @return [ActsAsTable::RecordModelClassMethods::SetValue]
448
+ def initialize(new_value_by_record_model_and_value_provider = {}, **options)
449
+ super()
450
+
451
+ options.assert_valid_keys(:default)
452
+
453
+ @new_value_by_record_model_and_value_provider, @options = new_value_by_record_model_and_value_provider, options.dup
454
+ end
455
+
456
+ # Invokes the traversal.
457
+ #
458
+ # @param [ActsAsTable::RecordModel] record_model
459
+ # @param [ActiveRecord::Base, nil] base
460
+ # @return [ActsAsTable::ValueProvider::WrappedValue]
461
+ # @raise [ArgumentError] If the name of a class for a given record does not match the class name for the corresponding ActsAsTable record model.
462
+ def call(record_model, base = nil)
463
+ ActsAsTable.adapter.wrap_value_for(record_model, base, nil, self.inject({}, record_model, base))
464
+ end
465
+
466
+ protected
467
+
468
+ # @param [Hash<ActsAsTable::RecordModel, Array<Object>>] acc
469
+ # @param [ActsAsTable::Path] path
470
+ # @yieldparam [Hash<ActsAsTable::RecordModel, Array<Object>>] acc
471
+ # @yieldparam [ActsAsTable::Path] path
472
+ # @yieldreturn [Hash<ActsAsTable::RecordModel, Array<Object>>]
473
+ # @return [Hash<ActsAsTable::RecordModel, Array<Object>>]
474
+ # @raise [ArgumentError]
475
+ def _around_inject(acc, path, &block)
476
+ # @return [ActsAsTable::RecordModel]
477
+ source_record_model = path.options.dig(:data, :source_record_model)
478
+
479
+ # @return [ActsAsTable::RecordModel]
480
+ target_record_model = path.options.dig(:data, :target_record_model)
481
+
482
+ if path.collect(&:options).any? { |options| options.dig(:data, :source_record_model) == target_record_model }
483
+ acc
484
+ else
485
+ # @return [Hash<ActsAsTable::RecordModel, Array<Object>>]
486
+ new_acc = block.call(acc)
487
+
488
+ unless source_record_model.nil?
489
+ if new_acc[target_record_model][1].changed?
490
+ new_acc[source_record_model][1].changed!
491
+ end
492
+ end
493
+
494
+ new_acc
495
+ end
496
+ end
497
+
498
+ # @param [Hash<ActsAsTable::RecordModel, Array<Object>>] acc
499
+ # @param [ActsAsTable::RecordModel] record_model
500
+ # @return [ActiveRecord::Base, nil]
501
+ def _at(acc, record_model)
502
+ acc.key?(record_model) ? acc[record_model][0] : nil
503
+ end
504
+
505
+ # @param [Hash<ActsAsTable::RecordModel, Array<Object>>] acc
506
+ # @param [ActsAsTable::RecordModel] record_model
507
+ # @param [ActiveRecord::Base, nil] base
508
+ # @param [Hash<Symbol, Object>] options
509
+ # @option options [#call] :find_or_initialize_by
510
+ # @option options [#call] :new
511
+ # @return [Array<Object>]
512
+ # @raise [ArgumentError]
513
+ def _find_or_initialize(acc, record_model, base = nil, **options)
514
+ acc[record_model] ||= begin
515
+ # @return [Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>, nil]
516
+ orig_value_by_value_provider = @new_value_by_record_model_and_value_provider.try(:[], record_model)
517
+
518
+ unless (primary_key = record_model.primary_keys.first).nil? || (base_id = orig_value_by_value_provider.try(:[], primary_key)).nil?
519
+ # @return [ActiveRecord::Base]
520
+ # @raise [ActiveRecord::RecordNotFound]
521
+ base = options[:find_or_initialize_by].call({
522
+ primary_key.method_name => base_id,
523
+ })
524
+
525
+ # @return [Hash<ActsAsTable::ValueProvider::InstanceMethods, Object>, nil]
526
+ new_value_by_value_provider = orig_value_by_value_provider.try(:each_pair).try(:all?) { |pair|
527
+ (pair[0] == primary_key) || pair[0].column_model.nil? || pair[1].nil?
528
+ } ? orig_value_by_value_provider.try(:delete_if) { |value_provider, value|
529
+ value_provider != primary_key
530
+ } : orig_value_by_value_provider.try(:delete_if) { |value_provider, value|
531
+ value_provider.column_model.nil?
532
+ }
533
+
534
+ [
535
+ base,
536
+ ActsAsTable.adapter.set_value_for(record_model, base, new_value_by_value_provider, **@options),
537
+ ]
538
+ else
539
+ if base.nil?
540
+ base = options[:new].call
541
+ end
542
+
543
+ [
544
+ base,
545
+ ActsAsTable.adapter.set_value_for(record_model, base, orig_value_by_value_provider, **@options),
546
+ ]
547
+ end
548
+ end
549
+
550
+ [acc, base]
551
+ end
552
+ end
553
+ end
554
+ end