acts_as_table 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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