whodunit 0.3.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63c3f8d658cdf1f7b9e7ee954ba759ccddb2d70171f1e755ecbcd2146e314143
4
- data.tar.gz: 33e6c62249309d051b20197d5a4a20df8a51fd9472a05aaf9243f88a2a092a4c
3
+ metadata.gz: ec3f89bd20ea577eb0a740137c86a7927500ccd189e9d9375d18b21c0b36ffdc
4
+ data.tar.gz: 20586d959bc0d25a47319f615f333021d1ebbec331d865c6a18400987f24ef39
5
5
  SHA512:
6
- metadata.gz: b34b74b871fa5a07491bf0a2516131024d3f6b464bc387815ec61b423c3ab32384b394581ca21b1a6d69508b8aa1c3d9b71f77878681ce4f6604b844d86ea2e8
7
- data.tar.gz: 3b790f6f5edb918b609689de6dceee29699ae19d85e214019b6bfc55e7c4ab820907e67e631d7fb68b4bb11289a66d62e5a81857e78c3007428bd81b4e1d938d
6
+ metadata.gz: 5cca573bee0c6d0313d5adf4079e18996d4bc5ecf354b7de39f77e9537b078c67d6122d4a2a7c8c3314ec9da44f40a5ec262a573c06e12b092f86935a951b327
7
+ data.tar.gz: 810bd48aaf36a9605802c62d8f6616d0d68eb63b78a2f92c955c0f06b0cc609f0f6c729274bfc33d5caf42e8263ce8a49d4aa253480db7103dd0c0ea756ce5e1
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ plugins:
6
6
 
7
7
  AllCops:
8
8
  NewCops: enable
9
- TargetRubyVersion: 3.1
9
+ TargetRubyVersion: 3.2
10
10
  Exclude:
11
11
  - 'vendor/**/*'
12
12
  - 'tmp/**/*'
@@ -35,6 +35,7 @@ Metrics/BlockLength:
35
35
 
36
36
  # Allow longer methods in specs and configuration
37
37
  Metrics/MethodLength:
38
+ Max: 15
38
39
  Exclude:
39
40
  - 'spec/**/*'
40
41
 
@@ -114,12 +115,12 @@ Metrics/AbcSize:
114
115
  - 'spec/**/*'
115
116
 
116
117
  Metrics/CyclomaticComplexity:
117
- Max: 8
118
+ Max: 10
118
119
  Exclude:
119
120
  - 'spec/**/*'
120
121
 
121
122
  Metrics/PerceivedComplexity:
122
- Max: 8
123
+ Max: 10
123
124
  Exclude:
124
125
  - 'spec/**/*'
125
126
  Gemspec/DevelopmentDependencies:
data/CHANGELOG.md CHANGED
@@ -5,7 +5,57 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.4.1] - 2026-06-01
9
+
10
+ ### Fixed
11
+
12
+ - Fixed `Whodunit::Stampable` when included on an abstract ActiveRecord base class, such as `ApplicationRecord`. Association setup and model registration are now deferred to concrete subclasses.
13
+ - Preserved existing `inherited` hooks by prepending an internal hook instead of replacing `self.inherited`.
14
+ - Supported multi-level abstract inheritance, such as `ApplicationRecord -> TenantRecord -> Post`, without registering abstract classes.
15
+
16
+ ### Added
17
+
18
+ - Added RSpec regression coverage for abstract-base inclusion, inherited-hook preservation, abstract subclass exclusion, concrete subclass registration, multi-level abstract inheritance, and inherited callback stamping.
19
+
20
+ ## [0.4.0] - 2025-05-20
21
+
22
+ ### Breaking Changes
23
+
24
+ - Dropped support for Ruby 3.1. Minimum supported Ruby version is now **3.2**.
25
+
26
+ ### Fixed
27
+
28
+ - `TableDefinitionExtension`: replaced `included { attr_accessor :_whodunit_stamps_added }` with
29
+ explicit reader/writer methods so the accessor is defined correctly when the module is **prepended**
30
+ (as the Railtie does) rather than included. Previously the accessor was silently missing in
31
+ production, causing `@_whodunit_stamps_added` to be inaccessible via the public API.
32
+
33
+ ### Changed
34
+
35
+ - **Test suite**: eliminated all hand-rolled ActiveRecord and Railtie mocks in favour of genuine
36
+ infrastructure — real `ActiveRecord::Base` subclasses backed by an in-memory SQLite3 database,
37
+ and real `ActiveRecord::ConnectionAdapters::TableDefinition` instances for migration-helper tests.
38
+ Callbacks, dirty tracking, `belongs_to` reflections, and `column_names` now go through actual AR
39
+ code paths rather than no-op stubs.
40
+ - `spec/support/rails_mocks.rb` — emptied; `activesupport` is a runtime dependency and needs no
41
+ re-implementation in tests.
42
+ - `spec/support/test_models.rb` — replaced `MockActiveRecord` stub class with real AR models
43
+ (`WhodunitRecord`, `WhodunitSoftDeleteRecord`) on an in-memory SQLite3 schema; backward-compat
44
+ aliases preserved.
45
+ - `spec/spec_helper.rb` — added `rescue Bundler::GemNotFound` guard on `require "bundler/setup"`
46
+ to allow running specs directly without `bundle exec`; wrapped every example in an AR transaction
47
+ that rolls back on completion; added missing config keys to `ORIGINAL_WHODUNIT_CONFIG`.
48
+ - `spec/whodunit/table_definition_extension_spec.rb` — now tests the real
49
+ `ActiveRecord::ConnectionAdapters::TableDefinition` with `TableDefinitionExtension` prepended,
50
+ rather than an anonymous class that reimplemented the extension logic internally.
51
+ - `spec/whodunit/stampable_spec.rb` — `being_soft_deleted?` tests use real AR dirty tracking via
52
+ genuine attribute writes; callback-integration tests persist records and assert database values
53
+ after `reload`.
54
+ - `spec/whodunit/migration_helpers_spec.rb` — `whodunit_stamps(table_def)` path uses a real
55
+ `TableDefinitionExtension`-prepended `TableDefinition` instance instead of a double.
56
+ - `spec/whodunit/per_model_config_spec.rb`, `railtie_spec.rb`,
57
+ `reverse_associations_integration_spec.rb` — all converted to real AR anonymous subclasses;
58
+ removed stubs for `before_create`, `belongs_to`, and `column_names`.
9
59
 
10
60
  ## [0.3.0] - 2025-01-24
11
61
 
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/whodunit.svg)](https://badge.fury.io/rb/whodunit)
4
4
  [![CI](https://github.com/kanutocd/whodunit/workflows/CI/badge.svg)](https://github.com/kanutocd/whodunit/actions)
5
5
  [![Coverage Status](https://codecov.io/gh/kanutocd/whodunit/branch/main/graph/badge.svg)](https://codecov.io/gh/kanutocd/whodunit)
6
- [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.1.0-ruby.svg)](https://www.ruby-lang.org/en/)
6
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.2-ruby.svg)](https://www.ruby-lang.org/en/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
9
  Lightweight creator/updater/deleter tracking for Rails ActiveRecord models.
@@ -16,7 +16,7 @@ Whodunit provides simple auditing for Rails applications by tracking who created
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Ruby 3.1.1+ (tested on 3.1.1, 3.2.0, 3.3.0, 3.4, 4.0). See the [the ruby-version matrix strategy of the CI workflow](https://github.com/kanutocd/whodunit/blob/main/.github/workflows/ci.yml#L15).
19
+ - Ruby 3.2+ (tested on 3.2, 3.3, 3.4, 4.0). See the [the ruby-version matrix strategy of the CI workflow](https://github.com/kanutocd/whodunit/blob/main/.github/workflows/ci.yml#L15).
20
20
  - Rails 7.2+ (tested on 7.2, 8.2, and edge)
21
21
  - ActiveRecord for database operations
22
22
 
@@ -22,19 +22,19 @@ module Whodunit
22
22
 
23
23
  # Extend ActiveRecord with migration helpers.
24
24
  #
25
- # This initializer adds the MigrationHelpers module to ActiveRecord,
25
+ # This initializer adds the MigrationHelpers module to ActiveRecord::Migration,
26
26
  # making methods like add_whodunit_stamps available in migrations.
27
27
  # It also extends TableDefinition for automatic whodunit_stamps injection.
28
28
  #
29
29
  # @api private
30
30
  initializer "whodunit.extend_active_record" do |_app|
31
31
  ActiveSupport.on_load(:active_record) do
32
- extend Whodunit::MigrationHelpers
32
+ ActiveRecord::Migration.include(Whodunit::MigrationHelpers)
33
33
 
34
- # Extend TableDefinition for automatic whodunit_stamps injection
35
- ActiveRecord::ConnectionAdapters::TableDefinition.include(
36
- Whodunit::TableDefinitionExtension
37
- )
34
+ # We use ActiveRecord::ConnectionAdapters::TableDefinition#timestamps to automatically inject whodunit_stamps
35
+ ActiveRecord::ConnectionAdapters::TableDefinition.prepend(Whodunit::TableDefinitionExtension)
36
+ # Reuse the `whodunit_stamps` flow in Whodunit::MigrationHelpers module
37
+ ActiveRecord::ConnectionAdapters::TableDefinition.extend(Whodunit::MigrationHelpers)
38
38
  end
39
39
  end
40
40
 
@@ -50,12 +50,16 @@ module Whodunit
50
50
  before_destroy :set_whodunit_deleter, if: :deleter_column?
51
51
  before_update :set_whodunit_deleter, if: :being_soft_deleted?
52
52
 
53
- # Set up associations - call on the class
54
- setup_whodunit_associations
55
-
56
- # Register this model for reverse association setup
57
- # This happens immediately, but the check for enabled status is done in register_model
58
- Whodunit.register_model(self)
53
+ if abstract_class?
54
+ install_whodunit_inherited_hook
55
+ else
56
+ # Set up associations - call on the including concrete class.
57
+ setup_whodunit_associations
58
+
59
+ # Register this including model for reverse association setup.
60
+ # This happens immediately, but the check for enabled status is done in register_model.
61
+ Whodunit.register_model(self)
62
+ end
59
63
  end
60
64
 
61
65
  class_methods do # rubocop:disable Metrics/BlockLength
@@ -144,7 +148,46 @@ module Whodunit
144
148
 
145
149
  private
146
150
 
151
+ # Install an inherited hook when Stampable is included on an abstract ActiveRecord
152
+ # base class, such as ApplicationRecord. Associations require concrete table
153
+ # metadata, so setup must be deferred until concrete subclasses are defined.
154
+ #
155
+ # The hook is prepended instead of assigned with `def self.inherited` so existing
156
+ # inherited hooks on the application base class remain intact. When the subclass
157
+ # is also abstract, the same hook is propagated so multi-level abstract
158
+ # inheritance is supported.
159
+ #
160
+ # @return [void]
161
+ # @api private
162
+ def install_whodunit_inherited_hook
163
+ return if instance_variable_defined?(:@whodunit_inherited_hook_installed) &&
164
+ @whodunit_inherited_hook_installed
165
+
166
+ inherited_hook = Module.new do
167
+ def inherited(subclass)
168
+ super
169
+
170
+ subclass.send(:install_whodunit_inherited_hook)
171
+
172
+ return if subclass.abstract_class?
173
+ return if subclass.name.nil?
174
+
175
+ subclass.send(:setup_whodunit_associations)
176
+ Whodunit.register_model(subclass)
177
+ end
178
+ end
179
+
180
+ singleton_class.prepend(inherited_hook)
181
+ @whodunit_inherited_hook_installed = true
182
+ end
183
+
184
+ # Set up associations for concrete ActiveRecord models.
185
+ #
186
+ # @return [void]
187
+ # @api private
147
188
  def setup_whodunit_associations
189
+ return if abstract_class?
190
+
148
191
  setup_creator_association if creator_column_exists? && model_creator_enabled?
149
192
  setup_updater_association if updater_column_exists? && model_updater_enabled?
150
193
  setup_deleter_association if deleter_column_exists? && model_deleter_enabled? && soft_delete_enabled?
@@ -28,30 +28,33 @@ module Whodunit
28
28
  module TableDefinitionExtension
29
29
  extend ActiveSupport::Concern
30
30
 
31
- included do
32
- # Track whether whodunit_stamps have been automatically added
33
- attr_accessor :_whodunit_stamps_added
31
+ def _whodunit_stamps_added
32
+ @_whodunit_stamps_added
33
+ end
34
+
35
+ def _whodunit_stamps_added=(value)
36
+ @_whodunit_stamps_added = value
34
37
  end
35
38
 
36
39
  # Override timestamps to trigger automatic whodunit_stamps injection
37
40
  def timestamps(**options)
41
+ options = options.dup
42
+ skip = options.delete(:skip_whodunit_stamps)
38
43
  result = super
39
44
 
40
- # Auto-inject whodunit_stamps after timestamps if enabled and not already added
41
- if Whodunit.auto_inject_whodunit_stamps &&
42
- !@_whodunit_stamps_added &&
43
- !options[:skip_whodunit_stamps]
45
+ if Whodunit.auto_inject_whodunit_stamps && !_whodunit_stamps_added && !skip
44
46
  whodunit_stamps(include_deleter: :auto)
45
- @_whodunit_stamps_added = true
46
47
  end
47
48
 
48
49
  result
49
50
  end
50
51
 
51
- # Also override whodunit_stamps to track that they've been added
52
- def whodunit_stamps(**options)
53
- @_whodunit_stamps_added = true
54
- super
52
+ # assign true tp the tracker `@_whodunit_stamps_added` before the
53
+ # reuse/call of the `whodunit_stamps` flow from the Whodunit::MigrationHelpers module (see railtie.rb)
54
+ #
55
+ def whodunit_stamps(include_deleter: :auto, creator_type: nil, updater_type: nil, deleter_type: nil)
56
+ self._whodunit_stamps_added = true
57
+ self.class.whodunit_stamps(self, include_deleter:, creator_type:, updater_type:, deleter_type:)
55
58
  end
56
59
  end
57
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whodunit
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whodunit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '3.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: sqlite3
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '2.9'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '2.9'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: yard
197
211
  requirement: !ruby/object:Gem::Requirement
@@ -259,9 +273,9 @@ require_paths:
259
273
  - lib
260
274
  required_ruby_version: !ruby/object:Gem::Requirement
261
275
  requirements:
262
- - - ">"
276
+ - - ">="
263
277
  - !ruby/object:Gem::Version
264
- version: '3.1'
278
+ version: '3.2'
265
279
  required_rubygems_version: !ruby/object:Gem::Requirement
266
280
  requirements:
267
281
  - - ">="