Mark engine as done in overview and build order. Update next-steps to focus on test gap analysis. Add engine file map, data flow, and dev setup quirks (Zeitwerk conflict, engine routes conflict) to architecture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.8 KiB
Architecture
File map
lib/
whetrails.rb Public entry point. Requires engine if Rails is loaded.
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
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
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
CLI
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
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
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
# 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:
- Callbacks whose source file is inside a Rails gem (
/gems/activerecord-*or/gems/activemodel-*) - Conditions that are
ActiveSupport::Callbacks::Conditionals::*instances (Rails adds these internally toafter_create/after_updateto guard against wrong lifecycle stage)
Validators
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]
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:
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.
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. -
_callbacksvs__callbacks: The research phase incorrectly identified_callbacksas 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/validatesat 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.