activerecord-polytypes 0.1.1 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce394222154b5e9cd2755f88800d48f83f9fd28df97a6b64626eee208647faa9
4
- data.tar.gz: ac48a24a4971b580ad884f486d6cf6a2fd6ca0b66afcfef0e382385cbc8c5332
3
+ metadata.gz: 233089223a03ffa632ef2091a59fed1f359871068c8dcda702827efa25533920
4
+ data.tar.gz: 12bcd3a00158a38887c60d95682535b9c8b0f2828e3c9b9bbf9bc4be34485221
5
5
  SHA512:
6
- metadata.gz: 6b07b0c0dcc9437ed8e3074662ad48ddf7dbd390c1f1859589841aff7ce940ffafcbfdb4c1ea1d2b0b0fa67909d9f20371a9effba41fb00bade6f63ee21023df
7
- data.tar.gz: 577f2ea8049127bc0a8ba6f23a8cca3d76f125c25820034591a3dca6883cd19d1147d2d41ba9424f8a321378ee62aec717262101e8802bf2a4362e0b5394ad9c
6
+ metadata.gz: 6c350a0b52209ac0da0b7f93e04d795ee9b6fa0e142dd40b8846a5e9fb5a28446b51dfd6a3676579a4bb2e87c179c7aaade3ecd2ee4f3e6965fb65538b566933
7
+ data.tar.gz: 2400da0809d720bd6e1c42448d18af1d3bd2cff5db3cc5ca5e94d98780d2c7c1b25477ffb1d970ef6c854c8151e00258b16736e6557e05701b7dc54e41281c3b
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordPolytypes
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -193,14 +193,14 @@ module ActiveRecordPolytypes
193
193
 
194
194
  case_components_by_type[association.name] = "WHEN #{association.table_name}.#{association.join_primary_key} IS NOT NULL THEN '#{subtype_class_name}'"
195
195
  join_components_by_type[association.name] = if association.belongs_to?
196
- "LEFT JOIN #{association.table_name} ON #{table_name}.#{association.foreign_key} = #{association.table_name}.#{association.join_primary_key}"
196
+ ["LEFT", association.table_name, "%s JOIN %s ON #{table_name}.#{association.foreign_key} = #{association.table_name}.#{association.join_primary_key}"]
197
197
  else
198
- "LEFT JOIN #{association.table_name} ON #{table_name}.#{association.association_primary_key} = #{association.table_name}.#{association.join_primary_key}"
198
+ ["LEFT", association.table_name, "%s JOIN %s ON #{table_name}.#{association.association_primary_key} = #{association.table_name}.#{association.join_primary_key}"]
199
199
  end
200
200
  end
201
201
 
202
202
  # Define a scope `with_subtypes` that enriches the base query with subtype information.
203
- scope :with_subtypes, ->(*typenames){
203
+ scope :with_subtypes, ->(*typenames, **join_sources){
204
204
  select_components, case_components, join_components = typenames.map do |typename|
205
205
  [
206
206
  select_components_by_type[typename],
@@ -209,10 +209,13 @@ module ActiveRecordPolytypes
209
209
  ]
210
210
  end.transpose
211
211
 
212
- select("#{table_name}.*").from(<<~SQL)
212
+ from(<<~SQL)
213
213
  (
214
214
  SELECT #{table_name}.*,#{select_components * ","}, CASE #{case_components * " "} ELSE '#{name}' END AS type
215
- FROM #{table_name} #{join_components * " "}
215
+ FROM #{table_name} #{typenames.map do |typename|
216
+ join_type, join_source, join_string, = join_components_by_type[typename]
217
+ join_string % join_sources.fetch(typename, [join_type, join_source])
218
+ end * " "}
216
219
  ) #{table_name}
217
220
  SQL
218
221
  }
@@ -230,14 +233,21 @@ module ActiveRecordPolytypes
230
233
  # Define a new class inherited from the current class acting as the subtype.
231
234
  subtype_class = supertype_type.const_set(base_type.name, Class.new(subtype_class))
232
235
  subtype_class.class_eval do
233
- attr_reader :inner
234
-
235
236
  # Only include records of this subtype in the default scope.
236
- default_scope ->{ with_subtypes(association.name) }
237
+ default_scope ->{
238
+ with_subtypes(association.name, **{
239
+ association.name => ["INNER", "#{association.table_name}"]
240
+ })
241
+ }
237
242
  # Define callbacks and methods for initializing and saving the inner object.
238
- after_initialize :initialize_inner_object
239
- before_save :save_inner_object_if_changed
240
- after_save :reload, if: :previously_new_record?
243
+ after_initialize :inner
244
+ if association.belongs_to?
245
+ before_save :save_inner_object_if_changed
246
+ else
247
+ after_save :save_inner_object_if_changed
248
+ end
249
+
250
+ after_save :reload_inner!, if: :previously_new_record?
241
251
 
242
252
  # Define attributes and delegation methods for columns inherited from the base type.
243
253
  base_type.reflect_on_all_associations.each do |assoc|
@@ -258,6 +268,26 @@ module ActiveRecordPolytypes
258
268
  self.send(assoc.macro, assoc.name, scope, **assoc.options.except(:inverse_of, :destroy, :as), primary_key: "#{association.name}_#{base_type.primary_key}", foreign_key: assoc.foreign_key, class_name: "::#{assoc.class_name}")
259
269
  end
260
270
  end
271
+
272
+ base_type.enum_index&.each do |_, kwargs, _|
273
+ kwargs = kwargs.dup
274
+ enum_type = kwargs.keys.first
275
+ enum_values = kwargs.delete(enum_type)
276
+ namespaced_attribute = "#{association.name}_#{enum_type}"
277
+ attribute namespaced_attribute, :integer
278
+ kwargs.merge!(namespaced_attribute => enum_values)
279
+ self.enum(**kwargs)
280
+ end
281
+
282
+ base_type.scope_index&.each do |args, kwargs, blk|
283
+ self.scope(args[0], proc do |*scope_args|
284
+ inner_scope = base_type.instance_exec(*scope_args, &args[1]).to_sql
285
+ unscope(:from).with_subtypes(association.name, **{
286
+ association.name => ["INNER", "(#{inner_scope}) #{association.table_name}"]
287
+ })
288
+ end)
289
+ end
290
+
261
291
  base_type.columns.each do |column|
262
292
  column_name = "#{association.name}_#{column.name}"
263
293
  attribute column_name
@@ -280,8 +310,13 @@ module ActiveRecordPolytypes
280
310
  end
281
311
  end
282
312
 
313
+ def inner
314
+ @inner ||= initialize_inner_object
315
+ end
316
+
283
317
  # Initialize the inner object based on the association's attributes or build a new association instance.
284
318
  define_method :initialize_inner_object do
319
+ return if @inner
285
320
  # Prepare attributes for instantiation.
286
321
  @inner_attributes ||= base_type.columns.each_with_object({}) do |c, attrs|
287
322
  attrs[c.name.to_s] = self["#{association.name}_#{c.name}"]
@@ -304,13 +339,14 @@ module ActiveRecordPolytypes
304
339
  # Override `as_json` to include attributes from both the outer and inner objects.
305
340
  define_method :as_json do |options={}|
306
341
  only = base_type.column_names + ["type"] + (options || {}).fetch(:only,[])
307
- outer = super(**(options || {}), only: )
342
+ outer = super(**(options || {}), only:)
308
343
  @inner.as_json(options).merge(outer)
309
344
  end
310
345
 
311
346
  # Save the inner object if it has changed before saving the outer object.
312
347
  def save_inner_object_if_changed
313
- @inner.save if @inner.changed?
348
+ @inner.save if @inner.changed? || @inner.new_record?
349
+ self.errors.merge!(@inner.errors)
314
350
  end
315
351
 
316
352
  # Check if an attribute exists in either the outer or inner object.
@@ -318,14 +354,30 @@ module ActiveRecordPolytypes
318
354
  super || @inner._has_attribute?(attribute)
319
355
  end
320
356
 
321
- # Reload both the outer and inner objects to ensure consistency.
322
- define_method :reload do
323
- super()
324
- @inner.reload
357
+ define_method :_assign_attribute do |name, value|
358
+ inner.has_attribute?(name) ? inner.send(:_assign_attribute, name, value) : super(name, value)
359
+ end
360
+
361
+ define_method :update_column do |key, value|
362
+ return super if self.class.column_names.include?(key.to_s)
363
+ return inner.update_column(key, value) if inner.class.column_names.include?(key.to_s)
364
+ key = key.to_s.gsub(%r{^#{association.name}_}, '')
365
+ return inner.update_column(key.to_sym, value) if inner.class.column_names.include?(key)
366
+ end
367
+
368
+ define_method :reload_inner! do
369
+ inner.reload if inner.persisted?
325
370
  # Update attributes from the reloaded inner object.
326
371
  base_type.columns.each_with_object({}) do |c, attrs|
327
372
  self["#{association.name}_#{c.name}"] = @inner[c.name.to_s]
328
373
  end
374
+ end
375
+
376
+ # Reload both the outer and inner objects to ensure consistency.
377
+ define_method :reload do
378
+ super()
379
+ reload_inner!
380
+
329
381
  self
330
382
  end
331
383
  end
@@ -333,7 +385,30 @@ module ActiveRecordPolytypes
333
385
  end
334
386
  end
335
387
 
388
+ module ActiveRecordPolytypeInterceptors
389
+ module ClassMethods
390
+ attr_accessor :enum_index, :scope_index
391
+
392
+ def enum(*args, **kwargs, &blk)
393
+ @enum_index ||= []
394
+ @enum_index << [args.deep_dup, kwargs.deep_dup, blk]
395
+ puts "Defining enum: #{args} #{kwargs} #{blk} for #{self.name}. #{@enum_index}"
396
+ super
397
+ end
398
+
399
+ def scope(*args, **kwargs, &blk)
400
+ @scope_index ||= []
401
+ @scope_index << [args.dup, kwargs.dup, blk]
402
+ super
403
+ end
404
+ end
405
+
406
+ def self.prepended(mod)
407
+ mod.singleton_class.prepend(ClassMethods)
408
+ end
409
+ end
336
410
  # Hook into ActiveSupport's on_load mechanism to automatically include this functionality into ActiveRecord.
337
411
  ActiveSupport.on_load(:active_record) do
338
412
  include ActiveRecordPolytypes
413
+ prepend ActiveRecordPolytypeInterceptors
339
414
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-polytypes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-16 00:00:00.000000000 Z
11
+ date: 2024-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord