# 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 `__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] ``` ## 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. ## 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.