activerecord-polytypes 0.1.1 → 0.1.3

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