2026-06-08 10:27:39 -06:00
|
|
|
# Architecture
|
|
|
|
|
|
|
|
|
|
## File map
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
lib/
|
2026-06-08 20:14:19 -06:00
|
|
|
whetrails.rb Public entry point. Requires engine if Rails is loaded.
|
2026-06-08 10:27:39 -06:00
|
|
|
whetrails/
|
|
|
|
|
version.rb VERSION constant (0.1.0)
|
|
|
|
|
chain.rb Data structures (ModelChain, CallbackEntry, ValidatorEntry)
|
|
|
|
|
inspector.rb Core engine — reads Rails internals, builds ModelChain
|
|
|
|
|
formatter.rb Terminal renderer — formats ModelChain as plain text
|
|
|
|
|
cli.rb CLI logic — boots Rails, resolves model name, calls inspector
|
2026-06-08 20:14:19 -06:00
|
|
|
engine.rb Rails::Engine definition (isolate_namespace Whetrails)
|
|
|
|
|
engine_routes.rb Engine routes — resources :models + root
|
|
|
|
|
|
|
|
|
|
app/
|
|
|
|
|
controllers/whetrails/
|
|
|
|
|
application_controller.rb Base: before_action loads all user-defined AR models
|
|
|
|
|
models_controller.rb index (redirect to first model), show (render chain)
|
|
|
|
|
helpers/whetrails/
|
|
|
|
|
models_helper.rb vscode_uri, short_path, condition_label, validator_type_label
|
|
|
|
|
views/
|
|
|
|
|
layouts/whetrails/
|
|
|
|
|
application.html.erb Layout: sidebar model list + main content area
|
|
|
|
|
whetrails/models/
|
|
|
|
|
index.html.erb Empty — index action always redirects
|
|
|
|
|
show.html.erb Lifecycle timeline + validators table
|
2026-06-08 10:27:39 -06:00
|
|
|
|
|
|
|
|
exe/
|
|
|
|
|
whetrails Executable entry point — calls Whetrails::CLI.run(ARGV)
|
|
|
|
|
|
|
|
|
|
whetrails.gemspec Gem metadata and dependencies
|
|
|
|
|
|
|
|
|
|
app/models/order.rb Development fixture model with sample callbacks/validators
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Data flow
|
|
|
|
|
|
2026-06-08 20:14:19 -06:00
|
|
|
### CLI
|
|
|
|
|
|
2026-06-08 10:27:39 -06:00
|
|
|
```
|
|
|
|
|
exe/whetrails
|
|
|
|
|
→ CLI#run_inspect
|
|
|
|
|
→ CLI#boot_rails loads the host app's config/application.rb
|
|
|
|
|
→ CLI#resolve_model turns "Order" string into Order class constant
|
|
|
|
|
→ Inspector#call
|
|
|
|
|
reads Model._validation_callbacks (before/after_validation)
|
|
|
|
|
reads Model._save_callbacks (before/after_save)
|
|
|
|
|
reads Model._create_callbacks (before/after_create)
|
|
|
|
|
reads Model._update_callbacks (before/after_update)
|
|
|
|
|
reads Model._destroy_callbacks (before/after_destroy)
|
|
|
|
|
reads Model._commit_callbacks (after_commit)
|
|
|
|
|
reads Model._rollback_callbacks (after_rollback)
|
|
|
|
|
reads Model.validators (all validators)
|
|
|
|
|
→ returns ModelChain
|
|
|
|
|
→ Formatter#render prints to stdout
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-08 20:14:19 -06:00
|
|
|
### Engine (browser UI)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
GET /whetrails/
|
|
|
|
|
→ ModelsController#index
|
|
|
|
|
→ eager_load! + discover user models via Object.const_source_location
|
|
|
|
|
→ redirect_to first model
|
|
|
|
|
|
|
|
|
|
GET /whetrails/models/:id (:id is the model name, e.g. "Order")
|
|
|
|
|
→ ModelsController#show
|
|
|
|
|
→ params[:id].constantize → model class
|
|
|
|
|
→ Inspector#inspect_model → ModelChain
|
|
|
|
|
→ renders show.html.erb: lifecycle timeline grouped by event,
|
|
|
|
|
each callback shows filter name, vscode:// source link, condition badges,
|
|
|
|
|
module badge if defined in a concern
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-08 10:27:39 -06:00
|
|
|
## Key Rails API details
|
|
|
|
|
|
|
|
|
|
These are the actual Rails internals the inspector uses — documented here because they are
|
|
|
|
|
undocumented in official Rails docs and took investigation to discover.
|
|
|
|
|
|
|
|
|
|
### Callbacks
|
|
|
|
|
|
|
|
|
|
Each `_<event>_callbacks` method returns an `ActiveSupport::Callbacks::CallbackChain`.
|
|
|
|
|
Each entry in the chain is an `ActiveSupport::Callbacks::Callback` with:
|
|
|
|
|
|
|
|
|
|
| Field | Access | Notes |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Lifecycle stage | `cb.kind` | `:before`, `:after`, or `:around` |
|
|
|
|
|
| Method name | `cb.filter` | Symbol (method name) or Proc |
|
|
|
|
|
| `if:` conditions | `cb.instance_variable_get(:@if)` | Array — `.options` does NOT exist |
|
|
|
|
|
| `unless:` conditions | `cb.instance_variable_get(:@unless)` | Array |
|
|
|
|
|
|
|
|
|
|
`cb.options` does **not** exist in Rails 8 — this is a common mistake from outdated docs.
|
|
|
|
|
Conditions are stored in `@if` / `@unless` instance variables.
|
|
|
|
|
|
|
|
|
|
### Source location
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
# For symbol callbacks:
|
|
|
|
|
model.instance_method(cb.filter).source_location # => ["path/to/file.rb", 42]
|
|
|
|
|
model.instance_method(cb.filter).owner # => the module that defines it
|
|
|
|
|
|
|
|
|
|
# For proc callbacks:
|
|
|
|
|
cb.filter.source_location
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Rails internal filter
|
|
|
|
|
|
|
|
|
|
Two types of internals are filtered out:
|
|
|
|
|
1. Callbacks whose source file is inside a Rails gem (`/gems/activerecord-*` or `/gems/activemodel-*`)
|
|
|
|
|
2. Conditions that are `ActiveSupport::Callbacks::Conditionals::*` instances (Rails adds these
|
|
|
|
|
internally to `after_create` / `after_update` to guard against wrong lifecycle stage)
|
|
|
|
|
|
|
|
|
|
### Validators
|
|
|
|
|
|
|
|
|
|
```ruby
|
|
|
|
|
Model.validators # => Array of validator objects
|
|
|
|
|
validator.class # => ActiveRecord::Validations::PresenceValidator, etc.
|
|
|
|
|
validator.attributes # => [:status]
|
|
|
|
|
validator.options[:if] # => conditions (NOT instance variables — different from callbacks)
|
|
|
|
|
validator.options[:unless]
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-08 20:14:19 -06:00
|
|
|
## Dev setup quirks
|
|
|
|
|
|
|
|
|
|
This repo is both the gem source and the development Rails app. That creates two issues
|
|
|
|
|
that were solved and should not be undone:
|
|
|
|
|
|
|
|
|
|
**1. Zeitwerk autoloading conflict**
|
|
|
|
|
|
|
|
|
|
`config/application.rb` has `config.autoload_lib(ignore: %w[assets tasks whetrails.rb whetrails])`.
|
|
|
|
|
The `whetrails.rb` and `whetrails` entries are critical: without them, Zeitwerk tries to
|
|
|
|
|
autoload `lib/whetrails/chain.rb` as `Whetrails::Chain` (wrong constant name), crashing on
|
|
|
|
|
every request. The gem files use manual `require_relative` — they must not be Zeitwerk-autoloaded.
|
|
|
|
|
|
|
|
|
|
**2. Engine routes conflict**
|
|
|
|
|
|
|
|
|
|
`lib/whetrails/engine.rb` overrides the engine's routing path:
|
|
|
|
|
```ruby
|
|
|
|
|
config.paths.add "config/routes.rb", with: "lib/whetrails/engine_routes.rb"
|
|
|
|
|
```
|
|
|
|
|
Without this, Rails uses `config/routes.rb` as the engine's routes file (same directory),
|
|
|
|
|
which causes `mount Whetrails::Engine` to execute twice and fail with a duplicate route name error.
|
|
|
|
|
Engine routes live in `lib/whetrails/engine_routes.rb` instead.
|
|
|
|
|
|
|
|
|
|
**3. Gem not in Gemfile by default**
|
|
|
|
|
|
|
|
|
|
The gem is listed as `gem "whetrails", path: "."` in `Gemfile`. This is intentional —
|
|
|
|
|
`Bundler.require` must explicitly load the gem so `Whetrails::Engine` is defined before
|
|
|
|
|
`config/routes.rb` tries to mount it.
|
|
|
|
|
|
2026-06-08 10:27:39 -06:00
|
|
|
## Known limitations
|
|
|
|
|
|
|
|
|
|
- **Proc source location**: Proc-based callbacks (e.g. `before_save { do_something }`) report
|
|
|
|
|
source location from Ruby's Proc#source_location, which may point inside a concern or DSL
|
|
|
|
|
wrapper rather than the user's code. This is a Ruby limitation.
|
|
|
|
|
|
|
|
|
|
- **`_callbacks` vs `__callbacks`**: The research phase incorrectly identified `_callbacks`
|
|
|
|
|
as the API. The actual method is `__callbacks` (two underscores) for the hash, or individual
|
|
|
|
|
chain accessors like `_save_callbacks`. The individual accessors are used in this codebase.
|
|
|
|
|
|
|
|
|
|
- **Validator source location**: Not yet implemented. Validators don't expose source location
|
|
|
|
|
directly. A future approach would be to patch `validate` / `validates` at load time to capture it.
|
|
|
|
|
|
|
|
|
|
- **around callbacks**: Registered but the lifecycle semantics are more complex (they wrap the
|
|
|
|
|
operation with yield). Not currently rendered differently from before/after.
|