agent-notes 2.0.4__py3-none-any.whl
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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-controllers
|
|
3
|
+
description: "Rails controllers: structure template, concerns, parameter handling, and response patterns"
|
|
4
|
+
group: rails
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Controllers
|
|
8
|
+
|
|
9
|
+
Comprehensive patterns and best practices for Rails controllers.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Core Philosophy
|
|
14
|
+
|
|
15
|
+
1. **Thin controllers** - Delegate business logic to models
|
|
16
|
+
2. **Standard REST** - Use index, show, new, create, edit, update, destroy
|
|
17
|
+
3. **Resource actions as resources** - Not custom actions
|
|
18
|
+
4. **Concerns for shared behavior** - Authentication, scoping, etc.
|
|
19
|
+
5. **Respond to multiple formats** - HTML, JSON, Turbo Stream
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
app/controllers/
|
|
27
|
+
├── application_controller.rb
|
|
28
|
+
├── boards_controller.rb # CRUD controller
|
|
29
|
+
├── cards/
|
|
30
|
+
│ ├── closures_controller.rb # Resource-action controller
|
|
31
|
+
│ ├── goldnesses_controller.rb # Resource-action controller
|
|
32
|
+
│ ├── pins_controller.rb
|
|
33
|
+
│ └── comments_controller.rb
|
|
34
|
+
└── concerns/
|
|
35
|
+
├── authentication.rb # Shared concern
|
|
36
|
+
├── card_scoped.rb # Scoping concern
|
|
37
|
+
└── filter_scoped.rb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Controller Structure Template
|
|
43
|
+
|
|
44
|
+
### CRUD Controller
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class BoardsController < ApplicationController
|
|
48
|
+
# 1. CONCERNS (at top)
|
|
49
|
+
include FilterScoped
|
|
50
|
+
|
|
51
|
+
# 2. BEFORE ACTIONS (explicit conditions)
|
|
52
|
+
before_action :set_board, except: %i[ index new create ]
|
|
53
|
+
before_action :ensure_permission_to_admin_board, only: %i[ update destroy ]
|
|
54
|
+
before_action :ensure_user_has_access, only: %i[ show ]
|
|
55
|
+
|
|
56
|
+
# 3. ACTIONS (in REST order: index, show, new, create, edit, update, destroy)
|
|
57
|
+
|
|
58
|
+
def index
|
|
59
|
+
@boards = Current.user.boards.ordered
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show
|
|
63
|
+
@cards = @board.cards.published.preloaded
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def new
|
|
67
|
+
@board = Board.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create
|
|
71
|
+
@board = Current.user.boards.create!(board_params)
|
|
72
|
+
|
|
73
|
+
respond_to do |format|
|
|
74
|
+
format.html { redirect_to board_path(@board), notice: "Board created" }
|
|
75
|
+
format.json { head :created, location: board_path(@board, format: :json) }
|
|
76
|
+
format.turbo_stream { render turbo_stream: turbo_stream.prepend(:boards, @board) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def edit
|
|
81
|
+
# Renders edit form
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def update
|
|
85
|
+
@board.update!(board_params)
|
|
86
|
+
|
|
87
|
+
respond_to do |format|
|
|
88
|
+
format.html { redirect_to board_path(@board), notice: "Board updated" }
|
|
89
|
+
format.json { head :no_content }
|
|
90
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace(@board, @board) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def destroy
|
|
95
|
+
@board.destroy!
|
|
96
|
+
|
|
97
|
+
respond_to do |format|
|
|
98
|
+
format.html { redirect_to boards_path, notice: "Board deleted" }
|
|
99
|
+
format.json { head :no_content }
|
|
100
|
+
format.turbo_stream { render turbo_stream: turbo_stream.remove(@board) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# 4. PRIVATE METHODS (ordered by invocation)
|
|
105
|
+
private
|
|
106
|
+
def set_board
|
|
107
|
+
@board = Current.user.boards.find params[:id]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ensure_permission_to_admin_board
|
|
111
|
+
unless Current.user.can_administer_board?(@board)
|
|
112
|
+
head :forbidden
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ensure_user_has_access
|
|
117
|
+
unless @board.accessible_to?(Current.user)
|
|
118
|
+
head :forbidden
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def board_params
|
|
123
|
+
params.expect(board: [ :name, :all_access, :auto_postpone_period, :public_description ])
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Resource-Action Controller (Singleton)
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Route: resource :closure
|
|
132
|
+
# POST /cards/:card_id/closure → create (close)
|
|
133
|
+
# DELETE /cards/:card_id/closure → destroy (reopen)
|
|
134
|
+
|
|
135
|
+
class Cards::ClosuresController < ApplicationController
|
|
136
|
+
include CardScoped # Sets @card and @board
|
|
137
|
+
|
|
138
|
+
def create
|
|
139
|
+
@card.close(user: Current.user) # Delegate to model
|
|
140
|
+
|
|
141
|
+
respond_to do |format|
|
|
142
|
+
format.html { redirect_back fallback_location: @card }
|
|
143
|
+
format.json { head :no_content }
|
|
144
|
+
format.turbo_stream { render_card_replacement }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def destroy
|
|
149
|
+
@card.reopen(user: Current.user)
|
|
150
|
+
|
|
151
|
+
respond_to do |format|
|
|
152
|
+
format.html { redirect_back fallback_location: @card }
|
|
153
|
+
format.json { head :no_content }
|
|
154
|
+
format.turbo_stream { render_card_replacement }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Nested Resource Controller
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class Cards::CommentsController < ApplicationController
|
|
164
|
+
include CardScoped # Sets @card
|
|
165
|
+
|
|
166
|
+
before_action :set_comment, only: %i[ show edit update destroy ]
|
|
167
|
+
|
|
168
|
+
def index
|
|
169
|
+
@comments = @card.comments.chronologically
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def show
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def create
|
|
176
|
+
@comment = @card.comments.create!(comment_params.merge(creator: Current.user))
|
|
177
|
+
|
|
178
|
+
respond_to do |format|
|
|
179
|
+
format.turbo_stream
|
|
180
|
+
format.json { render json: @comment, status: :created }
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def update
|
|
185
|
+
@comment.update!(comment_params)
|
|
186
|
+
|
|
187
|
+
respond_to do |format|
|
|
188
|
+
format.turbo_stream
|
|
189
|
+
format.json { head :no_content }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def destroy
|
|
194
|
+
@comment.destroy!
|
|
195
|
+
|
|
196
|
+
respond_to do |format|
|
|
197
|
+
format.turbo_stream { render turbo_stream: turbo_stream.remove(@comment) }
|
|
198
|
+
format.json { head :no_content }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
def set_comment
|
|
204
|
+
@comment = @card.comments.find(params[:id])
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def comment_params
|
|
208
|
+
params.expect(comment: [ :body ])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Controller Concerns
|
|
216
|
+
|
|
217
|
+
### Authentication Concern
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# app/controllers/concerns/authentication.rb
|
|
221
|
+
module Authentication
|
|
222
|
+
extend ActiveSupport::Concern
|
|
223
|
+
|
|
224
|
+
included do
|
|
225
|
+
before_action :require_account
|
|
226
|
+
before_action :require_authentication
|
|
227
|
+
|
|
228
|
+
helper_method :authenticated?, :current_user
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
class_methods do
|
|
232
|
+
# Allow specific actions without authentication
|
|
233
|
+
def allow_unauthenticated_access(**options)
|
|
234
|
+
skip_before_action :require_authentication, **options
|
|
235
|
+
before_action :resume_session, **options
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Require unauthenticated (for login pages)
|
|
239
|
+
def require_unauthenticated_access(**options)
|
|
240
|
+
allow_unauthenticated_access **options
|
|
241
|
+
before_action :redirect_authenticated_user, **options
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
def authenticated?
|
|
247
|
+
Current.identity.present?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def current_user
|
|
251
|
+
Current.user
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def require_authentication
|
|
255
|
+
unless authenticated?
|
|
256
|
+
redirect_to new_session_path
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def require_account
|
|
261
|
+
unless Current.account
|
|
262
|
+
redirect_to root_url(untenanted: true)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def resume_session
|
|
267
|
+
if session_cookie = cookies.signed[:session_id]
|
|
268
|
+
Current.session = Session.find_by(id: session_cookie)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def redirect_authenticated_user
|
|
273
|
+
if authenticated?
|
|
274
|
+
redirect_to root_path
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Scoping Concern
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# app/controllers/concerns/card_scoped.rb
|
|
284
|
+
module CardScoped
|
|
285
|
+
extend ActiveSupport::Concern
|
|
286
|
+
|
|
287
|
+
included do
|
|
288
|
+
before_action :set_card, :set_board
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
def set_card
|
|
293
|
+
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def set_board
|
|
297
|
+
@board = @card.board
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Helper methods for this resource
|
|
301
|
+
def render_card_replacement
|
|
302
|
+
render turbo_stream: turbo_stream.replace(
|
|
303
|
+
[ @card, :card_container ],
|
|
304
|
+
partial: "cards/container",
|
|
305
|
+
method: :morph,
|
|
306
|
+
locals: { card: @card.reload }
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def render_card_preview_replacement
|
|
311
|
+
render turbo_stream: turbo_stream.replace(
|
|
312
|
+
[ @card, :preview ],
|
|
313
|
+
partial: "cards/display/preview",
|
|
314
|
+
locals: { card: @card.reload }
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Feature Toggle Concern
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
module FeatureGuarded
|
|
324
|
+
extend ActiveSupport::Concern
|
|
325
|
+
|
|
326
|
+
included do
|
|
327
|
+
before_action :ensure_feature_enabled
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
private
|
|
331
|
+
def ensure_feature_enabled
|
|
332
|
+
unless Current.account.feature_enabled?(controller_name)
|
|
333
|
+
head :forbidden
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Parameter Handling
|
|
342
|
+
|
|
343
|
+
### Rails 8+ params.expect
|
|
344
|
+
|
|
345
|
+
Rails 8 introduced `params.expect` as the modern, safer alternative to `params.require().permit()`.
|
|
346
|
+
|
|
347
|
+
**Key difference:** `expect` renders a 400 Bad Request response for malformed params (production-friendly), while `expect!` raises an exception (for debugging/internal APIs).
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
# Simple parameters
|
|
351
|
+
def board_params
|
|
352
|
+
params.expect(board: [ :name, :description ])
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Nested parameters
|
|
356
|
+
def card_params
|
|
357
|
+
params.expect(card: [ :title, :status, { tag_ids: [] } ])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Multiple nested levels
|
|
361
|
+
def user_params
|
|
362
|
+
params.expect(user: [
|
|
363
|
+
:name,
|
|
364
|
+
:email,
|
|
365
|
+
:role,
|
|
366
|
+
{
|
|
367
|
+
avatar: [:image],
|
|
368
|
+
preferences: [:theme, :notifications]
|
|
369
|
+
}
|
|
370
|
+
])
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**expect vs expect!**
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
# expect - Production use (returns 400 response for malformed params)
|
|
378
|
+
def create
|
|
379
|
+
@board = Board.create!(board_params)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
def board_params
|
|
384
|
+
params.expect(board: [ :name, :description ])
|
|
385
|
+
end
|
|
386
|
+
# Missing or malformed params → renders 400 Bad Request
|
|
387
|
+
|
|
388
|
+
# expect! - Debugging/Internal APIs (raises exception)
|
|
389
|
+
def board_params
|
|
390
|
+
params.expect!(board: [ :name, :description ])
|
|
391
|
+
end
|
|
392
|
+
# Missing or malformed params → raises ActionController::ParameterMissing
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Legacy pattern (Rails 7 and earlier)**
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# Still works in Rails 8, but expect is preferred
|
|
399
|
+
def board_params
|
|
400
|
+
params.require(:board).permit(:name, :description, :all_access)
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Parameter Sanitization
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
private
|
|
408
|
+
def sanitized_tag_title_param
|
|
409
|
+
params.required(:tag_title).strip.gsub(/\A#/, "")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def sanitized_email_param
|
|
413
|
+
params.required(:email).downcase.strip
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def normalized_url_param
|
|
417
|
+
url = params.required(:url)
|
|
418
|
+
url = "https://#{url}" unless url.start_with?("http")
|
|
419
|
+
url
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Response Patterns
|
|
426
|
+
|
|
427
|
+
### Multi-Format Responses
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
def create
|
|
431
|
+
@board = Board.create!(board_params)
|
|
432
|
+
|
|
433
|
+
respond_to do |format|
|
|
434
|
+
format.html { redirect_to board_path(@board), notice: "Created!" }
|
|
435
|
+
format.json { render json: @board, status: :created }
|
|
436
|
+
format.turbo_stream { render turbo_stream: turbo_stream.prepend(:boards, @board) }
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Redirect Patterns
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# Basic redirect
|
|
445
|
+
redirect_to board_path(@board)
|
|
446
|
+
|
|
447
|
+
# With notice/alert
|
|
448
|
+
redirect_to @board, notice: "Board created"
|
|
449
|
+
redirect_to @board, alert: "Something went wrong"
|
|
450
|
+
|
|
451
|
+
# Redirect back with fallback
|
|
452
|
+
redirect_back fallback_location: @board
|
|
453
|
+
|
|
454
|
+
# Conditional redirect
|
|
455
|
+
if @board.accessible_to?(Current.user)
|
|
456
|
+
redirect_to @board
|
|
457
|
+
else
|
|
458
|
+
redirect_to boards_path
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Turbo-aware redirect
|
|
462
|
+
redirect_to @board, status: :see_other # Forces GET request in Turbo
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Status Codes
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
# Success
|
|
469
|
+
head :ok # 200
|
|
470
|
+
head :created # 201
|
|
471
|
+
head :no_content # 204
|
|
472
|
+
|
|
473
|
+
# Client errors
|
|
474
|
+
head :bad_request # 400
|
|
475
|
+
head :unauthorized # 401
|
|
476
|
+
head :forbidden # 403
|
|
477
|
+
head :not_found # 404
|
|
478
|
+
head :unprocessable_entity # 422
|
|
479
|
+
|
|
480
|
+
# Server errors
|
|
481
|
+
head :internal_server_error # 500
|
|
482
|
+
|
|
483
|
+
# With location
|
|
484
|
+
head :created, location: board_path(@board)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Turbo Stream Responses
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
# Replace element
|
|
491
|
+
render turbo_stream: turbo_stream.replace(@card, partial: "cards/card")
|
|
492
|
+
|
|
493
|
+
# Update element
|
|
494
|
+
render turbo_stream: turbo_stream.update(@card, partial: "cards/card")
|
|
495
|
+
|
|
496
|
+
# Append/Prepend
|
|
497
|
+
render turbo_stream: turbo_stream.append(:cards, @card)
|
|
498
|
+
render turbo_stream: turbo_stream.prepend(:cards, @card)
|
|
499
|
+
|
|
500
|
+
# Remove
|
|
501
|
+
render turbo_stream: turbo_stream.remove(@card)
|
|
502
|
+
|
|
503
|
+
# Multiple actions
|
|
504
|
+
render turbo_stream: [
|
|
505
|
+
turbo_stream.replace(@card, @card),
|
|
506
|
+
turbo_stream.update(:sidebar, partial: "cards/sidebar")
|
|
507
|
+
]
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|