claude-agent-framework 1.0.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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
# Skill: Clean Code Principles
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Write readable, maintainable, and self-documenting Ruby and Rails code following Uncle Bob's Clean Code principles.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Naming Conventions
|
|
9
|
+
|
|
10
|
+
### Use Intention-Revealing Names
|
|
11
|
+
Names should tell you why something exists, what it does, and how it's used.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Bad
|
|
15
|
+
d = Time.now - user.created_at # elapsed time in days?
|
|
16
|
+
list = User.where("age > 18")
|
|
17
|
+
temp = calculate_something
|
|
18
|
+
|
|
19
|
+
# Good
|
|
20
|
+
days_since_registration = (Time.now - user.created_at).to_i / 1.day
|
|
21
|
+
adult_users = User.where("age >= ?", 18)
|
|
22
|
+
monthly_revenue = calculate_monthly_revenue
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Use Pronounceable Names
|
|
26
|
+
```ruby
|
|
27
|
+
# Bad
|
|
28
|
+
genymdhms = Time.now.strftime("%Y%m%d%H%M%S")
|
|
29
|
+
cstmr_lst = []
|
|
30
|
+
modymdhms = record.updated_at
|
|
31
|
+
|
|
32
|
+
# Good
|
|
33
|
+
generation_timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
|
34
|
+
customers = []
|
|
35
|
+
modification_timestamp = record.updated_at
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Use Searchable Names
|
|
39
|
+
```ruby
|
|
40
|
+
# Bad - magic numbers are not searchable
|
|
41
|
+
if status == 4
|
|
42
|
+
user.update(role: 2)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Good - constants are searchable and meaningful
|
|
46
|
+
STATUS_APPROVED = 4
|
|
47
|
+
ROLE_ADMIN = 2
|
|
48
|
+
|
|
49
|
+
if status == STATUS_APPROVED
|
|
50
|
+
user.update(role: ROLE_ADMIN)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Even better in Rails - use enums
|
|
54
|
+
class User < ApplicationRecord
|
|
55
|
+
enum status: { pending: 0, approved: 1, rejected: 2 }
|
|
56
|
+
enum role: { user: 0, moderator: 1, admin: 2 }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if user.approved?
|
|
60
|
+
user.admin!
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Naming Conventions by Type
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Classes/Modules: Nouns, PascalCase
|
|
68
|
+
class UserAccount; end
|
|
69
|
+
class PaymentProcessor; end
|
|
70
|
+
module Searchable; end
|
|
71
|
+
|
|
72
|
+
# Methods: Verbs, snake_case
|
|
73
|
+
def calculate_total; end
|
|
74
|
+
def send_notification; end
|
|
75
|
+
def validate_email; end
|
|
76
|
+
|
|
77
|
+
# Predicate methods: End with ?
|
|
78
|
+
def valid?; end
|
|
79
|
+
def admin?; end
|
|
80
|
+
def can_edit?; end
|
|
81
|
+
|
|
82
|
+
# Dangerous methods: End with !
|
|
83
|
+
def save!; end # Raises exception on failure
|
|
84
|
+
def delete!; end # Destructive operation
|
|
85
|
+
def normalize!; end # Mutates in place
|
|
86
|
+
|
|
87
|
+
# Variables: snake_case, descriptive
|
|
88
|
+
current_user = User.find(id)
|
|
89
|
+
total_amount = cart.items.sum(&:price)
|
|
90
|
+
is_authenticated = session[:user_id].present? # or: authenticated?
|
|
91
|
+
|
|
92
|
+
# Constants: SCREAMING_SNAKE_CASE
|
|
93
|
+
MAX_LOGIN_ATTEMPTS = 5
|
|
94
|
+
DEFAULT_PAGE_SIZE = 25
|
|
95
|
+
API_BASE_URL = "https://api.example.com"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Avoid Mental Mapping
|
|
99
|
+
```ruby
|
|
100
|
+
# Bad - reader must remember what a, b, c mean
|
|
101
|
+
def calculate(a, b, c)
|
|
102
|
+
a * b * (1 + c)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Good - names are self-documenting
|
|
106
|
+
def calculate_total_with_tax(quantity, unit_price, tax_rate)
|
|
107
|
+
quantity * unit_price * (1 + tax_rate)
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Functions / Methods
|
|
114
|
+
|
|
115
|
+
### Small and Focused
|
|
116
|
+
Methods should do one thing, do it well, and do it only.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Bad - method does too many things
|
|
120
|
+
def process_order(order)
|
|
121
|
+
# Validate
|
|
122
|
+
return false if order.items.empty?
|
|
123
|
+
return false unless order.user.active?
|
|
124
|
+
|
|
125
|
+
# Calculate totals
|
|
126
|
+
subtotal = order.items.sum(&:price)
|
|
127
|
+
tax = subtotal * 0.1
|
|
128
|
+
shipping = subtotal > 100 ? 0 : 10
|
|
129
|
+
total = subtotal + tax + shipping
|
|
130
|
+
|
|
131
|
+
# Process payment
|
|
132
|
+
payment_result = PaymentGateway.charge(total, order.payment_info)
|
|
133
|
+
return false unless payment_result.success?
|
|
134
|
+
|
|
135
|
+
# Update inventory
|
|
136
|
+
order.items.each do |item|
|
|
137
|
+
product = Product.find(item.product_id)
|
|
138
|
+
product.decrement!(:stock, item.quantity)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create records
|
|
142
|
+
order.update(
|
|
143
|
+
status: :paid,
|
|
144
|
+
total: total,
|
|
145
|
+
paid_at: Time.current
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Send notifications
|
|
149
|
+
OrderMailer.confirmation(order).deliver_later
|
|
150
|
+
AdminNotifier.new_order(order)
|
|
151
|
+
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Good - each method does one thing
|
|
156
|
+
def process_order(order)
|
|
157
|
+
return failure("Empty order") if order.items.empty?
|
|
158
|
+
return failure("Inactive user") unless order.user.active?
|
|
159
|
+
|
|
160
|
+
totals = calculate_totals(order)
|
|
161
|
+
payment = process_payment(order, totals[:total])
|
|
162
|
+
|
|
163
|
+
return failure(payment.error) unless payment.success?
|
|
164
|
+
|
|
165
|
+
finalize_order(order, totals, payment)
|
|
166
|
+
send_notifications(order)
|
|
167
|
+
|
|
168
|
+
success(order)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def calculate_totals(order)
|
|
174
|
+
subtotal = order.items.sum(&:price)
|
|
175
|
+
{
|
|
176
|
+
subtotal: subtotal,
|
|
177
|
+
tax: calculate_tax(subtotal),
|
|
178
|
+
shipping: calculate_shipping(subtotal),
|
|
179
|
+
total: subtotal + tax + shipping
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def process_payment(order, amount)
|
|
184
|
+
PaymentGateway.charge(amount, order.payment_info)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def finalize_order(order, totals, payment)
|
|
188
|
+
reduce_inventory(order.items)
|
|
189
|
+
order.update(status: :paid, total: totals[:total], paid_at: Time.current)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def send_notifications(order)
|
|
193
|
+
OrderMailer.confirmation(order).deliver_later
|
|
194
|
+
AdminNotifier.new_order(order)
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Few Arguments (Ideally Zero to Two)
|
|
199
|
+
```ruby
|
|
200
|
+
# Bad - too many arguments
|
|
201
|
+
def create_user(first_name, last_name, email, password, role, department, manager_id, start_date)
|
|
202
|
+
# ...
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Good - use a hash or object
|
|
206
|
+
def create_user(attributes)
|
|
207
|
+
User.create!(attributes)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Or named parameters
|
|
211
|
+
def create_user(email:, password:, name: nil, role: :user)
|
|
212
|
+
User.create!(email: email, password: password, name: name, role: role)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Best - use a form object or builder
|
|
216
|
+
class UserRegistration
|
|
217
|
+
include ActiveModel::Model
|
|
218
|
+
|
|
219
|
+
attr_accessor :email, :password, :name, :role
|
|
220
|
+
|
|
221
|
+
validates :email, :password, presence: true
|
|
222
|
+
|
|
223
|
+
def save
|
|
224
|
+
return false unless valid?
|
|
225
|
+
User.create!(attributes)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
registration = UserRegistration.new(params[:user])
|
|
230
|
+
registration.save
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Avoid Flag Arguments
|
|
234
|
+
```ruby
|
|
235
|
+
# Bad - boolean parameter
|
|
236
|
+
def render_page(content, include_sidebar)
|
|
237
|
+
if include_sidebar
|
|
238
|
+
render_with_sidebar(content)
|
|
239
|
+
else
|
|
240
|
+
render_without_sidebar(content)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
render_page(content, true) # What does true mean?
|
|
245
|
+
|
|
246
|
+
# Good - separate methods
|
|
247
|
+
def render_page_with_sidebar(content)
|
|
248
|
+
render_with_sidebar(content)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def render_page_without_sidebar(content)
|
|
252
|
+
render_without_sidebar(content)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Or use named parameters
|
|
256
|
+
def render_page(content, sidebar: false)
|
|
257
|
+
sidebar ? render_with_sidebar(content) : render_without_sidebar(content)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
render_page(content, sidebar: true) # Clear intent
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Command Query Separation
|
|
264
|
+
Methods should either do something (command) or return something (query), not both.
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
# Bad - does something AND returns something
|
|
268
|
+
def set_and_check_name(name)
|
|
269
|
+
@name = name
|
|
270
|
+
@name.present?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Good - separate command and query
|
|
274
|
+
def set_name(name)
|
|
275
|
+
@name = name
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def name_present?
|
|
279
|
+
@name.present?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Usage
|
|
283
|
+
set_name("John")
|
|
284
|
+
if name_present?
|
|
285
|
+
# ...
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Comments
|
|
292
|
+
|
|
293
|
+
### Code Should Be Self-Documenting
|
|
294
|
+
```ruby
|
|
295
|
+
# Bad - comment explains what (obvious from code)
|
|
296
|
+
# Check if user is an adult
|
|
297
|
+
if user.age >= 18
|
|
298
|
+
# Allow access
|
|
299
|
+
grant_access
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Good - code explains itself
|
|
303
|
+
if user.adult?
|
|
304
|
+
grant_access
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# In User model
|
|
308
|
+
def adult?
|
|
309
|
+
age >= ADULT_AGE
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Comment the "Why", Not the "What"
|
|
314
|
+
```ruby
|
|
315
|
+
# Bad - explains what the code does (obvious)
|
|
316
|
+
# Increment counter by 1
|
|
317
|
+
counter += 1
|
|
318
|
+
|
|
319
|
+
# Good - explains why
|
|
320
|
+
# We add a buffer day to account for timezone differences
|
|
321
|
+
# when calculating subscription expiry
|
|
322
|
+
expiry_date = subscription.end_date + 1.day
|
|
323
|
+
|
|
324
|
+
# Performance optimization: batch processing to avoid memory issues
|
|
325
|
+
# with large datasets (see issue #1234)
|
|
326
|
+
User.find_each(batch_size: 1000) do |user|
|
|
327
|
+
process(user)
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Good Uses of Comments
|
|
332
|
+
```ruby
|
|
333
|
+
# TODO: Refactor this when we upgrade to Rails 8
|
|
334
|
+
# FIXME: Race condition possible with concurrent updates
|
|
335
|
+
# HACK: Workaround for third-party API bug (ticket #XYZ)
|
|
336
|
+
# NOTE: This algorithm is O(n^2), acceptable for small datasets
|
|
337
|
+
|
|
338
|
+
# Legal/copyright headers (required by license)
|
|
339
|
+
# frozen_string_literal: true
|
|
340
|
+
|
|
341
|
+
# Public API documentation
|
|
342
|
+
# Calculates the compound interest for an investment.
|
|
343
|
+
#
|
|
344
|
+
# @param principal [Float] The initial investment amount
|
|
345
|
+
# @param rate [Float] Annual interest rate (e.g., 0.05 for 5%)
|
|
346
|
+
# @param years [Integer] Number of years
|
|
347
|
+
# @return [Float] The final amount after compound interest
|
|
348
|
+
#
|
|
349
|
+
# @example
|
|
350
|
+
# compound_interest(1000, 0.05, 10) #=> 1628.89
|
|
351
|
+
def compound_interest(principal, rate, years)
|
|
352
|
+
principal * (1 + rate) ** years
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Warning about consequences
|
|
356
|
+
# WARNING: This will delete all user data permanently.
|
|
357
|
+
# Only call this in development/test environments.
|
|
358
|
+
def reset_database!
|
|
359
|
+
raise "Not in production!" if Rails.env.production?
|
|
360
|
+
# ...
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Avoid Commented-Out Code
|
|
365
|
+
```ruby
|
|
366
|
+
# Bad - commented code is confusing and clutters
|
|
367
|
+
def calculate_discount(price)
|
|
368
|
+
# old_discount = price * 0.1
|
|
369
|
+
# if customer.vip?
|
|
370
|
+
# old_discount *= 2
|
|
371
|
+
# end
|
|
372
|
+
# return old_discount
|
|
373
|
+
|
|
374
|
+
# new implementation
|
|
375
|
+
DiscountCalculator.new(price, customer).calculate
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Good - remove dead code, use git for history
|
|
379
|
+
def calculate_discount(price)
|
|
380
|
+
DiscountCalculator.new(price, customer).calculate
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Formatting
|
|
387
|
+
|
|
388
|
+
### Consistent Indentation
|
|
389
|
+
```ruby
|
|
390
|
+
# Use 2 spaces for indentation in Ruby (standard)
|
|
391
|
+
class User
|
|
392
|
+
def full_name
|
|
393
|
+
"#{first_name} #{last_name}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def admin?
|
|
397
|
+
role == :admin
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Vertical Spacing
|
|
403
|
+
```ruby
|
|
404
|
+
class OrderService
|
|
405
|
+
# Group related methods together
|
|
406
|
+
# Separate groups with blank lines
|
|
407
|
+
|
|
408
|
+
# Public interface
|
|
409
|
+
def create_order(user, cart)
|
|
410
|
+
validate_cart(cart)
|
|
411
|
+
order = build_order(user, cart)
|
|
412
|
+
process_order(order)
|
|
413
|
+
order
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def cancel_order(order)
|
|
417
|
+
refund_payment(order)
|
|
418
|
+
restore_inventory(order)
|
|
419
|
+
order.cancel!
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
private
|
|
423
|
+
|
|
424
|
+
# Validation helpers
|
|
425
|
+
def validate_cart(cart)
|
|
426
|
+
raise EmptyCartError if cart.empty?
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Order building
|
|
430
|
+
def build_order(user, cart)
|
|
431
|
+
Order.new(user: user, items: cart.items)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Processing
|
|
435
|
+
def process_order(order)
|
|
436
|
+
charge_payment(order)
|
|
437
|
+
reserve_inventory(order)
|
|
438
|
+
send_confirmation(order)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Payment operations
|
|
442
|
+
def charge_payment(order)
|
|
443
|
+
# ...
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def refund_payment(order)
|
|
447
|
+
# ...
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Inventory operations
|
|
451
|
+
def reserve_inventory(order)
|
|
452
|
+
# ...
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def restore_inventory(order)
|
|
456
|
+
# ...
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Notifications
|
|
460
|
+
def send_confirmation(order)
|
|
461
|
+
# ...
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Line Length
|
|
467
|
+
```ruby
|
|
468
|
+
# Keep lines under 80-120 characters
|
|
469
|
+
# Break long method chains
|
|
470
|
+
User
|
|
471
|
+
.where(active: true)
|
|
472
|
+
.includes(:orders)
|
|
473
|
+
.order(created_at: :desc)
|
|
474
|
+
.limit(10)
|
|
475
|
+
|
|
476
|
+
# Break long argument lists
|
|
477
|
+
create_user(
|
|
478
|
+
email: "john@example.com",
|
|
479
|
+
first_name: "John",
|
|
480
|
+
last_name: "Doe",
|
|
481
|
+
role: :admin,
|
|
482
|
+
department: "Engineering"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Break long conditionals
|
|
486
|
+
if user.active? &&
|
|
487
|
+
user.email_verified? &&
|
|
488
|
+
user.subscription.valid? &&
|
|
489
|
+
user.two_factor_enabled?
|
|
490
|
+
grant_premium_access
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Or extract to methods
|
|
494
|
+
if eligible_for_premium_access?(user)
|
|
495
|
+
grant_premium_access
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Error Handling
|
|
502
|
+
|
|
503
|
+
### Use Exceptions, Not Return Codes
|
|
504
|
+
```ruby
|
|
505
|
+
# Bad - return codes
|
|
506
|
+
def withdraw(amount)
|
|
507
|
+
return -1 if amount <= 0
|
|
508
|
+
return -2 if amount > balance
|
|
509
|
+
@balance -= amount
|
|
510
|
+
0 # success
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
result = account.withdraw(100)
|
|
514
|
+
case result
|
|
515
|
+
when 0 then puts "Success"
|
|
516
|
+
when -1 then puts "Invalid amount"
|
|
517
|
+
when -2 then puts "Insufficient funds"
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Good - exceptions
|
|
521
|
+
class InvalidAmountError < StandardError; end
|
|
522
|
+
class InsufficientFundsError < StandardError; end
|
|
523
|
+
|
|
524
|
+
def withdraw(amount)
|
|
525
|
+
raise InvalidAmountError, "Amount must be positive" if amount <= 0
|
|
526
|
+
raise InsufficientFundsError, "Balance too low" if amount > balance
|
|
527
|
+
@balance -= amount
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
begin
|
|
531
|
+
account.withdraw(100)
|
|
532
|
+
puts "Success"
|
|
533
|
+
rescue InvalidAmountError => e
|
|
534
|
+
puts "Error: #{e.message}"
|
|
535
|
+
rescue InsufficientFundsError => e
|
|
536
|
+
puts "Error: #{e.message}"
|
|
537
|
+
end
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Fail Fast
|
|
541
|
+
```ruby
|
|
542
|
+
# Bad - deeply nested conditionals
|
|
543
|
+
def process_payment(order)
|
|
544
|
+
if order.present?
|
|
545
|
+
if order.items.any?
|
|
546
|
+
if order.user.active?
|
|
547
|
+
if order.payment_method.valid?
|
|
548
|
+
# Finally, the actual logic
|
|
549
|
+
charge_customer(order)
|
|
550
|
+
else
|
|
551
|
+
log_error("Invalid payment method")
|
|
552
|
+
end
|
|
553
|
+
else
|
|
554
|
+
log_error("Inactive user")
|
|
555
|
+
end
|
|
556
|
+
else
|
|
557
|
+
log_error("Empty order")
|
|
558
|
+
end
|
|
559
|
+
else
|
|
560
|
+
log_error("No order")
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Good - guard clauses / fail fast
|
|
565
|
+
def process_payment(order)
|
|
566
|
+
raise ArgumentError, "Order required" unless order.present?
|
|
567
|
+
raise EmptyOrderError unless order.items.any?
|
|
568
|
+
raise InactiveUserError unless order.user.active?
|
|
569
|
+
raise InvalidPaymentError unless order.payment_method.valid?
|
|
570
|
+
|
|
571
|
+
charge_customer(order)
|
|
572
|
+
end
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Define Custom Exceptions
|
|
576
|
+
```ruby
|
|
577
|
+
# app/errors/application_error.rb
|
|
578
|
+
class ApplicationError < StandardError
|
|
579
|
+
attr_reader :code, :details
|
|
580
|
+
|
|
581
|
+
def initialize(message = nil, code: nil, details: {})
|
|
582
|
+
@code = code
|
|
583
|
+
@details = details
|
|
584
|
+
super(message)
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Domain-specific errors
|
|
589
|
+
class PaymentError < ApplicationError; end
|
|
590
|
+
class PaymentDeclinedError < PaymentError; end
|
|
591
|
+
class PaymentGatewayError < PaymentError; end
|
|
592
|
+
|
|
593
|
+
class OrderError < ApplicationError; end
|
|
594
|
+
class OutOfStockError < OrderError; end
|
|
595
|
+
class InvalidOrderError < OrderError; end
|
|
596
|
+
|
|
597
|
+
# Usage with details
|
|
598
|
+
raise OutOfStockError.new(
|
|
599
|
+
"Product not available",
|
|
600
|
+
code: "OUT_OF_STOCK",
|
|
601
|
+
details: { product_id: product.id, requested: 5, available: 2 }
|
|
602
|
+
)
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Handle Errors at the Right Level
|
|
606
|
+
```ruby
|
|
607
|
+
# In Rails controller - handle and respond
|
|
608
|
+
class OrdersController < ApplicationController
|
|
609
|
+
rescue_from OrderError, with: :handle_order_error
|
|
610
|
+
rescue_from PaymentError, with: :handle_payment_error
|
|
611
|
+
|
|
612
|
+
def create
|
|
613
|
+
@order = OrderService.new.create(order_params)
|
|
614
|
+
redirect_to @order, notice: "Order placed!"
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
private
|
|
618
|
+
|
|
619
|
+
def handle_order_error(error)
|
|
620
|
+
flash.now[:alert] = error.message
|
|
621
|
+
render :new, status: :unprocessable_entity
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def handle_payment_error(error)
|
|
625
|
+
flash.now[:alert] = "Payment failed: #{error.message}"
|
|
626
|
+
render :checkout, status: :payment_required
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# In service - raise, don't handle
|
|
631
|
+
class OrderService
|
|
632
|
+
def create(params)
|
|
633
|
+
order = Order.new(params)
|
|
634
|
+
validate!(order)
|
|
635
|
+
process_payment!(order)
|
|
636
|
+
order.save!
|
|
637
|
+
order
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
private
|
|
641
|
+
|
|
642
|
+
def validate!(order)
|
|
643
|
+
raise InvalidOrderError, "No items" if order.items.empty?
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def process_payment!(order)
|
|
647
|
+
result = PaymentGateway.charge(order.total)
|
|
648
|
+
raise PaymentDeclinedError, result.message unless result.success?
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## DRY - Don't Repeat Yourself
|
|
656
|
+
|
|
657
|
+
### Extract Common Patterns
|
|
658
|
+
```ruby
|
|
659
|
+
# Bad - repeated code
|
|
660
|
+
class UsersController < ApplicationController
|
|
661
|
+
def show
|
|
662
|
+
@user = User.find(params[:id])
|
|
663
|
+
authorize @user
|
|
664
|
+
respond_to do |format|
|
|
665
|
+
format.html
|
|
666
|
+
format.json { render json: @user }
|
|
667
|
+
end
|
|
668
|
+
rescue ActiveRecord::RecordNotFound
|
|
669
|
+
redirect_to users_path, alert: "User not found"
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def edit
|
|
673
|
+
@user = User.find(params[:id])
|
|
674
|
+
authorize @user
|
|
675
|
+
respond_to do |format|
|
|
676
|
+
format.html
|
|
677
|
+
format.json { render json: @user }
|
|
678
|
+
end
|
|
679
|
+
rescue ActiveRecord::RecordNotFound
|
|
680
|
+
redirect_to users_path, alert: "User not found"
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Good - DRY with callbacks and concerns
|
|
685
|
+
class UsersController < ApplicationController
|
|
686
|
+
before_action :set_user, only: [:show, :edit, :update, :destroy]
|
|
687
|
+
|
|
688
|
+
def show
|
|
689
|
+
respond_with @user
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def edit
|
|
693
|
+
respond_with @user
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
private
|
|
697
|
+
|
|
698
|
+
def set_user
|
|
699
|
+
@user = User.find(params[:id])
|
|
700
|
+
authorize @user
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Or with a concern for common behavior
|
|
705
|
+
module Findable
|
|
706
|
+
extend ActiveSupport::Concern
|
|
707
|
+
|
|
708
|
+
class_methods do
|
|
709
|
+
def set_resource(name, scope: nil)
|
|
710
|
+
before_action only: [:show, :edit, :update, :destroy] do
|
|
711
|
+
model = name.to_s.classify.constantize
|
|
712
|
+
query = scope ? instance_exec(&scope) : model
|
|
713
|
+
instance_variable_set("@#{name}", query.find(params[:id]))
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
class UsersController < ApplicationController
|
|
720
|
+
include Findable
|
|
721
|
+
set_resource :user
|
|
722
|
+
end
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### But Don't Over-DRY
|
|
726
|
+
```ruby
|
|
727
|
+
# Sometimes repetition is okay if:
|
|
728
|
+
# 1. The code is simple enough
|
|
729
|
+
# 2. The concepts are different even if similar
|
|
730
|
+
# 3. DRY would create unnecessary coupling
|
|
731
|
+
|
|
732
|
+
# Okay to repeat - different domains
|
|
733
|
+
class User < ApplicationRecord
|
|
734
|
+
def full_name
|
|
735
|
+
"#{first_name} #{last_name}"
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
class Author < ApplicationRecord
|
|
740
|
+
def full_name
|
|
741
|
+
"#{first_name} #{last_name}"
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# These look similar but represent different concepts
|
|
746
|
+
# Extracting to a module might create unwanted coupling
|
|
747
|
+
# If full_name changes for Author (e.g., add title), User shouldn't be affected
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## KISS - Keep It Simple, Stupid
|
|
753
|
+
|
|
754
|
+
### Prefer Simple Solutions
|
|
755
|
+
```ruby
|
|
756
|
+
# Bad - over-engineered
|
|
757
|
+
class StringReverser
|
|
758
|
+
def initialize(strategy: DefaultReversalStrategy.new)
|
|
759
|
+
@strategy = strategy
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def reverse(string)
|
|
763
|
+
@strategy.reverse(string)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
class DefaultReversalStrategy
|
|
767
|
+
def reverse(string)
|
|
768
|
+
string.chars.reverse.join
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
reverser = StringReverser.new
|
|
774
|
+
reverser.reverse("hello")
|
|
775
|
+
|
|
776
|
+
# Good - simple
|
|
777
|
+
"hello".reverse
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Avoid Premature Optimization
|
|
781
|
+
```ruby
|
|
782
|
+
# Bad - premature optimization
|
|
783
|
+
class UserSearch
|
|
784
|
+
def initialize
|
|
785
|
+
@cache = LRUCache.new(1000)
|
|
786
|
+
@index = BloomFilter.new
|
|
787
|
+
@thread_pool = ThreadPool.new(4)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def search(query)
|
|
791
|
+
return @cache.get(query) if @cache.has?(query)
|
|
792
|
+
# Complex parallel search...
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Good - start simple, optimize when needed
|
|
797
|
+
class UserSearch
|
|
798
|
+
def search(query)
|
|
799
|
+
User.where("name ILIKE ?", "%#{query}%").limit(20)
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Add complexity only when you have evidence it's needed:
|
|
804
|
+
# - Slow queries in production logs
|
|
805
|
+
# - Performance tests showing bottlenecks
|
|
806
|
+
# - Clear requirements for scale
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## YAGNI - You Aren't Gonna Need It
|
|
812
|
+
|
|
813
|
+
### Don't Build Features You Don't Need Yet
|
|
814
|
+
```ruby
|
|
815
|
+
# Bad - building for hypothetical future requirements
|
|
816
|
+
class Report
|
|
817
|
+
def generate(format = :pdf)
|
|
818
|
+
data = fetch_data
|
|
819
|
+
|
|
820
|
+
case format
|
|
821
|
+
when :pdf then generate_pdf(data)
|
|
822
|
+
when :csv then generate_csv(data)
|
|
823
|
+
when :excel then generate_excel(data)
|
|
824
|
+
when :xml then generate_xml(data) # Never requested
|
|
825
|
+
when :json then generate_json(data) # Never requested
|
|
826
|
+
when :html then generate_html(data) # Never requested
|
|
827
|
+
when :markdown then generate_markdown(data) # Never requested
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Good - build what's needed now
|
|
833
|
+
class Report
|
|
834
|
+
def generate(format = :pdf)
|
|
835
|
+
data = fetch_data
|
|
836
|
+
|
|
837
|
+
case format
|
|
838
|
+
when :pdf then generate_pdf(data)
|
|
839
|
+
when :csv then generate_csv(data)
|
|
840
|
+
else raise ArgumentError, "Unsupported format: #{format}"
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
# Add other formats when/if they're actually needed
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Avoid Over-Abstraction
|
|
848
|
+
```ruby
|
|
849
|
+
# Bad - abstraction for one use case
|
|
850
|
+
class NotificationStrategyFactory
|
|
851
|
+
def self.create(type)
|
|
852
|
+
case type
|
|
853
|
+
when :email
|
|
854
|
+
EmailNotificationStrategy.new
|
|
855
|
+
end
|
|
856
|
+
# Only email exists, but we built a whole factory
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Good - just send the email
|
|
861
|
+
class NotificationService
|
|
862
|
+
def notify(user, message)
|
|
863
|
+
UserMailer.notification(user, message).deliver_later
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
# When you actually need SMS and push:
|
|
868
|
+
class NotificationService
|
|
869
|
+
def notify(user, message, via: :email)
|
|
870
|
+
case via
|
|
871
|
+
when :email then UserMailer.notification(user, message).deliver_later
|
|
872
|
+
when :sms then SmsClient.send(user.phone, message)
|
|
873
|
+
when :push then PushService.send(user.device_token, message)
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## Boy Scout Rule
|
|
882
|
+
|
|
883
|
+
### Leave Code Better Than You Found It
|
|
884
|
+
|
|
885
|
+
```ruby
|
|
886
|
+
# Before - you're fixing a bug in this method
|
|
887
|
+
def process_order(order)
|
|
888
|
+
if order.items.count > 0
|
|
889
|
+
total = 0
|
|
890
|
+
for item in order.items
|
|
891
|
+
total = total + item.price
|
|
892
|
+
end
|
|
893
|
+
order.total = total
|
|
894
|
+
order.save
|
|
895
|
+
UserMailer.order_confirmation(order.user, order).deliver
|
|
896
|
+
return true
|
|
897
|
+
else
|
|
898
|
+
return false
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# After - fixed the bug AND improved the code
|
|
903
|
+
def process_order(order)
|
|
904
|
+
return false if order.items.empty?
|
|
905
|
+
|
|
906
|
+
order.update!(total: order.items.sum(&:price))
|
|
907
|
+
OrderMailer.confirmation(order).deliver_later
|
|
908
|
+
|
|
909
|
+
true
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
# Improvements made while fixing the bug:
|
|
913
|
+
# 1. Guard clause instead of wrapping if
|
|
914
|
+
# 2. Idiomatic Ruby (empty?, sum)
|
|
915
|
+
# 3. Bang method for save (fail fast)
|
|
916
|
+
# 4. deliver_later instead of deliver
|
|
917
|
+
# 5. Simplified mailer call
|
|
918
|
+
# 6. Removed unnecessary variable
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Small Incremental Improvements
|
|
922
|
+
```ruby
|
|
923
|
+
# Don't try to rewrite everything at once
|
|
924
|
+
# Make small improvements as you touch code:
|
|
925
|
+
|
|
926
|
+
# If you see: outdated syntax, improve it
|
|
927
|
+
# Before
|
|
928
|
+
{ :key => "value" }
|
|
929
|
+
# After
|
|
930
|
+
{ key: "value" }
|
|
931
|
+
|
|
932
|
+
# If you see: unclear names, clarify them
|
|
933
|
+
# Before
|
|
934
|
+
def calc(x, y)
|
|
935
|
+
# After
|
|
936
|
+
def calculate_total(price, quantity)
|
|
937
|
+
|
|
938
|
+
# If you see: missing tests, add them
|
|
939
|
+
# Before: no tests
|
|
940
|
+
# After: at least cover the method you're modifying
|
|
941
|
+
|
|
942
|
+
# If you see: dead code, remove it
|
|
943
|
+
# Before: commented-out code from 2019
|
|
944
|
+
# After: delete it (git has history)
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## Rails-Specific Clean Code
|
|
950
|
+
|
|
951
|
+
### Fat Models, Skinny Controllers (But Not Too Fat)
|
|
952
|
+
```ruby
|
|
953
|
+
# Controller: Only HTTP concerns
|
|
954
|
+
class OrdersController < ApplicationController
|
|
955
|
+
def create
|
|
956
|
+
result = CreateOrder.call(order_params, current_user)
|
|
957
|
+
|
|
958
|
+
if result.success?
|
|
959
|
+
redirect_to result.order, notice: "Order placed!"
|
|
960
|
+
else
|
|
961
|
+
@order = result.order
|
|
962
|
+
flash.now[:alert] = result.error
|
|
963
|
+
render :new, status: :unprocessable_entity
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
private
|
|
968
|
+
|
|
969
|
+
def order_params
|
|
970
|
+
params.require(:order).permit(:shipping_address, items: [:product_id, :quantity])
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
# Service: Business logic
|
|
975
|
+
class CreateOrder
|
|
976
|
+
def self.call(params, user)
|
|
977
|
+
new(params, user).call
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
def initialize(params, user)
|
|
981
|
+
@params = params
|
|
982
|
+
@user = user
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def call
|
|
986
|
+
order = @user.orders.build(@params)
|
|
987
|
+
|
|
988
|
+
if order.valid? && process_payment(order)
|
|
989
|
+
order.save!
|
|
990
|
+
notify(order)
|
|
991
|
+
Result.new(success: true, order: order)
|
|
992
|
+
else
|
|
993
|
+
Result.new(success: false, order: order, error: order.errors.full_messages.join(", "))
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
private
|
|
998
|
+
|
|
999
|
+
def process_payment(order)
|
|
1000
|
+
# ...
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
def notify(order)
|
|
1004
|
+
# ...
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
Result = Struct.new(:success, :order, :error, keyword_init: true) do
|
|
1008
|
+
def success?
|
|
1009
|
+
success
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
# Model: Data, validations, associations, scopes
|
|
1015
|
+
class Order < ApplicationRecord
|
|
1016
|
+
belongs_to :user
|
|
1017
|
+
has_many :items, class_name: "OrderItem"
|
|
1018
|
+
|
|
1019
|
+
validates :shipping_address, presence: true
|
|
1020
|
+
|
|
1021
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
1022
|
+
scope :pending, -> { where(status: :pending) }
|
|
1023
|
+
|
|
1024
|
+
def total
|
|
1025
|
+
items.sum(&:subtotal)
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
### Use Query Objects for Complex Queries
|
|
1031
|
+
```ruby
|
|
1032
|
+
# Instead of putting complex queries in models
|
|
1033
|
+
class UserSearch
|
|
1034
|
+
def initialize(params)
|
|
1035
|
+
@params = params
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def call
|
|
1039
|
+
scope = User.all
|
|
1040
|
+
scope = filter_by_status(scope)
|
|
1041
|
+
scope = filter_by_role(scope)
|
|
1042
|
+
scope = filter_by_date_range(scope)
|
|
1043
|
+
scope = search_by_name(scope)
|
|
1044
|
+
scope = apply_sorting(scope)
|
|
1045
|
+
scope
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
private
|
|
1049
|
+
|
|
1050
|
+
def filter_by_status(scope)
|
|
1051
|
+
return scope unless @params[:status].present?
|
|
1052
|
+
scope.where(status: @params[:status])
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def filter_by_role(scope)
|
|
1056
|
+
return scope unless @params[:role].present?
|
|
1057
|
+
scope.where(role: @params[:role])
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def filter_by_date_range(scope)
|
|
1061
|
+
scope = scope.where("created_at >= ?", @params[:from]) if @params[:from]
|
|
1062
|
+
scope = scope.where("created_at <= ?", @params[:to]) if @params[:to]
|
|
1063
|
+
scope
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def search_by_name(scope)
|
|
1067
|
+
return scope unless @params[:q].present?
|
|
1068
|
+
scope.where("name ILIKE ?", "%#{@params[:q]}%")
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
def apply_sorting(scope)
|
|
1072
|
+
column = @params[:sort] || :created_at
|
|
1073
|
+
direction = @params[:direction] || :desc
|
|
1074
|
+
scope.order(column => direction)
|
|
1075
|
+
end
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
# Usage
|
|
1079
|
+
users = UserSearch.new(params).call
|
|
1080
|
+
```
|