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,299 @@
1
+ module ActsAsTable
2
+ # ActsAsTable foreign key (value provider).
3
+ #
4
+ # @!attribute [rw] default_value
5
+ # Returns the default value for this ActsAsTable foreign key.
6
+ #
7
+ # @return [String, nil]
8
+ # @!attribute [rw] method_name
9
+ # Returns the method name for this ActsAsTable foreign key (a `:belongs_to` association).
10
+ #
11
+ # @return [String]
12
+ # @!attribute [rw] polymorphic
13
+ # Returns 'true' if this ActsAsTable foreign key is polymorphic. Otherwise, returns `false`. By default, this is `false`.
14
+ #
15
+ # @return [Boolean]
16
+ # @!attribute [rw] primary_key
17
+ # Returns the name of the method that returns the primary key used for the association. By default, this is `id`.
18
+ #
19
+ # @return [String]
20
+ class ForeignKey < ::ActiveRecord::Base
21
+ # @!parse
22
+ # include ActsAsTable::ValueProvider
23
+ # include ActsAsTable::ValueProvider::InstanceMethods
24
+ # include ActsAsTable::ValueProviderAssociationMethods
25
+
26
+ self.table_name = ActsAsTable.foreign_keys_table
27
+
28
+ acts_as_table_value_provider
29
+
30
+ # Returns the ActsAsTable column model for this ActsAsTable foreign key or `nil`.
31
+ #
32
+ # @return [ActsAsTable::ColumnModel, nil]
33
+ belongs_to :column_model, **{
34
+ class_name: 'ActsAsTable::ColumnModel',
35
+ inverse_of: :foreign_keys,
36
+ required: false,
37
+ }
38
+
39
+ # Returns the ActsAsTable record model for this ActsAsTable foreign key.
40
+ belongs_to :record_model, **{
41
+ class_name: 'ActsAsTable::RecordModel',
42
+ inverse_of: :foreign_keys,
43
+ required: true,
44
+ }
45
+
46
+ # Returns the maps for this ActsAsTable foreign key.
47
+ has_many :foreign_key_maps, -> { order(position: :asc) }, **{
48
+ autosave: true,
49
+ class_name: 'ActsAsTable::ForeignKeyMap',
50
+ dependent: :destroy,
51
+ foreign_key: 'foreign_key_id',
52
+ inverse_of: :foreign_key,
53
+ validate: true,
54
+ }
55
+
56
+ # Returns the ActsAsTable values that have been provided by this ActsAsTable foreign key.
57
+ has_many :values, **{
58
+ as: :value_provider,
59
+ class_name: 'ActsAsTable::Value',
60
+ dependent: :restrict_with_exception,
61
+ foreign_key: 'value_provider_id',
62
+ foreign_type: 'value_provider_type',
63
+ inverse_of: :value_provider,
64
+ }
65
+
66
+ accepts_nested_attributes_for :foreign_key_maps, **{
67
+ allow_destroy: true,
68
+ }
69
+
70
+ validates :default_value, **{
71
+ presence: {
72
+ unless: :column_model,
73
+ },
74
+ }
75
+
76
+ validates :method_name, **{
77
+ presence: true,
78
+ }
79
+
80
+ # validates :polymorphic, **{}
81
+
82
+ validates :primary_key, **{
83
+ presence: true,
84
+ }
85
+
86
+ validate :method_name_must_be_belongs_to_association, **{
87
+ if: ::Proc.new { |foreign_key| foreign_key.method_name.present? },
88
+ }
89
+
90
+ # @return [Regexp]
91
+ CAPTURE_GROUP_INDEX_REGEXP = ::Regexp.new("#{Regexp.quote('\\')}(0|[1-9][0-9]*)").freeze
92
+
93
+ # @return [String]
94
+ POLYMORPHIC_SEPARATOR = ':'.freeze
95
+
96
+ protected
97
+
98
+ # @param [ActiveRecord::Base, nil] base
99
+ # @return [Object, nil]
100
+ def _get_value(base = nil)
101
+ if base.nil?
102
+ nil
103
+ else
104
+ base.send(:"#{self.method_name}").try { |record|
105
+ record.try(:"#{self.primary_key}").try { |record_id|
106
+ if self.polymorphic?
107
+ record_class_name = record.class.name
108
+ record_table_name = ActsAsTable.adapter.tableize_for(self, record.class.name)
109
+
110
+ if record_table_name.nil? || (record_table_name.is_a?(::String) && record_table_name.include?(POLYMORPHIC_SEPARATOR))
111
+ raise "invalid record table name - #{record_table_name.inspect}"
112
+ end
113
+
114
+ [record_table_name, record_id].join(POLYMORPHIC_SEPARATOR)
115
+ else
116
+ record_id
117
+ end
118
+ }
119
+ }
120
+ end
121
+ end
122
+
123
+ # @param [Object, nil] new_value
124
+ # @return [Object, nil]
125
+ def _modify_set_value_before_default(new_value = nil)
126
+ if new_value.nil?
127
+ new_value
128
+ elsif self.polymorphic?
129
+ # @return [Array<String>]
130
+ array = new_value.split(POLYMORPHIC_SEPARATOR)
131
+
132
+ # @return [String]
133
+ record_table_name = array[0]
134
+
135
+ # @return [String]
136
+ record_id = array[1..-1].join(POLYMORPHIC_SEPARATOR)
137
+
138
+ self.foreign_key_maps.each do |foreign_key_map|
139
+ # @return [Regexp]
140
+ source_value_as_regexp = foreign_key_map.source_value_as_regexp
141
+
142
+ unless (md = source_value_as_regexp.match(record_id)).nil?
143
+ return foreign_key_map.target_value.gsub(CAPTURE_GROUP_INDEX_REGEXP) { |other_md|
144
+ # @return [Integer]
145
+ i = other_md[1].to_i
146
+
147
+ [record_table_name, md[i]].join(POLYMORPHIC_SEPARATOR)
148
+ }
149
+ end
150
+ end
151
+
152
+ [record_table_name, record_id].join(POLYMORPHIC_SEPARATOR)
153
+ else
154
+ self.foreign_key_maps.each do |foreign_key_map|
155
+ # @return [Regexp]
156
+ source_value_as_regexp = foreign_key_map.source_value_as_regexp
157
+
158
+ unless (md = source_value_as_regexp.match(new_value)).nil?
159
+ return foreign_key_map.target_value.gsub(CAPTURE_GROUP_INDEX_REGEXP) { |other_md|
160
+ # @return [Integer]
161
+ i = other_md[1].to_i
162
+
163
+ md[i]
164
+ }
165
+ end
166
+ end
167
+
168
+ new_value
169
+ end
170
+ end
171
+
172
+ # @param [ActiveRecord::Base, nil] base
173
+ # @param [Object, nil] new_value
174
+ # @return [Array<Object>]
175
+ def _set_value(base = nil, new_value = nil)
176
+ if base.nil?
177
+ [
178
+ nil,
179
+ false,
180
+ ]
181
+ elsif self.polymorphic?
182
+ # @return [Object, nil]
183
+ orig_value = _get_value(base)
184
+
185
+ # @return [Class]
186
+ klass = self.record_model.class_name.constantize
187
+
188
+ # @return [ActiveRecord::Reflection::MacroAssociation]
189
+ reflection = klass.reflect_on_association(self.method_name)
190
+
191
+ unless reflection.polymorphic?
192
+ raise "invalid reflection (monomorphic) - #{reflection.inspect}"
193
+ end
194
+
195
+ # @return [Array<String>]
196
+ array = new_value.split(POLYMORPHIC_SEPARATOR)
197
+
198
+ # @return [String]
199
+ record_table_name = array[0]
200
+
201
+ # @return [String]
202
+ record_id = array[1..-1].join(POLYMORPHIC_SEPARATOR)
203
+
204
+ # @return [String]
205
+ record_class_name = ActsAsTable.adapter.classify_for(self, record_table_name)
206
+
207
+ # @return [ActiveRecord::Base, nil]
208
+ new_target_record = record_class_name.try(:constantize).try(:"find_by_#{self.primary_key}", record_id)
209
+
210
+ # @return [Boolean]
211
+ changed = !self.column_model.nil? && !(orig_value.nil? ? new_target_record.nil? : (new_target_record.nil? ? false : (orig_value == new_target_record.send(self.primary_key))))
212
+
213
+ [
214
+ new_target_record.try { |record| [record_table_name, base.send(:"#{self.method_name}=", record).try(self.primary_key)].join(POLYMORPHIC_SEPARATOR) },
215
+ changed,
216
+ ]
217
+ else
218
+ # @return [Object, nil]
219
+ orig_value = _get_value(base)
220
+
221
+ # @return [Class]
222
+ klass = self.record_model.class_name.constantize
223
+
224
+ # @return [ActiveRecord::Reflection::MacroAssociation]
225
+ reflection = klass.reflect_on_association(self.method_name)
226
+
227
+ # @return [ActiveRecord::Base, nil]
228
+ new_target_record = reflection.klass.send(:"find_by_#{self.primary_key}", new_value)
229
+
230
+ # @return [Boolean]
231
+ changed = !self.column_model.nil? && !(orig_value.nil? ? new_target_record.nil? : (new_target_record.nil? ? false : (orig_value == new_target_record.send(self.primary_key))))
232
+
233
+ [
234
+ base.send(:"#{self.method_name}=", new_target_record).try(self.primary_key),
235
+ changed,
236
+ ]
237
+ end
238
+ end
239
+
240
+ private
241
+
242
+ # @param [Class] klass
243
+ # @param [String] column_name
244
+ # @return [Boolean]
245
+ def _column?(klass, column_name)
246
+ klass.column_names.include?(column_name.to_s)
247
+ end
248
+
249
+ # @return [void]
250
+ def method_name_must_be_belongs_to_association
251
+ self.record_model.try { |record_model| record_model.class_name.constantize rescue nil }.try { |klass|
252
+ # @return [ActiveRecord::Reflection::MacroReflection]
253
+ reflection = klass.reflect_on_association(self.method_name)
254
+
255
+ if reflection.nil?
256
+ self.errors.add('method_name', :required)
257
+ elsif reflection.macro == :belongs_to
258
+ if self.polymorphic?
259
+ unless reflection.polymorphic?
260
+ self.errors.add('method_name', :invalid)
261
+ end
262
+
263
+ unless self.primary_key.eql?('id')
264
+ self.errors.add('primary_key', :invalid)
265
+ end
266
+ else
267
+ if reflection.polymorphic?
268
+ self.errors.add('method_name', :invalid)
269
+ end
270
+
271
+ self.primary_key.try { |primary_key|
272
+ if _column?(reflection.klass, primary_key)
273
+ self.default_value.try { |default_value|
274
+ unless reflection.klass.exists?({ primary_key => default_value, })
275
+ self.errors.add('default_value', :required)
276
+ end
277
+ }
278
+
279
+ self.foreign_key_maps.each do |foreign_key_map|
280
+ foreign_key_map.target_value.try { |target_value|
281
+ unless reflection.klass.exists?({ primary_key => target_value, })
282
+ foreign_key_map.errors.add('target_value', :required)
283
+ end
284
+ }
285
+ end
286
+ else
287
+ self.errors.add('primary_key', :required)
288
+ end
289
+ }
290
+ end
291
+ else
292
+ self.errors.add('method_name', :invalid)
293
+ end
294
+ }
295
+
296
+ return
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,121 @@
1
+ module ActsAsTable
2
+ # ActsAsTable foreign key map.
3
+ #
4
+ # @!attribute [rw] extended
5
+ # Returns `true` if the source value for this ActsAsTable foreign key map is an extended regular expression. Otherwise, returns `false`.
6
+ #
7
+ # @return [Boolean]
8
+ # @!attribute [rw] ignore_case
9
+ # Returns `true` if the source value for this ActsAsTable foreign key map is a regular expression that ignores character case. Otherwise, returns `false`.
10
+ #
11
+ # @return [Boolean]
12
+ # @!attribute [rw] multiline
13
+ # Returns `true` if the source value for this ActsAsTable foreign key map is a multiline regular expression. Otherwise, returns `false`.
14
+ #
15
+ # @return [Boolean]
16
+ # @!attribute [rw] position
17
+ # Returns the position of this ActsAsTable foreign key map.
18
+ #
19
+ # @return [Integer]
20
+ # @!attribute [rw] regexp
21
+ # Returns `true` if the source value for this ActsAsTable foreign key map is a regular expression. Otherwise, returns `false`.
22
+ #
23
+ # @return [Boolean]
24
+ # @!attribute [rw] source_value
25
+ # Returns the source value for this ActsAsTable foreign key map.
26
+ #
27
+ # @return [Boolean]
28
+ # @!attribute [rw] target_value
29
+ # Returns the target value for this ActsAsTable foreign key map.
30
+ #
31
+ # @note If the source value for this ActsAsTable foreign key map is a regular expression, then the target value may reference any capture groups.
32
+ #
33
+ # @return [Boolean]
34
+ class ForeignKeyMap < ::ActiveRecord::Base
35
+ # @!parse
36
+ # include ActsAsTable::ValueProvider
37
+ # include ActsAsTable::ValueProviderAssociationMethods
38
+
39
+ self.table_name = ActsAsTable.foreign_key_maps_table
40
+
41
+ # Returns the ActsAsTable foreign key for this map.
42
+ belongs_to :foreign_key, **{
43
+ class_name: 'ActsAsTable::ForeignKey',
44
+ inverse_of: :foreign_key_maps,
45
+ required: true,
46
+ }
47
+
48
+ # validates :extended, **{}
49
+
50
+ # validates :ignore_case, **{}
51
+
52
+ # validates :multiline, **{}
53
+
54
+ validates :position, **{
55
+ numericality: {
56
+ greater_than_or_equal_to: 1,
57
+ only_integer: true,
58
+ },
59
+ presence: true,
60
+ uniqueness: {
61
+ scope: ['foreign_key_id'],
62
+ },
63
+ }
64
+
65
+ # validates :regexp, **{}
66
+
67
+ validates :source_value, **{
68
+ presence: true,
69
+ uniqueness: {
70
+ scope: ['foreign_key_id'],
71
+ },
72
+ }
73
+
74
+ validates :target_value, **{
75
+ presence: true,
76
+ }
77
+
78
+ validate :source_value_as_regexp_must_be_valid, **{
79
+ if: ::Proc.new { |foreign_key_map| foreign_key_map.source_value.present? },
80
+ }
81
+
82
+ # Returns the source value for this ActsAsTable foreign key map as a regular expression.
83
+ #
84
+ # @return [Regexp]
85
+ # @raise [RegexpError]
86
+ def source_value_as_regexp
87
+ # @return [String]
88
+ pattern = self.source_value.to_s
89
+
90
+ unless self.regexp?
91
+ pattern = "\\A#{::Regexp.quote(pattern)}\\z"
92
+ end
93
+
94
+ # @return [Integer]
95
+ flags = {
96
+ :extended => :EXTENDED,
97
+ :ignore_case => :IGNORECASE,
98
+ :multiline => :MULTILINE,
99
+ }.each_pair.inject(0) { |acc, pair|
100
+ method_name, const_name = *pair
101
+
102
+ self.send(:"#{method_name}?") ? (acc | ::Regexp.const_get(const_name, false)) : acc
103
+ }
104
+
105
+ ::Regexp.new(pattern, flags)
106
+ end
107
+
108
+ private
109
+
110
+ # @return [void]
111
+ def source_value_as_regexp_must_be_valid
112
+ begin
113
+ self.source_value_as_regexp
114
+ rescue ::RegexpError
115
+ self.errors.add('source_value', :invalid)
116
+ end
117
+
118
+ return
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,90 @@
1
+ module ActsAsTable
2
+ # ActsAsTable collection macro association.
3
+ #
4
+ # @!attribute [rw] macro
5
+ # Returns the symbolic name for the macro for this ActsAsTable collection macro association.
6
+ #
7
+ # @note The macro must be either `:has_many` or `:has_and_belongs_to_many`.
8
+ #
9
+ # @return [String]
10
+ # @!attribute [rw] method_name
11
+ # Returns the method name for this ActsAsTable collection macro association.
12
+ #
13
+ # @return [String]
14
+ class HasMany < ::ActiveRecord::Base
15
+ # @!parse
16
+ # include ActsAsTable::ValueProvider
17
+ # include ActsAsTable::ValueProviderAssociationMethods
18
+
19
+ self.table_name = ActsAsTable.has_manies_table
20
+
21
+ # Returns the ActsAsTable row model for this ActsAsTable collection macro association.
22
+ belongs_to :row_model, **{
23
+ class_name: 'ActsAsTable::RowModel',
24
+ inverse_of: :has_manies,
25
+ required: true,
26
+ }
27
+
28
+ # Returns the source ActsAsTable record model for this ActsAsTable collection macro association.
29
+ belongs_to :source_record_model, **{
30
+ class_name: 'ActsAsTable::RecordModel',
31
+ inverse_of: :has_manies_as_source,
32
+ required: true,
33
+ }
34
+
35
+ # Returns the targets for this ActsAsTable collection macro association.
36
+ has_many :has_many_targets, -> { order(position: :asc) }, **{
37
+ autosave: true,
38
+ class_name: 'ActsAsTable::HasManyTarget',
39
+ dependent: :destroy,
40
+ foreign_key: 'has_many_id',
41
+ inverse_of: :has_many,
42
+ validate: true,
43
+ }
44
+
45
+ accepts_nested_attributes_for :has_many_targets, **{
46
+ allow_destroy: true,
47
+ }
48
+
49
+ validates :macro, **{
50
+ inclusion: {
51
+ in: ['has_many', 'has_and_belongs_to_many'],
52
+ },
53
+ presence: true,
54
+ }
55
+
56
+ validates :method_name, **{
57
+ presence: true,
58
+ }
59
+
60
+ validate :macro_and_method_name_must_be_valid_association, **{
61
+ if: ::Proc.new { |has_many| has_many.macro.present? && has_many.method_name.present? },
62
+ }
63
+
64
+ private
65
+
66
+ # @return [void]
67
+ def macro_and_method_name_must_be_valid_association
68
+ self.source_record_model.try { |record_model| record_model.class_name.constantize rescue nil }.try { |source_klass|
69
+ # @return [ActiveRecord::Reflection::MacroReflection]
70
+ reflection = source_klass.reflect_on_association(self.method_name)
71
+
72
+ if reflection.nil?
73
+ self.errors.add('method_name', :required)
74
+ elsif self.macro.eql?(reflection.macro.to_s)
75
+ self.has_many_targets.each do |has_many_target|
76
+ has_many_target.record_model.try { |record_model| record_model.class_name.constantize rescue nil }.try { |target_klass|
77
+ unless reflection.klass == target_klass
78
+ has_many_target.errors.add('record_model_id', :invalid)
79
+ end
80
+ }
81
+ end
82
+ else
83
+ self.errors.add('method_name', :invalid)
84
+ end
85
+ }
86
+
87
+ return
88
+ end
89
+ end
90
+ end