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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/hyperdrive.yml +24 -0
  3. data/layered-rails/skills/layered-rails/SKILL.md +293 -0
  4. data/layered-rails/skills/layered-rails/examples/authorization-to-policy.md +79 -0
  5. data/layered-rails/skills/layered-rails/examples/callbacks-to-service.md +72 -0
  6. data/layered-rails/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
  7. data/layered-rails/skills/layered-rails/examples/current-from-model.md +63 -0
  8. data/layered-rails/skills/layered-rails/examples/god-object-decomposition.md +81 -0
  9. data/layered-rails/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
  10. data/layered-rails/skills/layered-rails/examples/query-to-query-object.md +63 -0
  11. data/layered-rails/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
  12. data/layered-rails/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
  13. data/layered-rails/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
  14. data/layered-rails/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
  15. data/layered-rails/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
  16. data/layered-rails/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
  17. data/layered-rails/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
  18. data/layered-rails/skills/layered-rails/references/anti-patterns/testing.md +37 -0
  19. data/layered-rails/skills/layered-rails/references/core/architecture-layers.md +212 -0
  20. data/layered-rails/skills/layered-rails/references/core/extraction-signals.md +314 -0
  21. data/layered-rails/skills/layered-rails/references/core/specification-test.md +229 -0
  22. data/layered-rails/skills/layered-rails/references/gems/action-policy.md +437 -0
  23. data/layered-rails/skills/layered-rails/references/gems/active-agent.md +275 -0
  24. data/layered-rails/skills/layered-rails/references/gems/active-delivery.md +221 -0
  25. data/layered-rails/skills/layered-rails/references/gems/active-job-performs.md +171 -0
  26. data/layered-rails/skills/layered-rails/references/gems/alba.md +257 -0
  27. data/layered-rails/skills/layered-rails/references/gems/anyway-config.md +212 -0
  28. data/layered-rails/skills/layered-rails/references/gems/archspec.md +208 -0
  29. data/layered-rails/skills/layered-rails/references/gems/rubanok.md +246 -0
  30. data/layered-rails/skills/layered-rails/references/gems/view-component.md +223 -0
  31. data/layered-rails/skills/layered-rails/references/gems/workflow.md +305 -0
  32. data/layered-rails/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
  33. data/layered-rails/skills/layered-rails/references/patterns/concerns.md +381 -0
  34. data/layered-rails/skills/layered-rails/references/patterns/filter-objects.md +222 -0
  35. data/layered-rails/skills/layered-rails/references/patterns/form-objects.md +267 -0
  36. data/layered-rails/skills/layered-rails/references/patterns/policy-objects.md +298 -0
  37. data/layered-rails/skills/layered-rails/references/patterns/presenters.md +257 -0
  38. data/layered-rails/skills/layered-rails/references/patterns/query-objects.md +208 -0
  39. data/layered-rails/skills/layered-rails/references/patterns/repositories.md +382 -0
  40. data/layered-rails/skills/layered-rails/references/patterns/serializers.md +256 -0
  41. data/layered-rails/skills/layered-rails/references/patterns/service-objects.md +170 -0
  42. data/layered-rails/skills/layered-rails/references/patterns/state-machines.md +368 -0
  43. data/layered-rails/skills/layered-rails/references/patterns/value-objects.md +267 -0
  44. data/layered-rails/skills/layered-rails/references/topics/ai-integration.md +372 -0
  45. data/layered-rails/skills/layered-rails/references/topics/authorization.md +398 -0
  46. data/layered-rails/skills/layered-rails/references/topics/callbacks.md +336 -0
  47. data/layered-rails/skills/layered-rails/references/topics/configuration.md +389 -0
  48. data/layered-rails/skills/layered-rails/references/topics/current-attributes.md +307 -0
  49. data/layered-rails/skills/layered-rails/references/topics/instrumentation.md +386 -0
  50. data/layered-rails/skills/layered-rails/references/topics/notifications.md +373 -0
  51. data/layered-rails/skills/layered-rails/references/topics/view-components.md +454 -0
  52. data/layered-rails/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
  53. data/layered-rails/skills/layered-rails/workflows/analyze-gods.md +279 -0
  54. data/layered-rails/skills/layered-rails/workflows/analyze-services.md +1444 -0
  55. data/layered-rails/skills/layered-rails/workflows/analyze.md +386 -0
  56. data/layered-rails/skills/layered-rails/workflows/archspec.md +63 -0
  57. data/layered-rails/skills/layered-rails/workflows/plan.md +254 -0
  58. data/layered-rails/skills/layered-rails/workflows/review.md +473 -0
  59. data/layered-rails/skills/layered-rails/workflows/spec-test.md +282 -0
  60. data/rails-hyperdrive/layered-rails/templates/layered-rails/SKILL.md.erb +298 -0
  61. 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
+ ```