historiographer 4.1.7 → 4.1.8

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: fdfddfba56fcb69bb91c73edcf7c1ddff76adc25f517d4b225bcf87bdd356540
4
- data.tar.gz: 01e398edc375e2bcf17f22a81c4eb06ab2d507483b05db80b8014ea291f2ac82
3
+ metadata.gz: 28891ab31232a7eae8efccd015269069d992eb775259c6a6b9e738da1df69aed
4
+ data.tar.gz: 7a794f1a26351b47d194a0b8d18eb0222d49e9dd7d2b6cdcf90467d94cea62b0
5
5
  SHA512:
6
- metadata.gz: 64b288645843cad4862b06724619ec8c98405745b4d6ba572d00baabbc1ce0d4f706cbf95b42c7e1e6214c870ac0c335fb37ea760f599830037f11a94cf5443e
7
- data.tar.gz: '09663a6452efd27ce2f566b3764e7dd9f29499a61cb5eecc7b488e8fe31270d57a6ceb84489de983cbd4851ac8906cf6140127113ef06751fc6cb19839821906'
6
+ metadata.gz: 16ee4305c1aa8c6f09a13b042185afceea10fd18c0a19f7dba360bcab65a7f1a270dce78e24f15f2a30806fa4f4bcdf5ea6c10ecb81060b3dca02724918d7f5e
7
+ data.tar.gz: e66e7f2a31b5f65689f112ae6b43adeff5e286c42aeb6ed2ffc8931e1a75426dac3f4cb615b2fc06a537c74f070f4f2a245fe4668be8814b8382bd5084336651
@@ -0,0 +1,98 @@
1
+ module Historiographer
2
+ module History
3
+ module InstanceMethods
4
+ def destroy
5
+ false
6
+ end
7
+
8
+ def destroy!
9
+ false
10
+ end
11
+
12
+ def save(*args, **kwargs)
13
+ if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
14
+ false
15
+ else
16
+ super(*args, **kwargs)
17
+ end
18
+ end
19
+
20
+ def save!(*args, **kwargs)
21
+ if persisted? && (changes.keys - %w(history_ended_at snapshot_id)).any?
22
+ false
23
+ else
24
+ super(*args, **kwargs)
25
+ end
26
+ end
27
+
28
+ def snapshot
29
+ raise "Cannot snapshot a history model!"
30
+ end
31
+
32
+ def original_class
33
+ self.class.original_class
34
+ end
35
+
36
+ private
37
+
38
+ def dummy_instance
39
+ return @dummy_instance if @dummy_instance
40
+
41
+ cannot_keep_cols = %w(history_started_at history_ended_at history_user_id snapshot_id)
42
+ cannot_keep_cols += [self.class.inheritance_column.to_sym] if self.original_class.sti_enabled?
43
+ cannot_keep_cols += [self.class.history_foreign_key]
44
+ cannot_keep_cols.map!(&:to_s)
45
+
46
+ attrs = attributes.clone
47
+ attrs[original_class.primary_key] = attrs[self.class.history_foreign_key]
48
+
49
+ instance = original_class.find_or_initialize_by(original_class.primary_key => attrs[original_class.primary_key])
50
+ instance.assign_attributes(attrs.except(*cannot_keep_cols))
51
+
52
+ # Create a module to hold methods from the history class
53
+ history_methods_module = Module.new
54
+
55
+ # Get methods defined directly in the history class, excluding baseline methods
56
+ history_methods = self.class.instance_methods(false) - baseline_methods
57
+
58
+ history_methods.each do |method_name|
59
+ next if instance.singleton_class.method_defined?(method_name)
60
+
61
+ method = self.class.instance_method(method_name)
62
+ history_methods_module.define_method(method_name) do |*args, &block|
63
+ method.bind(self.instance_variable_get(:@_history_instance)).call(*args, &block)
64
+ end
65
+ end
66
+
67
+ instance.singleton_class.prepend(history_methods_module)
68
+
69
+ self.class.reflect_on_all_associations.each do |reflection|
70
+ instance.singleton_class.class_eval do
71
+ define_method(reflection.name) do |*args, &block|
72
+ history_instance = instance.instance_variable_get(:@_history_instance)
73
+ history_instance.send(reflection.name, *args, &block)
74
+ end
75
+ end
76
+ end
77
+
78
+ instance.singleton_class.class_eval do
79
+ define_method(:class) do
80
+ history_instance = instance.instance_variable_get(:@_history_instance)
81
+ history_instance.class
82
+ end
83
+ end
84
+
85
+ instance.instance_variable_set(:@_history_instance, self)
86
+ @dummy_instance = instance
87
+ end
88
+
89
+ def forward_method(method_name, *args, &block)
90
+ if method_name == :class || method_name == 'class'
91
+ self.class
92
+ else
93
+ dummy_instance.send(method_name, *args, &block)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -61,7 +61,7 @@ module Historiographer
61
61
  extend ActiveSupport::Concern
62
62
 
63
63
  included do |base|
64
- clear_validators!
64
+ clear_validators! if respond_to?(:clear_validators!)
65
65
  #
66
66
  # A History class (e.g. RetailerProductHistory) will gain
67
67
  # access to a current scope, returning
@@ -94,6 +94,7 @@ module Historiographer
94
94
 
95
95
  # Store the original class for method delegation
96
96
  class_variable_set(:@@original_class, foreign_class)
97
+ class_variable_set(:@@method_map, {})
97
98
 
98
99
  # Add method_added hook to the original class
99
100
  foreign_class.singleton_class.class_eval do
@@ -102,7 +103,6 @@ module Historiographer
102
103
  alias_method :original_method_added, :method_added
103
104
  end
104
105
 
105
- method_map = Hash.new(0)
106
106
  define_method(:method_added) do |method_name|
107
107
  # Skip if we're already in the process of defining a method
108
108
  return if Thread.current[:defining_historiographer_method]
@@ -121,6 +121,7 @@ module Historiographer
121
121
  return if foreign_class.history_class.method_defined?(method_name)
122
122
 
123
123
  # Define the method in the history class
124
+ foreign_class.history_class.set_method_map(method_name, false)
124
125
  foreign_class.history_class.class_eval do
125
126
  define_method(method_name) do |*args, &block|
126
127
  forward_method(method_name, *args, &block)
@@ -239,9 +240,30 @@ module Historiographer
239
240
  define_history_association(association)
240
241
  end
241
242
 
243
+ def snapshot
244
+ raise "Cannot snapshot a history model!"
245
+ end
246
+
242
247
  end
243
248
 
244
249
  class_methods do
250
+ def method_added(method_name)
251
+ set_method_map(method_name, true)
252
+ end
253
+
254
+ def set_method_map(method_name, is_overridden)
255
+ mm = method_map
256
+ mm[method_name.to_sym] = is_overridden
257
+ class_variable_set(:@@method_map, mm)
258
+ end
259
+
260
+ def method_map
261
+ unless class_variable_defined?(:@@method_map)
262
+ class_variable_set(:@@method_map, {})
263
+ end
264
+ class_variable_get(:@@method_map) || {}
265
+ end
266
+
245
267
  def original_class
246
268
  unless class_variable_defined?(:@@original_class)
247
269
  class_variable_set(:@@original_class, self.name.gsub(/History$/, '').constantize)
@@ -255,38 +277,38 @@ module Historiographer
255
277
  association = original_class.reflect_on_association(association)
256
278
  end
257
279
  assoc_name = association.name
280
+ assoc_module = association.active_record.module_parent
258
281
  assoc_history_class_name = "#{association.class_name}History"
259
- assoc_foreign_key = association.foreign_key
260
282
 
261
- # Skip if the association is already defined
262
- return if method_defined?(assoc_name)
283
+ begin
284
+ assoc_module.const_get(assoc_history_class_name)
285
+ assoc_history_class_name = "#{assoc_module}::#{assoc_history_class_name}" unless assoc_history_class_name.match?(Regexp.new("#{assoc_module}::"))
286
+ rescue
287
+ end
288
+
289
+ assoc_foreign_key = association.foreign_key
263
290
 
264
291
  # Skip through associations to history classes to avoid infinite loops
265
292
  return if association.class_name.end_with?('History')
266
293
 
267
- # We're writing a belongs_to
268
- # The dataset belongs_to the datasource
269
- # dataset#datasource_id => datasource.id
270
- #
271
- # For the history class, we're writing a belongs_to
272
- # the DatasetHistory belongs_to the DatasourceHistory
273
- # dataset_history#datasource_id => datasource_history.easy_ml_datasource_id
274
- #
275
- # The missing piece for us here is whatever DatasourceHistory would call easy_ml_datasource_id (history foreign key?)
294
+ # Always use the history class if it exists
295
+ assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
296
+ assoc_class_name = assoc_class.name
297
+
298
+ # Define the scope to filter by snapshot_id for history associations
299
+ scope = if assoc_class_name.match?(/History/)
300
+ ->(history_instance) { where(snapshot_id: history_instance.snapshot_id) }
301
+ else
302
+ ->(history_instance) { all }
303
+ end
276
304
 
277
305
  case association.macro
278
306
  when :belongs_to
279
- belongs_to assoc_name, ->(history_instance) {
280
- where(snapshot_id: history_instance.snapshot_id)
281
- }, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
307
+ belongs_to assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
282
308
  when :has_one
283
- has_one assoc_name, ->(history_instance) {
284
- where(snapshot_id: history_instance.snapshot_id)
285
- }, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
309
+ has_one assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
286
310
  when :has_many
287
- has_many assoc_name, ->(history_instance) {
288
- where(snapshot_id: history_instance.snapshot_id)
289
- }, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
311
+ has_many assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
290
312
  end
291
313
  end
292
314
  #
@@ -331,11 +353,52 @@ module Historiographer
331
353
 
332
354
  instance = original_class.find_or_initialize_by(original_class.primary_key => attrs[original_class.primary_key])
333
355
  instance.assign_attributes(attrs.except(*cannot_keep_cols))
356
+
357
+ # Filter out any methods that are not overridden on the history class
358
+ history_methods = self.class.instance_methods(false)
359
+ history_class_location = Module.const_source_location(self.class.name).first
360
+ history_methods.select! do |method|
361
+ self.class.instance_method(method).source_location.first == history_class_location
362
+ end
363
+
364
+ history_methods.each do |method_name|
365
+ instance.singleton_class.class_eval do
366
+ define_method(method_name) do |*args, &block|
367
+ history_instance = instance.instance_variable_get(:@_history_instance)
368
+ history_instance.send(method_name, *args, &block)
369
+ end
370
+ end
371
+ end
372
+
373
+ # For each association in the history class
374
+ self.class.reflect_on_all_associations.each do |reflection|
375
+ # Define a method that forwards to the history association
376
+ instance.singleton_class.class_eval do
377
+ define_method(reflection.name) do |*args, &block|
378
+ history_instance = instance.instance_variable_get(:@_history_instance)
379
+ history_instance.send(reflection.name, *args, &block)
380
+ end
381
+ end
382
+ end
383
+
384
+ # Override class method to return history class
385
+ instance.singleton_class.class_eval do
386
+ define_method(:class) do
387
+ history_instance = instance.instance_variable_get(:@_history_instance)
388
+ history_instance.class
389
+ end
390
+ end
391
+
392
+ instance.instance_variable_set(:@_history_instance, self)
334
393
  @dummy_instance = instance
335
394
  end
336
395
 
337
396
  def forward_method(method_name, *args, &block)
338
- dummy_instance.send(method_name, *args, &block)
397
+ if method_name == :class || method_name == 'class'
398
+ self.class
399
+ else
400
+ dummy_instance.send(method_name, *args, &block)
401
+ end
339
402
  end
340
403
  end
341
404
  end
@@ -1,3 +1,3 @@
1
1
  module Historiographer
2
- VERSION = "4.1.7"
2
+ VERSION = "4.1.8"
3
3
  end
@@ -216,21 +216,25 @@ module Historiographer
216
216
  base.singleton_class.prepend(Module.new do
217
217
  def belongs_to(name, scope = nil, **options, &extension)
218
218
  super
219
+ return if is_history_class?
219
220
  history_class.define_history_association(name)
220
221
  end
221
222
 
222
223
  def has_one(name, scope = nil, **options, &extension)
223
224
  super
225
+ return if is_history_class?
224
226
  history_class.define_history_association(name)
225
227
  end
226
228
 
227
229
  def has_many(name, scope = nil, **options, &extension)
228
230
  super
231
+ return if is_history_class?
229
232
  history_class.define_history_association(name)
230
233
  end
231
234
 
232
235
  def has_and_belongs_to_many(name, scope = nil, **options, &extension)
233
236
  super
237
+ return if is_history_class?
234
238
  history_class.define_history_association(name)
235
239
  end
236
240
  end)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: historiographer
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.7
4
+ version: 4.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - brettshollenberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-20 00:00:00.000000000 Z
11
+ date: 2024-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -220,6 +220,20 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: zeitwerk
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
223
237
  description: Append-only histories + chained snapshots of your ActiveRecord tables
224
238
  email: brett.shollenberger@gmail.com
225
239
  executables: []
@@ -233,6 +247,7 @@ files:
233
247
  - lib/historiographer.rb
234
248
  - lib/historiographer/configuration.rb
235
249
  - lib/historiographer/history.rb
250
+ - lib/historiographer/history/instance_methods.rb
236
251
  - lib/historiographer/history_migration.rb
237
252
  - lib/historiographer/history_migration_mysql.rb
238
253
  - lib/historiographer/mysql_migration.rb