Files
whetrails/handoff/architecture.md

106 lines
4.3 KiB
Markdown
Raw Normal View History

# Architecture
## File map
```
lib/
whetrails.rb Public entry point. Exposes Whetrails.inspect_model(ModelClass)
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
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
```
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
```
## 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]
```
## 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.