layered-rails-skills 2.1.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 +7 -0
- data/hyperdrive.yml +24 -0
- data/layered-rails/skills/layered-rails/SKILL.md +293 -0
- data/layered-rails/skills/layered-rails/examples/authorization-to-policy.md +79 -0
- data/layered-rails/skills/layered-rails/examples/callbacks-to-service.md +72 -0
- data/layered-rails/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
- data/layered-rails/skills/layered-rails/examples/current-from-model.md +63 -0
- data/layered-rails/skills/layered-rails/examples/god-object-decomposition.md +81 -0
- data/layered-rails/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
- data/layered-rails/skills/layered-rails/examples/query-to-query-object.md +63 -0
- data/layered-rails/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
- data/layered-rails/skills/layered-rails/references/anti-patterns/testing.md +37 -0
- data/layered-rails/skills/layered-rails/references/core/architecture-layers.md +212 -0
- data/layered-rails/skills/layered-rails/references/core/extraction-signals.md +314 -0
- data/layered-rails/skills/layered-rails/references/core/specification-test.md +229 -0
- data/layered-rails/skills/layered-rails/references/gems/action-policy.md +437 -0
- data/layered-rails/skills/layered-rails/references/gems/active-agent.md +275 -0
- data/layered-rails/skills/layered-rails/references/gems/active-delivery.md +221 -0
- data/layered-rails/skills/layered-rails/references/gems/active-job-performs.md +171 -0
- data/layered-rails/skills/layered-rails/references/gems/alba.md +257 -0
- data/layered-rails/skills/layered-rails/references/gems/anyway-config.md +212 -0
- data/layered-rails/skills/layered-rails/references/gems/archspec.md +208 -0
- data/layered-rails/skills/layered-rails/references/gems/rubanok.md +246 -0
- data/layered-rails/skills/layered-rails/references/gems/view-component.md +223 -0
- data/layered-rails/skills/layered-rails/references/gems/workflow.md +305 -0
- data/layered-rails/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
- data/layered-rails/skills/layered-rails/references/patterns/concerns.md +381 -0
- data/layered-rails/skills/layered-rails/references/patterns/filter-objects.md +222 -0
- data/layered-rails/skills/layered-rails/references/patterns/form-objects.md +267 -0
- data/layered-rails/skills/layered-rails/references/patterns/policy-objects.md +298 -0
- data/layered-rails/skills/layered-rails/references/patterns/presenters.md +257 -0
- data/layered-rails/skills/layered-rails/references/patterns/query-objects.md +208 -0
- data/layered-rails/skills/layered-rails/references/patterns/repositories.md +382 -0
- data/layered-rails/skills/layered-rails/references/patterns/serializers.md +256 -0
- data/layered-rails/skills/layered-rails/references/patterns/service-objects.md +170 -0
- data/layered-rails/skills/layered-rails/references/patterns/state-machines.md +368 -0
- data/layered-rails/skills/layered-rails/references/patterns/value-objects.md +267 -0
- data/layered-rails/skills/layered-rails/references/topics/ai-integration.md +372 -0
- data/layered-rails/skills/layered-rails/references/topics/authorization.md +398 -0
- data/layered-rails/skills/layered-rails/references/topics/callbacks.md +336 -0
- data/layered-rails/skills/layered-rails/references/topics/configuration.md +389 -0
- data/layered-rails/skills/layered-rails/references/topics/current-attributes.md +307 -0
- data/layered-rails/skills/layered-rails/references/topics/instrumentation.md +386 -0
- data/layered-rails/skills/layered-rails/references/topics/notifications.md +373 -0
- data/layered-rails/skills/layered-rails/references/topics/view-components.md +454 -0
- data/layered-rails/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
- data/layered-rails/skills/layered-rails/workflows/analyze-gods.md +279 -0
- data/layered-rails/skills/layered-rails/workflows/analyze-services.md +1444 -0
- data/layered-rails/skills/layered-rails/workflows/analyze.md +386 -0
- data/layered-rails/skills/layered-rails/workflows/archspec.md +63 -0
- data/layered-rails/skills/layered-rails/workflows/plan.md +254 -0
- data/layered-rails/skills/layered-rails/workflows/review.md +473 -0
- data/layered-rails/skills/layered-rails/workflows/spec-test.md +282 -0
- data/rails-hyperdrive/layered-rails/templates/layered-rails/SKILL.md.erb +298 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c25676f1a558e964f13864e74bff7a008b2dda1dae74401f64ebd6aef87d23bd
|
|
4
|
+
data.tar.gz: faac0c99678d18115c6fc7023766e97a7462d9418615f983227a81de67362192
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2573cf19ab75922d1e50f99a4b889aa1521f21a52f7b0b69c97e2c2d4e3bf87f8007dc605e44f3f74cd24fea6ef8568f059980748f6a2df6ada5de804d92e4f4
|
|
7
|
+
data.tar.gz: 488f18a1a8ea43c55d3b717c183404b0e578994ddfa59d2296e46c6c8a900863d12daaefabc24ea6690ef6f28609a9c78d47f2ace3bcf736bbe7343a6ecc4ef7
|
data/hyperdrive.yml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
gem: railties
|
|
2
|
+
versions: ">= 7.0"
|
|
3
|
+
|
|
4
|
+
skills:
|
|
5
|
+
layered-rails:
|
|
6
|
+
conditional:
|
|
7
|
+
references/gems/action-policy.md:
|
|
8
|
+
gem: "action_policy"
|
|
9
|
+
references/gems/view-component.md:
|
|
10
|
+
gem: "view_component"
|
|
11
|
+
references/gems/anyway-config.md:
|
|
12
|
+
gem: "anyway_config"
|
|
13
|
+
references/gems/active-delivery.md:
|
|
14
|
+
gem: "active_delivery"
|
|
15
|
+
references/gems/alba.md:
|
|
16
|
+
gem: "alba"
|
|
17
|
+
references/gems/workflow.md:
|
|
18
|
+
gem: "workflow, workflow-activerecord"
|
|
19
|
+
references/gems/rubanok.md:
|
|
20
|
+
gem: "rubanok"
|
|
21
|
+
references/gems/active-agent.md:
|
|
22
|
+
gem: "activeagent"
|
|
23
|
+
references/gems/active-job-performs.md:
|
|
24
|
+
gem: "active_job-performs"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layered-rails
|
|
3
|
+
description: Write, refactor, and review Rails code using layered architecture principles from "Layered Design for Ruby on Rails Applications". Use when writing or refactoring Rails code — models, controllers, services, jobs, mailers, policies, forms, query objects, presenters, view components, state machines, serializers, or AI/LLM features — to apply correct patterns and avoid layer violations; and when reviewing Rails code, PRs, or diffs for layer violations, fat controllers/models, anemic models, callback misuse, god objects, or specification-test failures. Triggers on "layered design", "architecture layers", "abstraction layer", "specification test", "layer violation", "fat controller/model", "god object", "anemic model", "extract service/callback/policy/concern", "service object", "form/policy/query/value object", "presenter", "view component", "state machine", "Active Delivery", "callback scoring", "Rails refactor/review", "Rails patterns/best practices", "archspec", "architecture linter/enforcement".
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Grep
|
|
6
|
+
- Glob
|
|
7
|
+
- Read
|
|
8
|
+
- Task
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Layered Rails
|
|
12
|
+
|
|
13
|
+
Design and review Rails applications using layered architecture principles.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Rails applications are organized into four architecture layers with **unidirectional data flow**:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────┐
|
|
21
|
+
│ PRESENTATION LAYER │
|
|
22
|
+
│ Controllers, Views, Channels, Jobs* │
|
|
23
|
+
│ Forms, Filters, Presenters, Serializers │
|
|
24
|
+
└─────────────────────────────────────────┘
|
|
25
|
+
↓
|
|
26
|
+
┌─────────────────────────────────────────┐
|
|
27
|
+
│ APPLICATION LAYER │
|
|
28
|
+
│ Services, Policies, Mailers, Deliveries │
|
|
29
|
+
└─────────────────────────────────────────┘
|
|
30
|
+
↓
|
|
31
|
+
┌─────────────────────────────────────────┐
|
|
32
|
+
│ DOMAIN LAYER │
|
|
33
|
+
│ Models, Value Objects, Query Objects │
|
|
34
|
+
└─────────────────────────────────────────┘
|
|
35
|
+
↓
|
|
36
|
+
┌─────────────────────────────────────────┐
|
|
37
|
+
│ INFRASTRUCTURE LAYER │
|
|
38
|
+
│ Active Record, API Clients, Storage │
|
|
39
|
+
└─────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
\* Jobs are **internal inbound** entry points: design-wise they follow the same rules as controllers, minus authentication and user-input handling.
|
|
43
|
+
|
|
44
|
+
**Core Rule:** Lower layers must never depend on higher layers.
|
|
45
|
+
|
|
46
|
+
See [Architecture Layers Reference](references/core/architecture-layers.md) for the full layer responsibilities and the Four Rules deep-dive.
|
|
47
|
+
|
|
48
|
+
## What this skill is for
|
|
49
|
+
|
|
50
|
+
Use this skill when:
|
|
51
|
+
|
|
52
|
+
1. **Analyzing a codebase** — apply the [architecture analysis](workflows/analyze.md) for a full audit, or zoom in with the [service-layer audit](workflows/analyze-services.md), [callback analysis](workflows/analyze-callbacks.md), or [god-object analysis](workflows/analyze-gods.md).
|
|
53
|
+
2. **Reviewing code changes** — run the [code review workflow](workflows/review.md) on a diff, file, or branch.
|
|
54
|
+
3. **Running the specification test** — use the [spec-test workflow](workflows/spec-test.md) on a single file or directory to evaluate whether code belongs in its current layer.
|
|
55
|
+
4. **Planning gradual adoption** — generate a phased roadmap with the [layerification plan workflow](workflows/plan.md), focused on a goal like "introduce authorization" or "decompose god objects."
|
|
56
|
+
5. **Planning a feature** — I'll apply the layered principles below to whichever code you're about to write.
|
|
57
|
+
6. **Implementing a specific pattern** — authorization, notifications, view components, AI integration, etc. — see the Pattern Catalog and Topic References below.
|
|
58
|
+
|
|
59
|
+
In Claude Code with this skill installed as a plugin (`/plugin install layered-rails@palkan-skills`), each workflow above is also reachable as a slash command — see [Slash Commands](#slash-commands). Natural-language requests ("review this file with layered-rails", "run the specification test on `app/models/order.rb`") work in any environment that has the skill loaded.
|
|
60
|
+
|
|
61
|
+
## Workflows
|
|
62
|
+
|
|
63
|
+
Reusable procedures bundled inside this skill. Read the file and apply it to the target code:
|
|
64
|
+
|
|
65
|
+
- [Architecture analysis](workflows/analyze.md) — full layered-architecture audit of a Rails codebase
|
|
66
|
+
- [Code review](workflows/review.md) — review a diff or file set for layer violations
|
|
67
|
+
- [Specification test](workflows/spec-test.md) — evaluate whether code belongs in its current layer
|
|
68
|
+
- [Service-layer audit](workflows/analyze-services.md) — deep audit of `app/services/` and service-like classes (per-cluster proposals, contracts, layer hygiene)
|
|
69
|
+
- [Callback analysis](workflows/analyze-callbacks.md) — score Active Record callbacks and find extraction candidates
|
|
70
|
+
- [God-object analysis](workflows/analyze-gods.md) — identify oversized models and recommend decomposition
|
|
71
|
+
- [Gradual layerification plan](workflows/plan.md) — incremental roadmap for adopting layered patterns
|
|
72
|
+
- [ArchSpec setup](workflows/archspec.md) — generate and verify a tailored `Archspec.rb` enforcing the layer boundaries in CI
|
|
73
|
+
|
|
74
|
+
## Core Principles
|
|
75
|
+
|
|
76
|
+
### The Four Rules
|
|
77
|
+
|
|
78
|
+
1. **Unidirectional Data Flow** - Data flows top-to-bottom only
|
|
79
|
+
2. **No Reverse Dependencies** - Lower layers never depend on higher layers
|
|
80
|
+
3. **Abstraction Boundaries** - Each abstraction belongs to exactly one layer
|
|
81
|
+
4. **Minimize Connections** - Fewer inter-layer connections = looser coupling
|
|
82
|
+
|
|
83
|
+
### Common Violations
|
|
84
|
+
|
|
85
|
+
| Violation | Example | Fix |
|
|
86
|
+
|-----------|---------|-----|
|
|
87
|
+
| Model uses Current | `Current.user` in model | Pass user as explicit parameter |
|
|
88
|
+
| Service accepts request | `param :request` in service | Extract value object from request |
|
|
89
|
+
| Controller has business logic | Pricing calculations in action | Extract to service or model |
|
|
90
|
+
| Anemic models | All logic in services | Keep domain logic in models |
|
|
91
|
+
|
|
92
|
+
| Category | Reference |
|
|
93
|
+
|----------|-----------|
|
|
94
|
+
| Layer violations (Current in models, request in services, notifications in models, business logic in controllers) | [layer-violations.md](references/anti-patterns/layer-violations.md) |
|
|
95
|
+
| Service objects (anemic models, bag of random objects, premature abstraction) | [service-objects.md](references/anti-patterns/service-objects.md) |
|
|
96
|
+
| Callbacks (operation callbacks, skip callbacks, control flags) | [callbacks.md](references/anti-patterns/callbacks.md) |
|
|
97
|
+
| Concerns (code-slicing, overgrown) | [concerns.md](references/anti-patterns/concerns.md) |
|
|
98
|
+
| Helpers (HTML construction in helpers) | [helpers.md](references/anti-patterns/helpers.md) |
|
|
99
|
+
| Jobs (anemic jobs) | [jobs.md](references/anti-patterns/jobs.md) |
|
|
100
|
+
| Testing (testing wrong layer) | [testing.md](references/anti-patterns/testing.md) |
|
|
101
|
+
|
|
102
|
+
### The Specification Test
|
|
103
|
+
|
|
104
|
+
> If the specification of an object describes features beyond the primary responsibility of its abstraction layer, such features should be extracted into lower layers.
|
|
105
|
+
|
|
106
|
+
**How to apply:**
|
|
107
|
+
1. List responsibilities the code handles
|
|
108
|
+
2. Evaluate each against the layer's primary concern
|
|
109
|
+
3. Extract misplaced responsibilities to appropriate layers
|
|
110
|
+
|
|
111
|
+
See [Specification Test Reference](references/core/specification-test.md) for detailed guide.
|
|
112
|
+
|
|
113
|
+
## Pattern Catalog
|
|
114
|
+
|
|
115
|
+
| Pattern | Layer | Use When | Reference |
|
|
116
|
+
|---------|-------|----------|-----------|
|
|
117
|
+
| Service Object | Application | Orchestrating domain operations | [service-objects.md](references/patterns/service-objects.md) |
|
|
118
|
+
| Query Object | Domain | Complex, reusable queries | [query-objects.md](references/patterns/query-objects.md) |
|
|
119
|
+
| Form Object | Presentation | Multi-model forms, complex validation | [form-objects.md](references/patterns/form-objects.md) |
|
|
120
|
+
| Filter Object | Presentation | Request parameter transformation | [filter-objects.md](references/patterns/filter-objects.md) |
|
|
121
|
+
| Presenter | Presentation | View-specific logic, multiple models | [presenters.md](references/patterns/presenters.md) |
|
|
122
|
+
| Serializer | Presentation | API response formatting | [serializers.md](references/patterns/serializers.md) |
|
|
123
|
+
| Policy Object | Application | Authorization decisions | [policy-objects.md](references/patterns/policy-objects.md) |
|
|
124
|
+
| Value Object | Domain | Immutable, identity-less concepts | [value-objects.md](references/patterns/value-objects.md) |
|
|
125
|
+
| Collaborator Object | Domain | A slice of one model's behavior in a typed delegate | [collaborator-objects.md](references/patterns/collaborator-objects.md) |
|
|
126
|
+
| State Machine | Domain | States, events, transitions | [state-machines.md](references/patterns/state-machines.md) |
|
|
127
|
+
| Concern | Domain | Shared behavioral extraction | [concerns.md](references/patterns/concerns.md) |
|
|
128
|
+
| Repository | Domain | **Last resort** — returning custom domain objects mapped from AR data, after AR scopes (simple) and query objects (query building) are insufficient | [repositories.md](references/patterns/repositories.md) |
|
|
129
|
+
|
|
130
|
+
### Pattern Selection Guide
|
|
131
|
+
|
|
132
|
+
**"Where should this code go?"**
|
|
133
|
+
|
|
134
|
+
| If you have... | Consider... |
|
|
135
|
+
|----------------|-------------|
|
|
136
|
+
| Complex multi-model form | Form Object |
|
|
137
|
+
| Request parameter filtering/transformation | Filter Object |
|
|
138
|
+
| View-specific formatting | Presenter |
|
|
139
|
+
| Complex database query used in multiple places | Query Object |
|
|
140
|
+
| Business operation spanning multiple models | Service Object (as waiting room) |
|
|
141
|
+
| Authorization rules | Policy Object |
|
|
142
|
+
| Multi-channel notifications | Delivery Object (Active Delivery) |
|
|
143
|
+
|
|
144
|
+
**Remember:** Services are a "waiting room" for code until proper abstractions emerge. Don't let `app/services` become a bag of random objects.
|
|
145
|
+
|
|
146
|
+
## Refactoring Scenarios
|
|
147
|
+
|
|
148
|
+
Canonical before/after transformations for the most common layerification moves. The [layerification plan workflow](workflows/plan.md) uses these as reference templates when proposing phases.
|
|
149
|
+
|
|
150
|
+
| Scenario | Goal area | Reference |
|
|
151
|
+
|----------|-----------|-----------|
|
|
152
|
+
| Extract callbacks to service | callbacks, after_create chains | [callbacks-to-service.md](examples/callbacks-to-service.md) |
|
|
153
|
+
| Extract authorization to policy | authorization, permissions | [authorization-to-policy.md](examples/authorization-to-policy.md) |
|
|
154
|
+
| Extract query logic to query object | complex scopes, reporting queries | [query-to-query-object.md](examples/query-to-query-object.md) |
|
|
155
|
+
| Extract Current from model | Current.* in domain | [current-from-model.md](examples/current-from-model.md) |
|
|
156
|
+
| Decompose god object with associated objects | god model, large User/Account | [god-object-decomposition.md](examples/god-object-decomposition.md) |
|
|
157
|
+
| Replace implicit state machine | timestamp-based status | [implicit-to-explicit-state-machine.md](examples/implicit-to-explicit-state-machine.md) |
|
|
158
|
+
| Extract view logic to presenter | template logic, formatting | [view-logic-to-presenter.md](examples/view-logic-to-presenter.md) |
|
|
159
|
+
| Form object for complex input | fat controllers, multi-model forms | [complex-input-to-form-object.md](examples/complex-input-to-form-object.md) |
|
|
160
|
+
|
|
161
|
+
## Slash Commands
|
|
162
|
+
|
|
163
|
+
These slash commands are available **only when this skill is installed as a Claude Code plugin** (`/plugin install layered-rails@palkan-skills`). When the skill is installed via [skills.sh](https://skills.sh/) or any other path that delivers `skills/layered-rails/` without the surrounding plugin, the commands won't be present — invoke the corresponding workflow directly (see [Workflows](#workflows)) or just ask in plain language.
|
|
164
|
+
|
|
165
|
+
| Command | Workflow | Purpose |
|
|
166
|
+
|---------|----------|---------|
|
|
167
|
+
| `/layered-rails:review` | [review](workflows/review.md) | Review code changes from a layered architecture perspective |
|
|
168
|
+
| `/layered-rails:spec-test` | [spec-test](workflows/spec-test.md) | Run specification test on specific files |
|
|
169
|
+
| `/layered-rails:analyze` | [analyze](workflows/analyze.md) | Full codebase abstraction-layer analysis |
|
|
170
|
+
| `/layered-rails:analyze-services` | [analyze-services](workflows/analyze-services.md) | Audit `app/services/` and service-like classes — conventions, clusters, layer hygiene, test consequences |
|
|
171
|
+
| `/layered-rails:analyze-callbacks` | [analyze-callbacks](workflows/analyze-callbacks.md) | Score model callbacks, find extraction candidates |
|
|
172
|
+
| `/layered-rails:analyze-gods` | [analyze-gods](workflows/analyze-gods.md) | Find god objects via churn × complexity |
|
|
173
|
+
| `/layered-rails:plan [goal]` | [plan](workflows/plan.md) | Plan gradual adoption of layered patterns |
|
|
174
|
+
| `/layered-rails:archspec` | [archspec](workflows/archspec.md) | Generate and verify an `Archspec.rb` enforcing layer boundaries in CI |
|
|
175
|
+
|
|
176
|
+
## Topic References
|
|
177
|
+
|
|
178
|
+
For deep dives on specific topics:
|
|
179
|
+
|
|
180
|
+
| Topic | Reference |
|
|
181
|
+
|-------|-----------|
|
|
182
|
+
| Authorization (RBAC, ABAC, policies) | [authorization.md](references/topics/authorization.md) |
|
|
183
|
+
| Notifications (multi-channel delivery) | [notifications.md](references/topics/notifications.md) |
|
|
184
|
+
| View Components | [view-components.md](references/topics/view-components.md) |
|
|
185
|
+
| AI Integration (LLM, agents, RAG, MCP) | [ai-integration.md](references/topics/ai-integration.md) |
|
|
186
|
+
| Configuration | [configuration.md](references/topics/configuration.md) |
|
|
187
|
+
| Callbacks (scoring, extraction) | [callbacks.md](references/topics/callbacks.md) |
|
|
188
|
+
| Current Attributes | [current-attributes.md](references/topics/current-attributes.md) |
|
|
189
|
+
| Instrumentation (logging, metrics) | [instrumentation.md](references/topics/instrumentation.md) |
|
|
190
|
+
|
|
191
|
+
## Gem References
|
|
192
|
+
|
|
193
|
+
For library-specific guidance:
|
|
194
|
+
|
|
195
|
+
| Gem | Purpose | Reference |
|
|
196
|
+
|-----|---------|-----------|
|
|
197
|
+
| action_policy | Authorization framework | [action-policy.md](references/gems/action-policy.md) |
|
|
198
|
+
| view_component | Component framework | [view-component.md](references/gems/view-component.md) |
|
|
199
|
+
| anyway_config | Typed configuration | [anyway-config.md](references/gems/anyway-config.md) |
|
|
200
|
+
| active_delivery | Multi-channel notifications | [active-delivery.md](references/gems/active-delivery.md) |
|
|
201
|
+
| alba | JSON serialization | [alba.md](references/gems/alba.md) |
|
|
202
|
+
| workflow | State machines | [workflow.md](references/gems/workflow.md) |
|
|
203
|
+
| rubanok | Filter/transformation DSL | [rubanok.md](references/gems/rubanok.md) |
|
|
204
|
+
| active_agent | AI agent framework | [active-agent.md](references/gems/active-agent.md) |
|
|
205
|
+
| active_job-performs | Eliminate anemic jobs | [active-job-performs.md](references/gems/active-job-performs.md) |
|
|
206
|
+
| archspec | Enforce layer boundaries in CI (reference `Archspec.rb` config) | [archspec.md](references/gems/archspec.md) |
|
|
207
|
+
|
|
208
|
+
## Extraction Signals
|
|
209
|
+
|
|
210
|
+
**When to extract from models:**
|
|
211
|
+
|
|
212
|
+
| Signal | Metric | Action |
|
|
213
|
+
|--------|--------|--------|
|
|
214
|
+
| God object | High churn × complexity | Decompose into concerns, delegates, or separate models |
|
|
215
|
+
| Operation callback | Score 1-2/5 | Extract to service or event handler |
|
|
216
|
+
| Code-slicing concern | Groups by artifact type | Convert to behavioral concern or extract |
|
|
217
|
+
| Current dependency | Model reads Current.* | Pass as explicit parameter |
|
|
218
|
+
|
|
219
|
+
**Callback Scoring:**
|
|
220
|
+
| Type | Score | Keep? |
|
|
221
|
+
|------|-------|-------|
|
|
222
|
+
| Transformer (compute values) | 5/5 | Yes |
|
|
223
|
+
| Normalizer (sanitize input) | 4/5 | Yes |
|
|
224
|
+
| Utility (counter caches) | 4/5 | Yes |
|
|
225
|
+
| Observer (side effects) | 2/5 | Maybe |
|
|
226
|
+
| Operation (business steps) | 1/5 | Extract |
|
|
227
|
+
|
|
228
|
+
See [Extraction Signals Reference](references/core/extraction-signals.md) for detailed guide.
|
|
229
|
+
|
|
230
|
+
## Model Organization
|
|
231
|
+
|
|
232
|
+
Recommended order within model files:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class User < ApplicationRecord
|
|
236
|
+
# 1. Gems/DSL extensions
|
|
237
|
+
has_secure_password
|
|
238
|
+
|
|
239
|
+
# 2. Associations
|
|
240
|
+
belongs_to :account
|
|
241
|
+
has_many :posts
|
|
242
|
+
|
|
243
|
+
# 3. Enums
|
|
244
|
+
enum :status, { pending: 0, active: 1 }
|
|
245
|
+
|
|
246
|
+
# 4. Normalization
|
|
247
|
+
normalizes :email, with: -> { _1.strip.downcase }
|
|
248
|
+
|
|
249
|
+
# 5. Validations
|
|
250
|
+
validates :email, presence: true
|
|
251
|
+
|
|
252
|
+
# 6. Scopes
|
|
253
|
+
scope :active, -> { where(status: :active) }
|
|
254
|
+
|
|
255
|
+
# 7. Callbacks (transformers only)
|
|
256
|
+
before_validation :set_defaults
|
|
257
|
+
|
|
258
|
+
# 8. Delegations
|
|
259
|
+
delegate :name, to: :account, prefix: true
|
|
260
|
+
|
|
261
|
+
# 9. Public methods
|
|
262
|
+
def full_name = "#{first_name} #{last_name}"
|
|
263
|
+
|
|
264
|
+
# 10. Private methods
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
def set_defaults
|
|
268
|
+
self.locale ||= I18n.default_locale
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Success Checklist
|
|
274
|
+
|
|
275
|
+
Well-layered code:
|
|
276
|
+
|
|
277
|
+
- [ ] No reverse dependencies (lower layers don't depend on higher)
|
|
278
|
+
- [ ] Models don't access Current attributes
|
|
279
|
+
- [ ] Services don't accept request objects
|
|
280
|
+
- [ ] Controllers are thin (HTTP concerns only)
|
|
281
|
+
- [ ] Domain logic lives in models, not services
|
|
282
|
+
- [ ] Callbacks score 4+ or are extracted
|
|
283
|
+
- [ ] Concerns are behavioral, not code-slicing
|
|
284
|
+
- [ ] Abstractions don't span multiple layers
|
|
285
|
+
- [ ] Tests verify appropriate layer responsibilities
|
|
286
|
+
|
|
287
|
+
## Guidelines
|
|
288
|
+
|
|
289
|
+
- **Use domain language** - Name models after business concepts (Participant, not User; Cloud, not GeneratedImage)
|
|
290
|
+
- **Patterns before abstractions** - Let code age before extracting; premature abstraction is worse than duplication
|
|
291
|
+
- **Services as waiting room** - Don't let `app/services` become permanent residence for code
|
|
292
|
+
- **Explicit over implicit** - Prefer explicit parameters over Current attributes
|
|
293
|
+
- **Extraction thresholds** - Consider extraction when methods exceed 15 lines or call external APIs
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Extract Authorization to Policy
|
|
2
|
+
|
|
3
|
+
Replace duplicated controller-side authorization checks with a policy object.
|
|
4
|
+
|
|
5
|
+
## Before
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class PostsController < ApplicationController
|
|
9
|
+
def update
|
|
10
|
+
@post = Post.find(params[:id])
|
|
11
|
+
|
|
12
|
+
# Authorization scattered in controller
|
|
13
|
+
unless current_user.admin? || @post.author == current_user
|
|
14
|
+
redirect_to posts_path, alert: "Not authorized"
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@post.update!(post_params)
|
|
19
|
+
redirect_to @post
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
@post = Post.find(params[:id])
|
|
24
|
+
|
|
25
|
+
# Duplicated logic
|
|
26
|
+
unless current_user.admin?
|
|
27
|
+
redirect_to posts_path, alert: "Not authorized"
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@post.destroy!
|
|
32
|
+
redirect_to posts_path
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## After
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# app/policies/post_policy.rb
|
|
41
|
+
class PostPolicy < ApplicationPolicy
|
|
42
|
+
def update?
|
|
43
|
+
owner? || admin?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def destroy?
|
|
47
|
+
admin?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def owner?
|
|
53
|
+
record.author_id == user.id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def admin?
|
|
57
|
+
user.admin?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# app/controllers/posts_controller.rb
|
|
62
|
+
class PostsController < ApplicationController
|
|
63
|
+
def update
|
|
64
|
+
@post = Post.find(params[:id])
|
|
65
|
+
authorize! @post
|
|
66
|
+
|
|
67
|
+
@post.update!(post_params)
|
|
68
|
+
redirect_to @post
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def destroy
|
|
72
|
+
@post = Post.find(params[:id])
|
|
73
|
+
authorize! @post
|
|
74
|
+
|
|
75
|
+
@post.destroy!
|
|
76
|
+
redirect_to posts_path
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Extract Callbacks to Service
|
|
2
|
+
|
|
3
|
+
Move multi-step user-creation side effects out of `after_create` callbacks into an explicit service.
|
|
4
|
+
|
|
5
|
+
## Before
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class User < ApplicationRecord
|
|
9
|
+
after_create :send_welcome_email
|
|
10
|
+
after_create :create_default_workspace
|
|
11
|
+
after_create :notify_admin
|
|
12
|
+
after_create :track_signup
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def send_welcome_email
|
|
17
|
+
UserMailer.welcome(self).deliver_later
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_default_workspace
|
|
21
|
+
workspaces.create!(name: "My Workspace")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def notify_admin
|
|
25
|
+
AdminMailer.new_user(self).deliver_later
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def track_signup
|
|
29
|
+
Analytics.track("user_signed_up", user_id: id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## After
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# app/models/user.rb
|
|
38
|
+
class User < ApplicationRecord
|
|
39
|
+
before_validation :normalize_email
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def normalize_email
|
|
44
|
+
self.email = email&.downcase&.strip
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# app/services/users/create.rb
|
|
49
|
+
class Users::Create < ApplicationService
|
|
50
|
+
def call(params)
|
|
51
|
+
user = User.create!(params)
|
|
52
|
+
|
|
53
|
+
UserMailer.welcome(user).deliver_later
|
|
54
|
+
user.workspaces.create!(name: "My Workspace")
|
|
55
|
+
AdminMailer.new_user(user).deliver_later
|
|
56
|
+
Analytics.track("user_signed_up", user_id: user.id)
|
|
57
|
+
|
|
58
|
+
user
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# app/controllers/users_controller.rb
|
|
63
|
+
class UsersController < ApplicationController
|
|
64
|
+
def create
|
|
65
|
+
@user = Users::Create.call(user_params)
|
|
66
|
+
redirect_to @user, notice: "Welcome!"
|
|
67
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
68
|
+
@user = e.record
|
|
69
|
+
render :new, status: :unprocessable_entity
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Form Object for Complex Input
|
|
2
|
+
|
|
3
|
+
Replace a controller orchestrating multiple models with a form object that owns the registration workflow.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Before (fat controller)
|
|
8
|
+
- After (form object)
|
|
9
|
+
|
|
10
|
+
## Before
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
class RegistrationsController < ApplicationController
|
|
14
|
+
def create
|
|
15
|
+
@user = User.new(user_params)
|
|
16
|
+
@user.profile = Profile.new(profile_params)
|
|
17
|
+
|
|
18
|
+
if @user.email.end_with?("@company.com")
|
|
19
|
+
@user.role = :employee
|
|
20
|
+
@user.team = Team.find_by(department: profile_params[:department])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if @user.save
|
|
24
|
+
UserMailer.welcome(@user).deliver_later
|
|
25
|
+
redirect_to dashboard_path
|
|
26
|
+
else
|
|
27
|
+
render :new, status: :unprocessable_entity
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def user_params
|
|
34
|
+
params.require(:user).permit(:email, :password, :name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def profile_params
|
|
38
|
+
params.require(:profile).permit(:bio, :department, :avatar)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## After
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# app/forms/registration_form.rb
|
|
47
|
+
class RegistrationForm < ApplicationForm
|
|
48
|
+
attribute :email, :string
|
|
49
|
+
attribute :password, :string
|
|
50
|
+
attribute :name, :string
|
|
51
|
+
attribute :bio, :string
|
|
52
|
+
attribute :department, :string
|
|
53
|
+
attribute :avatar
|
|
54
|
+
|
|
55
|
+
validates :email, :password, :name, presence: true
|
|
56
|
+
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
57
|
+
validates :password, length: { minimum: 8 }
|
|
58
|
+
|
|
59
|
+
def save
|
|
60
|
+
return false unless valid?
|
|
61
|
+
|
|
62
|
+
ApplicationRecord.transaction do
|
|
63
|
+
create_user
|
|
64
|
+
create_profile
|
|
65
|
+
assign_team if company_email?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
70
|
+
errors.merge!(e.record.errors)
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_reader :user
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def create_user
|
|
79
|
+
@user = User.create!(
|
|
80
|
+
email: email,
|
|
81
|
+
password: password,
|
|
82
|
+
name: name,
|
|
83
|
+
role: company_email? ? :employee : :member
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_profile
|
|
88
|
+
@user.create_profile!(bio: bio, department: department, avatar: avatar)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def assign_team
|
|
92
|
+
@user.update!(team: Team.find_by(department: department))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def company_email?
|
|
96
|
+
email.end_with?("@company.com")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# app/controllers/registrations_controller.rb
|
|
101
|
+
class RegistrationsController < ApplicationController
|
|
102
|
+
def create
|
|
103
|
+
@form = RegistrationForm.new(registration_params)
|
|
104
|
+
|
|
105
|
+
if @form.save
|
|
106
|
+
UserMailer.welcome(@form.user).deliver_later
|
|
107
|
+
redirect_to dashboard_path
|
|
108
|
+
else
|
|
109
|
+
render :new, status: :unprocessable_entity
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def registration_params
|
|
116
|
+
params.require(:registration).permit(:email, :password, :name, :bio, :department, :avatar)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Extract Current from Model
|
|
2
|
+
|
|
3
|
+
Remove `Current.user` access from a domain model by moving authorization to a policy and ownership to the controller.
|
|
4
|
+
|
|
5
|
+
## Before
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class Post < ApplicationRecord
|
|
9
|
+
belongs_to :author, class_name: "User"
|
|
10
|
+
|
|
11
|
+
before_validation :set_author, on: :create
|
|
12
|
+
|
|
13
|
+
def can_edit?
|
|
14
|
+
author == Current.user || Current.user&.admin?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def set_author
|
|
20
|
+
self.author = Current.user
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## After
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# app/models/post.rb
|
|
29
|
+
class Post < ApplicationRecord
|
|
30
|
+
belongs_to :author, class_name: "User"
|
|
31
|
+
# No Current access - domain is context-agnostic
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# app/policies/post_policy.rb
|
|
35
|
+
class PostPolicy < ApplicationPolicy
|
|
36
|
+
def edit?
|
|
37
|
+
owner? || admin?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def owner?
|
|
43
|
+
record.author_id == user.id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def admin?
|
|
47
|
+
user.admin?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# app/controllers/posts_controller.rb
|
|
52
|
+
class PostsController < ApplicationController
|
|
53
|
+
def create
|
|
54
|
+
@post = current_user.posts.build(post_params)
|
|
55
|
+
|
|
56
|
+
if @post.save
|
|
57
|
+
redirect_to @post
|
|
58
|
+
else
|
|
59
|
+
render :new, status: :unprocessable_entity
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|