whodunit 0.2.0 → 0.3.0

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: 0b45b1b2f4cfb91e98e2c4656c67305403108af39c5069127d91f0521599bc94
4
- data.tar.gz: 56472d61e9f422643eefe7dca84b7f050fa8d91bb4274bb2354a4e01f04d5ee4
3
+ metadata.gz: 323bc72651528d7bf5e205c0388462078acc718ea39b14b5775517644a2115fc
4
+ data.tar.gz: d20344c823d5716ae218497ca92f61f41c6660c8e8f27a5479b0e52bc64b6b25
5
5
  SHA512:
6
- metadata.gz: c959a0b51f441b28a855b95a537e5983592ed5eb7eef4cbf5eb42c1d4c120849a27ac4113c784f27defa6703939d6cb02a0c2ccc8bd24f0a0508673351407b97
7
- data.tar.gz: a5ca68eebccf5794c9b65521a0c7a232f3178d7aacb6af89c9de3b3bc1365588a6e62a39c52c9821987aaa8735231d289fbc1f28fc7a95be391171d40afbfe31
6
+ metadata.gz: f893afd5307dfa5cdabd58c8de25baa2e979809f9d2c810cdb5dd029a90ebb07f79a61ab24c149e9545159aafef09b3494e2fe15cdee93a6c66e1ae35c44bd2e
7
+ data.tar.gz: 9df8c395516af0720e71fd2a5e8b03a3471f4a85684f8b8a7b274222f74a19feeb1bb9a2b5b3e4795fbcf8938aef769a87e1dd380668838198efad31215d4e3b
data/.rubocop.yml CHANGED
@@ -13,6 +13,7 @@ AllCops:
13
13
  - 'coverage/**/*'
14
14
  - 'bin/**/*'
15
15
  - 'sig/**/*'
16
+ - 'example/**/*'
16
17
 
17
18
  # Layout & Formatting
18
19
  Layout/LineLength:
@@ -37,9 +38,17 @@ Metrics/MethodLength:
37
38
  Exclude:
38
39
  - 'spec/**/*'
39
40
 
41
+ # Allow longer modules for main configuration
42
+ Metrics/ModuleLength:
43
+ Exclude:
44
+ - 'lib/whodunit.rb'
45
+
40
46
  # RSpec specific configurations
41
47
  RSpec/ExampleLength:
42
48
  Max: 15
49
+ Exclude:
50
+ - 'spec/whodunit/reverse_associations_integration_spec.rb'
51
+ - 'spec/whodunit/reverse_associations_spec.rb'
43
52
 
44
53
  RSpec/MultipleExpectations:
45
54
  Max: 5
@@ -53,10 +62,21 @@ RSpec/ContextWording:
53
62
  RSpec/DescribeClass:
54
63
  Exclude:
55
64
  - 'spec/integration/**/*'
65
+ - 'spec/whodunit/reverse_associations_integration_spec.rb'
56
66
 
57
67
  RSpec/VerifiedDoubles:
58
68
  Enabled: false
59
69
 
70
+ RSpec/MessageSpies:
71
+ Exclude:
72
+ - 'spec/whodunit/reverse_associations_integration_spec.rb'
73
+ - 'spec/whodunit/reverse_associations_spec.rb'
74
+ - 'spec/whodunit/stampable_spec.rb'
75
+
76
+ RSpec/SpecFilePathFormat:
77
+ Exclude:
78
+ - 'spec/whodunit/reverse_associations_spec.rb'
79
+
60
80
  # Allow positional boolean arguments for standard Ruby method signatures
61
81
  Style/OptionalBooleanParameter:
62
82
  Exclude:
data/CHANGELOG.md CHANGED
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2025-01-24
11
+
12
+ ### Added
13
+
14
+ - **Automatic Reverse Associations**: When models include `Whodunit::Stampable`, reverse associations are automatically created on the user class (e.g., `user.created_posts`, `user.updated_comments`, `user.deleted_documents`)
15
+ - **Model Registry System**: Tracks models that include Stampable for automatic reverse association setup
16
+ - **Per-Model Reverse Association Control**: Models can disable reverse associations with `disable_whodunit_reverse_associations!`
17
+ - **Reverse Association Configuration**: Global configuration options including `auto_setup_reverse_associations`, `reverse_association_prefix`, and `reverse_association_suffix`
18
+ - **Smart Model Capability Detection**: Automatically detects which associations to create based on model capabilities (creator, updater, deleter, soft-delete)
19
+ - **Custom Column Support**: Reverse associations respect per-model custom column configurations
20
+ - **Manual Setup Methods**: `setup_whodunit_reverse_associations!` and `setup_all_reverse_associations` for manual control
21
+
22
+ ### Changed
23
+
24
+ - **Enhanced Stampable Module**: Now automatically registers models for reverse association setup when included
25
+ - **Improved Configuration**: Extended main configuration module with reverse association settings
26
+ - **Better Association Management**: Prevents duplicate associations and handles edge cases gracefully
27
+
28
+ ### Features
29
+
30
+ - **Zero Configuration**: Reverse associations work automatically with sensible defaults
31
+ - **Thread Safe**: Built on existing thread-safe architecture
32
+ - **Flexible Naming**: Configurable prefixes and suffixes for association names
33
+ - **Comprehensive Testing**: Full test coverage for all reverse association functionality
34
+ - **RuboCop Compliant**: All code follows project style guidelines
35
+
36
+ ## [0.2.1] - 2025-01-21
37
+
38
+ ### Added
39
+
40
+ - **ApplicationRecord Integration**: `whodunit install` now prompts to automatically add `Whodunit::Stampable` to `ApplicationRecord` for convenient all-model stamping
41
+ - **Enhanced Configuration Template**: Detailed explanations and examples for each configuration option in generated initializer
42
+ - **Post-install Message**: Helpful instructions displayed after gem installation via `bundle add whodunit`
43
+ - **Comprehensive Test Coverage**: Full test suite for ApplicationRecord integration with edge case handling
44
+
45
+ ### Changed
46
+
47
+ - **Improved CLI Experience**: More user-friendly prompts and messages throughout the installation process
48
+ - **Better Documentation**: Updated README with corrected installation commands and new ApplicationRecord feature
49
+ - **Code Organization**: Extracted ApplicationRecord integration logic into separate module for better maintainability
50
+
51
+ ### Fixed
52
+
53
+ - **Gemspec Consistency**: Corrected post-install message to show `whodunit install` instead of incorrect Rails generator command
54
+ - **RuboCop Compliance**: Fixed all style issues and reduced complexity across codebase
55
+
10
56
  ## [0.2.0] - 2025-01-20
11
57
 
12
58
  ### Added
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Whodunit
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/whodunit.svg)](https://badge.fury.io/rb/whodunit)
4
+ [![CI](https://github.com/kanutocd/whodunit/workflows/CI/badge.svg)](https://github.com/kanutocd/whodunit/actions)
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/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
3
9
  Lightweight creator/updater/deleter tracking for Rails ActiveRecord models.
4
10
 
5
11
  > **Fun Fact**: The term "whodunit" was coined by literary critic Donald Gordon in 1930 when reviewing a murder mystery novel for _American News of Books_. He described Milward Kennedy's _Half Mast Murder_ as "a satisfactory whodunit" - the first recorded use of this now-famous term for mystery stories! _([Source: Wikipedia](https://en.wikipedia.org/wiki/Whodunit))_
@@ -34,6 +40,26 @@ And then execute:
34
40
 
35
41
  $ bundle install
36
42
 
43
+ ### What's Next?
44
+
45
+ After installation, you have a few options:
46
+
47
+ 1. **Generate Configuration & Setup** (Recommended):
48
+
49
+ ```bash
50
+ whodunit install
51
+ ```
52
+
53
+ This will:
54
+
55
+ - Create `config/initializers/whodunit.rb` with all available configuration options
56
+ - Optionally add `Whodunit::Stampable` to your `ApplicationRecord` for automatic stamping on all models
57
+ - Provide clear next steps for adding stamp columns to your database
58
+
59
+ 2. **Quick Setup**: Jump directly to adding stamp columns to your models (see Quick Start below)
60
+
61
+ 3. **Learn More**: Check the [Complete Documentation](https://kanutocd.github.io/whodunit) for advanced configuration
62
+
37
63
  ## Quick Start
38
64
 
39
65
  ### 1. Add Stamp Columns
@@ -106,6 +132,51 @@ user.updater # => User who last updated this record
106
132
  user.deleter # => User who deleted this record (if soft-delete enabled)
107
133
  ```
108
134
 
135
+ ## Reverse Associations (Automatic)
136
+
137
+ Whodunit automatically sets up reverse associations on your User model when you include `Whodunit::Stampable` in other models:
138
+
139
+ ```ruby
140
+ # When you include Whodunit::Stampable in Post model:
141
+ class Post < ApplicationRecord
142
+ include Whodunit::Stampable
143
+ end
144
+
145
+ # Your User model automatically gets these associations:
146
+ user.created_posts # => Posts created by this user
147
+ user.updated_posts # => Posts last updated by this user
148
+ user.deleted_posts # => Posts deleted by this user (if soft-delete enabled)
149
+ ```
150
+
151
+ ### Configuring Reverse Associations
152
+
153
+ ```ruby
154
+ # config/initializers/whodunit.rb
155
+ Whodunit.configure do |config|
156
+ # Disable automatic reverse associations globally
157
+ config.auto_setup_reverse_associations = false
158
+
159
+ # Customize association names with prefix/suffix
160
+ config.reverse_association_prefix = "whodunit_"
161
+ config.reverse_association_suffix = "_tracked"
162
+ # Results in: user.whodunit_created_posts_tracked
163
+ end
164
+ ```
165
+
166
+ ### Per-Model Control
167
+
168
+ ```ruby
169
+ class Post < ApplicationRecord
170
+ include Whodunit::Stampable
171
+
172
+ # Disable reverse associations for this model only
173
+ disable_whodunit_reverse_associations!
174
+ end
175
+
176
+ # Or manually set up later
177
+ Post.setup_whodunit_reverse_associations!
178
+ ```
179
+
109
180
  ## Soft-Delete Support
110
181
 
111
182
  Whodunit automatically tracks who deleted records when using soft-delete. Simply configure your soft-delete column:
@@ -138,6 +209,11 @@ Whodunit.configure do |config|
138
209
  config.soft_delete_column = :discarded_at # Default: nil
139
210
  config.auto_inject_whodunit_stamps = false # Default: true
140
211
 
212
+ # Reverse association configuration
213
+ config.auto_setup_reverse_associations = false # Default: true
214
+ config.reverse_association_prefix = "track_" # Default: ""
215
+ config.reverse_association_suffix = "_logs" # Default: ""
216
+
141
217
  # Column data type configuration
142
218
  config.column_data_type = :integer # Default: :bigint (applies to all columns)
143
219
  config.creator_column_type = :string # Default: nil (uses column_data_type)
@@ -327,7 +403,7 @@ end
327
403
  | Feature | Whodunit | PaperTrail | Audited |
328
404
  | --------------------- | -------- | ---------- | ------- |
329
405
  | User tracking | ✅ | ✅ | ✅ |
330
- | Change history | | ✅ | ✅ |
406
+ | Change history | Via [whodunit-chronicles](https://github.com/kanutocd/whodunit-chronicles) | ✅ | ✅ |
331
407
  | Performance overhead | None | High | Medium |
332
408
  | Soft-delete detection | ✅ | ❌ | ❌ |
333
409
  | Setup complexity | Low | Medium | Medium |
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ class Generator
5
+ # Handles ApplicationRecord integration functionality
6
+ module ApplicationRecordIntegration
7
+ def self.handle_application_record_integration!
8
+ application_record_file = "app/models/application_record.rb"
9
+
10
+ return unless application_record_exists?(application_record_file)
11
+
12
+ content = File.read(application_record_file)
13
+ return if stampable_already_included?(content)
14
+
15
+ return unless user_wants_application_record_integration?
16
+
17
+ add_stampable_to_application_record!(application_record_file, content)
18
+ end
19
+
20
+ private_class_method def self.application_record_exists?(file_path)
21
+ return true if File.exist?(file_path)
22
+
23
+ puts "⚠️ ApplicationRecord not found at #{file_path}"
24
+ false
25
+ end
26
+
27
+ private_class_method def self.stampable_already_included?(content)
28
+ return false unless content.include?("Whodunit::Stampable")
29
+
30
+ puts "✅ Whodunit::Stampable already included in ApplicationRecord"
31
+ true
32
+ end
33
+
34
+ private_class_method def self.user_wants_application_record_integration?
35
+ puts ""
36
+ puts "🤔 Do you want to include Whodunit::Stampable in ApplicationRecord?"
37
+ puts " This will automatically enable stamping for ALL your models."
38
+ puts " (You can always include it manually in specific models instead)"
39
+ print " Add to ApplicationRecord? (Y/n): "
40
+
41
+ response = $stdin.gets.chomp.downcase
42
+ !%w[n no].include?(response)
43
+ end
44
+
45
+ private_class_method def self.add_stampable_to_application_record!(file_path, content)
46
+ updated_content = try_primary_pattern(content) || try_fallback_pattern(content)
47
+
48
+ if updated_content
49
+ write_updated_application_record!(file_path, updated_content)
50
+ else
51
+ show_manual_integration_message
52
+ end
53
+ end
54
+
55
+ private_class_method def self.try_primary_pattern(content)
56
+ return unless content.match?(/^(\s*class ApplicationRecord < ActiveRecord::Base\s*)$/)
57
+
58
+ content.gsub(
59
+ /^(\s*class ApplicationRecord < ActiveRecord::Base\s*)$/,
60
+ "\\1\n include Whodunit::Stampable"
61
+ )
62
+ end
63
+
64
+ private_class_method def self.try_fallback_pattern(content)
65
+ return unless content.match?(/^(\s*class ApplicationRecord.*\n)/)
66
+
67
+ content.gsub(
68
+ /^(\s*class ApplicationRecord.*\n)/,
69
+ "\\1 include Whodunit::Stampable\n"
70
+ )
71
+ end
72
+
73
+ private_class_method def self.write_updated_application_record!(file_path, content)
74
+ File.write(file_path, content)
75
+ puts "✅ Added Whodunit::Stampable to ApplicationRecord"
76
+ puts " All your models will now automatically include stamping!"
77
+ end
78
+
79
+ private_class_method def self.show_manual_integration_message
80
+ puts "⚠️ Could not automatically modify ApplicationRecord"
81
+ puts " Please manually add: include Whodunit::Stampable"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require_relative "generator/application_record_integration"
4
5
 
5
6
  module Whodunit
6
7
  # Generator for creating Whodunit configuration files
@@ -13,18 +14,48 @@ module Whodunit
13
14
  # Uncomment and modify the options you want to customize.
14
15
 
15
16
  # Whodunit.configure do |config|
17
+ # # User model configuration
18
+ # # Specify which model represents users in your application
16
19
  # config.user_class = 'Account' # Default: 'User'
20
+ # # Use 'Account', 'Admin', etc. if your user model has a different name
21
+ #
22
+ # # Column name configuration
23
+ # # Customize the names of the tracking columns in your database
17
24
  # config.creator_column = :created_by_id # Default: :creator_id
25
+ # # Column that stores who created the record
18
26
  # config.updater_column = :updated_by_id # Default: :updater_id
27
+ # # Column that stores who last updated the record
19
28
  # config.deleter_column = :deleted_by_id # Default: :deleter_id
20
- # config.soft_delete_column = :discarded_at # Default: nil
29
+ # # Column that stores who deleted the record (soft-delete only)
30
+ #
31
+ # # Soft-delete integration
32
+ # # Enable tracking of who deleted records when using soft-delete gems
33
+ # config.soft_delete_column = :discarded_at # Default: nil (disabled)
34
+ # # Set to :deleted_at for Paranoia gem
35
+ # # Set to :discarded_at for Discard gem
36
+ # # Set to your custom soft-delete column name
37
+ # # Set to nil to disable soft-delete tracking
38
+ #
39
+ # # Migration auto-injection
40
+ # # Control whether whodunit stamps are automatically added to new migrations
21
41
  # config.auto_inject_whodunit_stamps = false # Default: true
42
+ # # When true, automatically adds t.whodunit_stamps to create_table migrations
43
+ # # When false, you must manually add t.whodunit_stamps to your migrations
22
44
  #
23
45
  # # Column data type configuration
24
- # config.column_data_type = :integer # Default: :bigint (applies to all columns)
46
+ # # Configure what data types to use for the tracking columns
47
+ # config.column_data_type = :integer # Default: :bigint
48
+ # # Global default for all stamp columns
49
+ # # Common options: :bigint, :integer, :string, :uuid
50
+ #
51
+ # # Individual column type overrides
52
+ # # Override the data type for specific columns (takes precedence over column_data_type)
25
53
  # config.creator_column_type = :string # Default: nil (uses column_data_type)
54
+ # # Useful if your user IDs are strings or UUIDs
26
55
  # config.updater_column_type = :uuid # Default: nil (uses column_data_type)
56
+ # # Set to match your user model's primary key type
27
57
  # config.deleter_column_type = :integer # Default: nil (uses column_data_type)
58
+ # # Only used when soft_delete_column is configured
28
59
  # end
29
60
  RUBY
30
61
 
@@ -36,6 +67,7 @@ module Whodunit
36
67
  ensure_config_directory_exists!(config_dir)
37
68
  handle_existing_file!(config_file)
38
69
  create_initializer_file!(config_file)
70
+ ApplicationRecordIntegration.handle_application_record_integration!
39
71
  show_success_message(config_file)
40
72
  end
41
73
 
@@ -76,7 +108,10 @@ module Whodunit
76
108
  puts "📝 Next steps:"
77
109
  puts " 1. Edit #{config_file} to customize your configuration"
78
110
  puts " 2. Uncomment the options you want to change"
79
- puts " 3. Restart your Rails server to apply changes"
111
+ puts " 3. Add stamp columns to your models with migrations:"
112
+ puts " rails generate migration AddStampsToUsers"
113
+ puts " # Then add: add_whodunit_stamps :users"
114
+ puts " 4. Restart your Rails server to apply changes"
80
115
  puts ""
81
116
  puts "📖 For more information, see: https://github.com/kanutocd/whodunit?tab=readme-ov-file#configuration"
82
117
  end
@@ -52,6 +52,10 @@ module Whodunit
52
52
 
53
53
  # Set up associations - call on the class
54
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)
55
59
  end
56
60
 
57
61
  class_methods do # rubocop:disable Metrics/BlockLength
@@ -82,6 +86,31 @@ module Whodunit
82
86
  @soft_delete_enabled = false
83
87
  end
84
88
 
89
+ # Disable reverse association setup for this model
90
+ # Call this method in the model class to prevent automatic reverse associations
91
+ # @example
92
+ # class Post < ApplicationRecord
93
+ # include Whodunit::Stampable
94
+ # disable_whodunit_reverse_associations!
95
+ # end
96
+ def disable_whodunit_reverse_associations!
97
+ @whodunit_reverse_associations_disabled = true
98
+ # Remove from registered models if already registered
99
+ Whodunit.registered_models.delete(self)
100
+ end
101
+
102
+ # Check if reverse associations are enabled for this model
103
+ # @return [Boolean] true if reverse associations should be set up
104
+ def whodunit_reverse_associations_enabled?
105
+ !@whodunit_reverse_associations_disabled
106
+ end
107
+
108
+ # Manually set up reverse associations for this model
109
+ # Can be called if reverse associations were disabled but you want to set them up later
110
+ def setup_whodunit_reverse_associations!
111
+ Whodunit.setup_reverse_associations_for_model(self) if whodunit_reverse_associations_enabled?
112
+ end
113
+
85
114
  # Get effective configuration value (model override or global default)
86
115
  def whodunit_setting(key)
87
116
  return @whodunit_config_overrides[key] if @whodunit_config_overrides&.key?(key)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Whodunit
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/whodunit.rb CHANGED
@@ -83,6 +83,31 @@ module Whodunit
83
83
  # @return [Symbol, nil] the deleter column data type
84
84
  mattr_accessor :deleter_column_type, default: nil
85
85
 
86
+ # @!group Reverse Association Configuration
87
+
88
+ # Whether to automatically set up reverse associations on the user class (default: true)
89
+ # When enabled, including Whodunit::Stampable in a model will automatically add
90
+ # has_many associations to the user class (e.g., has_many :created_posts)
91
+ # @return [Boolean] auto reverse association setting
92
+ mattr_accessor :auto_setup_reverse_associations, default: true
93
+
94
+ # Prefix for reverse association names (default: "")
95
+ # Used to generate association names like "created_posts", "updated_comments"
96
+ # @return [String] the prefix for reverse association names
97
+ mattr_accessor :reverse_association_prefix, default: ""
98
+
99
+ # Suffix for reverse association names (default: "")
100
+ # Used to generate association names like "posts_created", "comments_updated"
101
+ # @return [String] the suffix for reverse association names
102
+ mattr_accessor :reverse_association_suffix, default: ""
103
+
104
+ # @!group Model Registry
105
+
106
+ # Registry to track models that include Whodunit::Stampable
107
+ # This is used to set up reverse associations on the user class
108
+ # @return [Array<Class>] array of model classes that include Stampable
109
+ mattr_accessor :registered_models, default: []
110
+
86
111
  # Configure Whodunit settings
87
112
  #
88
113
  # @example
@@ -151,6 +176,135 @@ module Whodunit
151
176
  !deleter_column.nil?
152
177
  end
153
178
 
179
+ # @!group Model Registration & Reverse Associations
180
+
181
+ # Register a model class that includes Whodunit::Stampable
182
+ # This is called automatically when Stampable is included
183
+ # @param [Class] model_class the model class to register
184
+ # @return [void]
185
+ def self.register_model(model_class)
186
+ return unless auto_setup_reverse_associations
187
+ return if registered_models.include?(model_class)
188
+ return if model_class.respond_to?(:whodunit_reverse_associations_enabled?) &&
189
+ !model_class.whodunit_reverse_associations_enabled?
190
+
191
+ registered_models << model_class
192
+ setup_reverse_associations_for_model(model_class)
193
+ end
194
+
195
+ # Set up reverse associations on the user class for a specific model
196
+ # @param [Class] model_class the model class to set up reverse associations for
197
+ # @return [void]
198
+ def self.setup_reverse_associations_for_model(model_class)
199
+ return unless auto_setup_reverse_associations
200
+
201
+ user_class_instance = resolve_user_class
202
+ return unless user_class_instance.respond_to?(:has_many)
203
+
204
+ model_plural = model_class.name.underscore.pluralize
205
+
206
+ setup_creator_reverse_association(user_class_instance, model_class, model_plural)
207
+ setup_updater_reverse_association(user_class_instance, model_class, model_plural)
208
+ setup_deleter_reverse_association(user_class_instance, model_class, model_plural)
209
+ end
210
+
211
+ # Set up all reverse associations for all registered models
212
+ # This can be called manually if needed (e.g., after configuration changes)
213
+ # @return [void]
214
+ def self.setup_all_reverse_associations
215
+ registered_models.each do |model_class|
216
+ setup_reverse_associations_for_model(model_class)
217
+ end
218
+ end
219
+
220
+ # Generate a reverse association name based on action and model name
221
+ # @param [String] action the action (created, updated, deleted)
222
+ # @param [String] model_plural the pluralized model name
223
+ # @return [String] the generated association name
224
+ def self.generate_reverse_association_name(action, model_plural)
225
+ "#{reverse_association_prefix}#{action}_#{model_plural}#{reverse_association_suffix}"
226
+ end
227
+
228
+ # Resolve the user class constant
229
+ # @return [Class, nil] the user class or nil if not found
230
+ def self.resolve_user_class
231
+ user_class.constantize
232
+ rescue StandardError
233
+ nil
234
+ end
235
+
236
+ # Set up creator reverse association
237
+ # @param [Class] user_class_instance the user class
238
+ # @param [Class] model_class the model class
239
+ # @param [String] model_plural the pluralized model name
240
+ # @return [void]
241
+ def self.setup_creator_reverse_association(user_class_instance, model_class, model_plural)
242
+ return unless model_class.respond_to?(:model_creator_enabled?) && model_class.model_creator_enabled?
243
+
244
+ association_name = generate_reverse_association_name("created", model_plural)
245
+ setup_user_reverse_association(user_class_instance, association_name, model_class, :creator_id)
246
+ end
247
+
248
+ # Set up updater reverse association
249
+ # @param [Class] user_class_instance the user class
250
+ # @param [Class] model_class the model class
251
+ # @param [String] model_plural the pluralized model name
252
+ # @return [void]
253
+ def self.setup_updater_reverse_association(user_class_instance, model_class, model_plural)
254
+ return unless model_class.respond_to?(:model_updater_enabled?) && model_class.model_updater_enabled?
255
+
256
+ association_name = generate_reverse_association_name("updated", model_plural)
257
+ setup_user_reverse_association(user_class_instance, association_name, model_class, :updater_id)
258
+ end
259
+
260
+ # Set up deleter reverse association
261
+ # @param [Class] user_class_instance the user class
262
+ # @param [Class] model_class the model class
263
+ # @param [String] model_plural the pluralized model name
264
+ # @return [void]
265
+ def self.setup_deleter_reverse_association(user_class_instance, model_class, model_plural)
266
+ return unless model_class.respond_to?(:model_deleter_enabled?) && model_class.model_deleter_enabled?
267
+ return unless model_class.respond_to?(:soft_delete_enabled?) && model_class.soft_delete_enabled?
268
+
269
+ association_name = generate_reverse_association_name("deleted", model_plural)
270
+ setup_user_reverse_association(user_class_instance, association_name, model_class, :deleter_id)
271
+ end
272
+
273
+ # Set up a specific reverse association on the user class
274
+ # @param [Class] user_class_instance the user class
275
+ # @param [String] association_name the name of the association
276
+ # @param [Class] model_class the model class
277
+ # @param [Symbol] foreign_key_column the foreign key column name
278
+ # @return [void]
279
+ def self.setup_user_reverse_association(user_class_instance, association_name, model_class, foreign_key_column)
280
+ actual_foreign_key = resolve_foreign_key(model_class, foreign_key_column)
281
+
282
+ # Check if association already exists to avoid duplicates
283
+ return if user_class_instance.reflect_on_association(association_name.to_sym)
284
+
285
+ user_class_instance.has_many association_name.to_sym,
286
+ class_name: model_class.name,
287
+ foreign_key: actual_foreign_key,
288
+ dependent: :nullify
289
+ end
290
+
291
+ # Resolve the actual foreign key column name from model configuration
292
+ # @param [Class] model_class the model class
293
+ # @param [Symbol] foreign_key_column the default foreign key column
294
+ # @return [Symbol] the actual foreign key column name
295
+ def self.resolve_foreign_key(model_class, foreign_key_column)
296
+ return foreign_key_column unless model_class.respond_to?(:whodunit_setting)
297
+
298
+ column_mapping = {
299
+ creator_id: :creator_column,
300
+ updater_id: :updater_column,
301
+ deleter_id: :deleter_column
302
+ }
303
+
304
+ setting_key = column_mapping[foreign_key_column]
305
+ setting_key ? (model_class.whodunit_setting(setting_key) || foreign_key_column) : foreign_key_column
306
+ end
307
+
154
308
  # Validate that column configuration is valid
155
309
  # @raise [Whodunit::Error] if both creator_column and updater_column are nil
156
310
  def self.validate_column_configuration!
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.2.0
4
+ version: 0.3.0
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: 2025-07-20 00:00:00.000000000 Z
11
+ date: 2025-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -217,6 +217,7 @@ files:
217
217
  - lib/whodunit/controller_methods.rb
218
218
  - lib/whodunit/current.rb
219
219
  - lib/whodunit/generator.rb
220
+ - lib/whodunit/generator/application_record_integration.rb
220
221
  - lib/whodunit/migration_helpers.rb
221
222
  - lib/whodunit/railtie.rb
222
223
  - lib/whodunit/stampable.rb
@@ -234,7 +235,11 @@ metadata:
234
235
  bug_tracker_uri: https://github.com/kanutocd/whodunit/issues
235
236
  documentation_uri: https://kanutocd.github.io/whodunit
236
237
  rubygems_mfa_required: 'true'
237
- post_install_message:
238
+ post_install_message: "\U0001F389 Thanks for installing Whodunit!\n\nWhat's next?\n\n1.
239
+ Generate configuration (recommended):\n whodunit install\n\n2. Add stamp columns
240
+ to your models:\n rails generate migration AddStampsToUsers\n # Then add: add_whodunit_stamps
241
+ :users\n\n3. Include Whodunit::Stampable in your models:\n class User < ApplicationRecord\n
242
+ \ include Whodunit::Stampable\n end\n\n\U0001F4D6 Complete documentation: https://kanutocd.github.io/whodunit\n"
238
243
  rdoc_options: []
239
244
  require_paths:
240
245
  - lib