@su-record/vibe 2.7.14 → 2.7.15
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/.env.example +37 -37
- package/CLAUDE.md +134 -126
- package/LICENSE +21 -21
- package/README.md +449 -449
- package/agents/architect-low.md +41 -41
- package/agents/architect-medium.md +59 -59
- package/agents/architect.md +80 -80
- package/agents/build-error-resolver.md +115 -115
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/docs/api-documenter.md +99 -99
- package/agents/docs/changelog-writer.md +93 -93
- package/agents/e2e-tester.md +294 -294
- package/agents/explorer-low.md +42 -42
- package/agents/explorer-medium.md +59 -59
- package/agents/explorer.md +48 -48
- package/agents/implementer-low.md +43 -43
- package/agents/implementer-medium.md +52 -52
- package/agents/implementer.md +54 -54
- package/agents/junior-mentor.md +141 -141
- package/agents/planning/requirements-analyst.md +84 -84
- package/agents/planning/ux-advisor.md +83 -83
- package/agents/qa/acceptance-tester.md +86 -86
- package/agents/qa/edge-case-finder.md +93 -93
- package/agents/refactor-cleaner.md +143 -143
- package/agents/research/best-practices-agent.md +199 -199
- package/agents/research/codebase-patterns-agent.md +157 -157
- package/agents/research/framework-docs-agent.md +188 -188
- package/agents/research/security-advisory-agent.md +213 -213
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +150 -150
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +120 -120
- package/agents/tester.md +49 -49
- package/agents/ui/ui-a11y-auditor.md +93 -93
- package/agents/ui/ui-antipattern-detector.md +94 -94
- package/agents/ui/ui-dataviz-advisor.md +69 -69
- package/agents/ui/ui-design-system-gen.md +57 -57
- package/agents/ui/ui-industry-analyzer.md +49 -49
- package/agents/ui/ui-layout-architect.md +65 -65
- package/agents/ui/ui-stack-implementer.md +68 -68
- package/agents/ui/ux-compliance-reviewer.md +81 -81
- package/agents/ui-previewer.md +258 -258
- package/commands/vibe.analyze.md +11 -13
- package/commands/vibe.review.md +43 -1
- package/commands/vibe.run.md +2124 -2078
- package/commands/vibe.spec.md +9 -4
- package/commands/vibe.spec.review.md +569 -565
- package/commands/vibe.utils.md +413 -413
- package/commands/vibe.verify.md +33 -8
- package/dist/cli/collaborator.js +52 -52
- package/dist/cli/commands/evolution.js +12 -12
- package/dist/cli/commands/info.js +54 -54
- package/dist/cli/commands/init.js +5 -5
- package/dist/cli/commands/remove.js +14 -14
- package/dist/cli/commands/sentinel.js +27 -27
- package/dist/cli/commands/skills.js +5 -5
- package/dist/cli/commands/slack.js +10 -10
- package/dist/cli/commands/telegram.js +12 -12
- package/dist/cli/detect.js +32 -32
- package/dist/cli/index.js +51 -51
- package/dist/cli/llm/claude-commands.js +16 -16
- package/dist/cli/llm/config.js +18 -18
- package/dist/cli/llm/gemini-commands.js +16 -16
- package/dist/cli/llm/gpt-commands.js +19 -19
- package/dist/cli/llm/help.js +21 -21
- package/dist/cli/postinstall/cursor-agents.js +32 -32
- package/dist/cli/postinstall/cursor-rules.js +83 -83
- package/dist/cli/postinstall/cursor-skills.js +743 -743
- package/dist/cli/setup/Provisioner.js +42 -42
- package/dist/infra/lib/DeepInit.js +24 -24
- package/dist/infra/lib/IterationTracker.js +11 -11
- package/dist/infra/lib/PythonParser.js +108 -108
- package/dist/infra/lib/ReviewRace.js +96 -96
- package/dist/infra/lib/SkillFrontmatter.js +28 -28
- package/dist/infra/lib/SkillQualityGate.js +9 -9
- package/dist/infra/lib/SkillRepository.js +159 -159
- package/dist/infra/lib/UltraQA.js +99 -99
- package/dist/infra/lib/autonomy/AuditStore.js +41 -41
- package/dist/infra/lib/autonomy/ConfirmationStore.js +30 -30
- package/dist/infra/lib/autonomy/EventOutbox.js +38 -38
- package/dist/infra/lib/autonomy/PolicyEngine.js +18 -18
- package/dist/infra/lib/autonomy/SecuritySentinel.js +1 -1
- package/dist/infra/lib/autonomy/SuggestionStore.js +33 -33
- package/dist/infra/lib/embedding/VectorStore.js +22 -22
- package/dist/infra/lib/evolution/AgentAnalyzer.js +10 -10
- package/dist/infra/lib/evolution/DescriptionOptimizer.js +21 -21
- package/dist/infra/lib/evolution/GenerationRegistry.js +36 -36
- package/dist/infra/lib/evolution/InsightStore.js +90 -90
- package/dist/infra/lib/evolution/RollbackManager.js +5 -5
- package/dist/infra/lib/evolution/SkillBenchmark.js +23 -23
- package/dist/infra/lib/evolution/SkillEvalRunner.js +50 -50
- package/dist/infra/lib/evolution/SkillGapDetector.js +10 -10
- package/dist/infra/lib/evolution/UsageTracker.js +28 -28
- package/dist/infra/lib/gemini/orchestration.js +5 -5
- package/dist/infra/lib/gpt/orchestration.js +4 -4
- package/dist/infra/lib/memory/KnowledgeGraph.js +4 -4
- package/dist/infra/lib/memory/MemorySearch.js +57 -57
- package/dist/infra/lib/memory/MemoryStorage.js +181 -181
- package/dist/infra/lib/memory/ObservationStore.js +28 -28
- package/dist/infra/lib/memory/ReflectionStore.js +30 -30
- package/dist/infra/lib/memory/SessionRAGRetriever.js +7 -7
- package/dist/infra/lib/memory/SessionRAGStore.js +225 -225
- package/dist/infra/lib/memory/SessionSummarizer.js +9 -9
- package/dist/infra/orchestrator/AgentManager.js +12 -12
- package/dist/infra/orchestrator/AgentRegistry.js +65 -65
- package/dist/infra/orchestrator/MultiLlmResearch.js +8 -8
- package/dist/infra/orchestrator/SwarmOrchestrator.test.js +16 -16
- package/dist/infra/orchestrator/parallelResearch.js +24 -24
- package/dist/tools/convention/analyzeComplexity.test.js +115 -115
- package/dist/tools/convention/validateCodeQuality.test.js +104 -104
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +23 -23
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/dist/tools/semantic/astGrep.test.js +6 -6
- package/dist/tools/spec/prdParser.test.js +171 -171
- package/dist/tools/spec/specGenerator.js +169 -169
- package/dist/tools/spec/traceabilityMatrix.js +64 -64
- package/dist/tools/spec/traceabilityMatrix.test.js +28 -28
- package/hooks/gemini-hooks.json +73 -73
- package/hooks/hooks.json +137 -137
- package/hooks/scripts/code-check.js +77 -70
- package/hooks/scripts/context-save.js +212 -212
- package/hooks/scripts/hud-status.js +291 -291
- package/hooks/scripts/keyword-detector.js +214 -214
- package/hooks/scripts/llm-orchestrate.js +475 -475
- package/hooks/scripts/post-edit.js +32 -32
- package/hooks/scripts/pre-tool-guard.js +125 -125
- package/hooks/scripts/prompt-dispatcher.js +185 -185
- package/hooks/scripts/sentinel-guard.js +104 -104
- package/hooks/scripts/session-start.js +106 -106
- package/hooks/scripts/stop-notify.js +209 -209
- package/hooks/scripts/utils.js +100 -100
- package/languages/csharp-unity.md +515 -515
- package/languages/gdscript-godot.md +470 -470
- package/languages/ruby-rails.md +489 -489
- package/languages/typescript-angular.md +433 -433
- package/languages/typescript-astro.md +416 -416
- package/languages/typescript-electron.md +406 -406
- package/languages/typescript-nestjs.md +524 -524
- package/languages/typescript-svelte.md +407 -407
- package/languages/typescript-tauri.md +365 -365
- package/package.json +121 -121
- package/skills/agents-md/SKILL.md +120 -120
- package/skills/arch-guard/SKILL.md +180 -180
- package/skills/brand-assets/SKILL.md +146 -146
- package/skills/capability-loop/SKILL.md +167 -167
- package/skills/characterization-test/SKILL.md +206 -206
- package/skills/commerce-patterns/SKILL.md +59 -59
- package/skills/commit-push-pr/SKILL.md +75 -75
- package/skills/context7-usage/SKILL.md +105 -105
- package/skills/core-capabilities/SKILL.md +48 -48
- package/skills/e2e-commerce/SKILL.md +57 -57
- package/skills/exec-plan/SKILL.md +147 -147
- package/skills/frontend-design/SKILL.md +73 -73
- package/skills/git-worktree/SKILL.md +72 -72
- package/skills/handoff/SKILL.md +109 -109
- package/skills/parallel-research/SKILL.md +87 -87
- package/skills/priority-todos/SKILL.md +63 -63
- package/skills/seo-checklist/SKILL.md +57 -57
- package/skills/techdebt/SKILL.md +122 -122
- package/skills/tool-fallback/SKILL.md +103 -103
- package/skills/typescript-advanced-types/SKILL.md +66 -66
- package/skills/ui-ux-pro-max/SKILL.md +206 -206
- package/skills/vercel-react-best-practices/SKILL.md +59 -59
- package/skills/video-production/SKILL.md +51 -51
- package/vibe/config.json +29 -29
- package/vibe/constitution.md +227 -227
- package/vibe/rules/principles/communication-guide.md +98 -98
- package/vibe/rules/principles/development-philosophy.md +52 -52
- package/vibe/rules/principles/quick-start.md +102 -102
- package/vibe/rules/quality/bdd-contract-testing.md +393 -393
- package/vibe/rules/quality/checklist.md +276 -276
- package/vibe/rules/quality/performance.md +236 -236
- package/vibe/rules/quality/testing-strategy.md +440 -440
- package/vibe/rules/standards/anti-patterns.md +541 -541
- package/vibe/rules/standards/code-structure.md +291 -291
- package/vibe/rules/standards/complexity-metrics.md +313 -313
- package/vibe/rules/standards/git-workflow.md +237 -237
- package/vibe/rules/standards/naming-conventions.md +198 -198
- package/vibe/rules/standards/security.md +305 -305
- package/vibe/rules/writing/document-style.md +74 -74
- package/vibe/setup.sh +31 -31
- package/vibe/templates/constitution-template.md +252 -252
- package/vibe/templates/contract-backend-template.md +526 -526
- package/vibe/templates/contract-frontend-template.md +599 -599
- package/vibe/templates/feature-template.md +96 -96
- package/vibe/templates/spec-template.md +221 -221
- package/vibe/ui-ux-data/charts.csv +26 -26
- package/vibe/ui-ux-data/colors.csv +97 -97
- package/vibe/ui-ux-data/icons.csv +101 -101
- package/vibe/ui-ux-data/landing.csv +31 -31
- package/vibe/ui-ux-data/products.csv +96 -96
- package/vibe/ui-ux-data/react-performance.csv +45 -45
- package/vibe/ui-ux-data/stacks/astro.csv +54 -54
- package/vibe/ui-ux-data/stacks/flutter.csv +53 -53
- package/vibe/ui-ux-data/stacks/html-tailwind.csv +56 -56
- package/vibe/ui-ux-data/stacks/jetpack-compose.csv +53 -53
- package/vibe/ui-ux-data/stacks/nextjs.csv +53 -53
- package/vibe/ui-ux-data/stacks/nuxt-ui.csv +51 -51
- package/vibe/ui-ux-data/stacks/nuxtjs.csv +59 -59
- package/vibe/ui-ux-data/stacks/react-native.csv +52 -52
- package/vibe/ui-ux-data/stacks/react.csv +54 -54
- package/vibe/ui-ux-data/stacks/shadcn.csv +61 -61
- package/vibe/ui-ux-data/stacks/svelte.csv +54 -54
- package/vibe/ui-ux-data/stacks/swiftui.csv +51 -51
- package/vibe/ui-ux-data/stacks/vue.csv +50 -50
- package/vibe/ui-ux-data/styles.csv +68 -68
- package/vibe/ui-ux-data/typography.csv +57 -57
- package/vibe/ui-ux-data/ui-reasoning.csv +101 -101
- package/vibe/ui-ux-data/ux-guidelines.csv +99 -99
- package/vibe/ui-ux-data/version.json +31 -31
- package/vibe/ui-ux-data/web-interface.csv +31 -31
package/languages/ruby-rails.md
CHANGED
|
@@ -1,489 +1,489 @@
|
|
|
1
|
-
# 💎 Ruby on Rails Quality Rules
|
|
2
|
-
|
|
3
|
-
## Core Principles (inherited from core)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ Single Responsibility (SRP)
|
|
7
|
-
✅ Don't Repeat Yourself (DRY)
|
|
8
|
-
✅ Reusability
|
|
9
|
-
✅ Low Complexity
|
|
10
|
-
✅ Methods ≤ 30 lines
|
|
11
|
-
✅ Nesting ≤ 3 levels
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Rails Architecture (MVC)
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
┌─────────────────────────────────────────────┐
|
|
19
|
-
│ Controller (Request Handling) │
|
|
20
|
-
│ - Thin controllers │
|
|
21
|
-
│ - Delegate to services │
|
|
22
|
-
├─────────────────────────────────────────────┤
|
|
23
|
-
│ Model (Business Logic + Data) │
|
|
24
|
-
│ - Validations, associations │
|
|
25
|
-
│ - Scopes, callbacks │
|
|
26
|
-
├─────────────────────────────────────────────┤
|
|
27
|
-
│ View (Presentation) │
|
|
28
|
-
│ - ERB, Slim, or ViewComponents │
|
|
29
|
-
│ - Helpers, partials │
|
|
30
|
-
└─────────────────────────────────────────────┘
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Rails Patterns
|
|
34
|
-
|
|
35
|
-
### 1. Controller Pattern
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
# ✅ Thin Controller
|
|
39
|
-
class UsersController < ApplicationController
|
|
40
|
-
before_action :authenticate_user!
|
|
41
|
-
before_action :set_user, only: [:show, :edit, :update, :destroy]
|
|
42
|
-
|
|
43
|
-
def index
|
|
44
|
-
@users = User.active.order(created_at: :desc).page(params[:page])
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def show
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def create
|
|
51
|
-
result = Users::CreateService.call(user_params)
|
|
52
|
-
|
|
53
|
-
if result.success?
|
|
54
|
-
redirect_to result.user, notice: 'User created successfully.'
|
|
55
|
-
else
|
|
56
|
-
@user = result.user
|
|
57
|
-
render :new, status: :unprocessable_entity
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def update
|
|
62
|
-
if @user.update(user_params)
|
|
63
|
-
redirect_to @user, notice: 'User updated successfully.'
|
|
64
|
-
else
|
|
65
|
-
render :edit, status: :unprocessable_entity
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def destroy
|
|
70
|
-
@user.destroy
|
|
71
|
-
redirect_to users_url, notice: 'User deleted.'
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def set_user
|
|
77
|
-
@user = User.find(params[:id])
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def user_params
|
|
81
|
-
params.require(:user).permit(:name, :email, :role)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# ❌ Fat Controller (avoid)
|
|
86
|
-
class UsersController < ApplicationController
|
|
87
|
-
def create
|
|
88
|
-
@user = User.new(user_params)
|
|
89
|
-
@user.generate_token
|
|
90
|
-
@user.send_welcome_email
|
|
91
|
-
@user.notify_admins
|
|
92
|
-
@user.create_default_settings
|
|
93
|
-
# Too much logic in controller
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 2. Model Pattern
|
|
99
|
-
|
|
100
|
-
```ruby
|
|
101
|
-
# ✅ Model with proper organization
|
|
102
|
-
class User < ApplicationRecord
|
|
103
|
-
# Constants
|
|
104
|
-
ROLES = %w[admin member guest].freeze
|
|
105
|
-
|
|
106
|
-
# Associations
|
|
107
|
-
belongs_to :organization
|
|
108
|
-
has_many :posts, dependent: :destroy
|
|
109
|
-
has_many :comments, dependent: :destroy
|
|
110
|
-
has_one :profile, dependent: :destroy
|
|
111
|
-
|
|
112
|
-
# Validations
|
|
113
|
-
validates :email, presence: true, uniqueness: { case_sensitive: false }
|
|
114
|
-
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
115
|
-
validates :role, inclusion: { in: ROLES }
|
|
116
|
-
|
|
117
|
-
# Scopes
|
|
118
|
-
scope :active, -> { where(active: true) }
|
|
119
|
-
scope :admins, -> { where(role: 'admin') }
|
|
120
|
-
scope :recent, -> { order(created_at: :desc) }
|
|
121
|
-
scope :search, ->(query) { where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%") }
|
|
122
|
-
|
|
123
|
-
# Callbacks (use sparingly)
|
|
124
|
-
before_validation :normalize_email
|
|
125
|
-
after_create :send_welcome_email
|
|
126
|
-
|
|
127
|
-
# Instance methods
|
|
128
|
-
def full_name
|
|
129
|
-
"#{first_name} #{last_name}".strip
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def admin?
|
|
133
|
-
role == 'admin'
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
private
|
|
137
|
-
|
|
138
|
-
def normalize_email
|
|
139
|
-
self.email = email.downcase.strip if email.present?
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def send_welcome_email
|
|
143
|
-
UserMailer.welcome(self).deliver_later
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 3. Service Object Pattern
|
|
149
|
-
|
|
150
|
-
```ruby
|
|
151
|
-
# app/services/users/create_service.rb
|
|
152
|
-
module Users
|
|
153
|
-
class CreateService
|
|
154
|
-
include Callable
|
|
155
|
-
|
|
156
|
-
def initialize(params, current_user: nil)
|
|
157
|
-
@params = params
|
|
158
|
-
@current_user = current_user
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def call
|
|
162
|
-
user = User.new(@params)
|
|
163
|
-
user.created_by = @current_user
|
|
164
|
-
|
|
165
|
-
if user.save
|
|
166
|
-
send_notifications(user)
|
|
167
|
-
Result.success(user: user)
|
|
168
|
-
else
|
|
169
|
-
Result.failure(user: user, errors: user.errors)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
private
|
|
174
|
-
|
|
175
|
-
def send_notifications(user)
|
|
176
|
-
UserMailer.welcome(user).deliver_later
|
|
177
|
-
AdminNotifier.new_user(user).deliver_later
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# app/services/concerns/callable.rb
|
|
183
|
-
module Callable
|
|
184
|
-
extend ActiveSupport::Concern
|
|
185
|
-
|
|
186
|
-
class_methods do
|
|
187
|
-
def call(...)
|
|
188
|
-
new(...).call
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# app/services/result.rb
|
|
194
|
-
class Result
|
|
195
|
-
attr_reader :data, :errors
|
|
196
|
-
|
|
197
|
-
def initialize(success:, data: {}, errors: [])
|
|
198
|
-
@success = success
|
|
199
|
-
@data = data
|
|
200
|
-
@errors = errors
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def success?
|
|
204
|
-
@success
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def failure?
|
|
208
|
-
!success?
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def method_missing(method_name, *args)
|
|
212
|
-
@data[method_name] || super
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def self.success(data = {})
|
|
216
|
-
new(success: true, data: data)
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def self.failure(errors: [], **data)
|
|
220
|
-
new(success: false, data: data, errors: errors)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### 4. Query Object Pattern
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
# app/queries/users_query.rb
|
|
229
|
-
class UsersQuery
|
|
230
|
-
def initialize(relation = User.all)
|
|
231
|
-
@relation = relation
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def call(params = {})
|
|
235
|
-
@relation
|
|
236
|
-
.then { |r| filter_by_status(r, params[:status]) }
|
|
237
|
-
.then { |r| filter_by_role(r, params[:role]) }
|
|
238
|
-
.then { |r| search(r, params[:search]) }
|
|
239
|
-
.then { |r| sort(r, params[:sort]) }
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
private
|
|
243
|
-
|
|
244
|
-
def filter_by_status(relation, status)
|
|
245
|
-
return relation if status.blank?
|
|
246
|
-
|
|
247
|
-
case status
|
|
248
|
-
when 'active' then relation.active
|
|
249
|
-
when 'inactive' then relation.inactive
|
|
250
|
-
else relation
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def filter_by_role(relation, role)
|
|
255
|
-
return relation if role.blank?
|
|
256
|
-
|
|
257
|
-
relation.where(role: role)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def search(relation, query)
|
|
261
|
-
return relation if query.blank?
|
|
262
|
-
|
|
263
|
-
relation.search(query)
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def sort(relation, sort_param)
|
|
267
|
-
return relation.recent if sort_param.blank?
|
|
268
|
-
|
|
269
|
-
case sort_param
|
|
270
|
-
when 'name' then relation.order(:name)
|
|
271
|
-
when 'email' then relation.order(:email)
|
|
272
|
-
when 'oldest' then relation.order(:created_at)
|
|
273
|
-
else relation.recent
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Usage in controller
|
|
279
|
-
def index
|
|
280
|
-
@users = UsersQuery.new.call(params).page(params[:page])
|
|
281
|
-
end
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### 5. Form Object Pattern
|
|
285
|
-
|
|
286
|
-
```ruby
|
|
287
|
-
# app/forms/user_registration_form.rb
|
|
288
|
-
class UserRegistrationForm
|
|
289
|
-
include ActiveModel::Model
|
|
290
|
-
include ActiveModel::Attributes
|
|
291
|
-
|
|
292
|
-
attribute :name, :string
|
|
293
|
-
attribute :email, :string
|
|
294
|
-
attribute :password, :string
|
|
295
|
-
attribute :password_confirmation, :string
|
|
296
|
-
attribute :terms_accepted, :boolean
|
|
297
|
-
|
|
298
|
-
validates :name, presence: true, length: { minimum: 2 }
|
|
299
|
-
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
300
|
-
validates :password, presence: true, length: { minimum: 8 }
|
|
301
|
-
validates :password_confirmation, presence: true
|
|
302
|
-
validate :passwords_match
|
|
303
|
-
validate :terms_must_be_accepted
|
|
304
|
-
|
|
305
|
-
def save
|
|
306
|
-
return false unless valid?
|
|
307
|
-
|
|
308
|
-
ActiveRecord::Base.transaction do
|
|
309
|
-
@user = User.create!(
|
|
310
|
-
name: name,
|
|
311
|
-
email: email,
|
|
312
|
-
password: password
|
|
313
|
-
)
|
|
314
|
-
@user.create_profile!
|
|
315
|
-
send_welcome_email
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
true
|
|
319
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
320
|
-
errors.add(:base, e.message)
|
|
321
|
-
false
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def user
|
|
325
|
-
@user
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
private
|
|
329
|
-
|
|
330
|
-
def passwords_match
|
|
331
|
-
return if password == password_confirmation
|
|
332
|
-
|
|
333
|
-
errors.add(:password_confirmation, "doesn't match password")
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
def terms_must_be_accepted
|
|
337
|
-
return if terms_accepted
|
|
338
|
-
|
|
339
|
-
errors.add(:terms_accepted, 'must be accepted')
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def send_welcome_email
|
|
343
|
-
UserMailer.welcome(@user).deliver_later
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### 6. API Controllers
|
|
349
|
-
|
|
350
|
-
```ruby
|
|
351
|
-
# app/controllers/api/v1/base_controller.rb
|
|
352
|
-
module Api
|
|
353
|
-
module V1
|
|
354
|
-
class BaseController < ApplicationController
|
|
355
|
-
skip_before_action :verify_authenticity_token
|
|
356
|
-
before_action :authenticate_api_user!
|
|
357
|
-
|
|
358
|
-
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
359
|
-
rescue_from ActionController::ParameterMissing, with: :bad_request
|
|
360
|
-
|
|
361
|
-
private
|
|
362
|
-
|
|
363
|
-
def authenticate_api_user!
|
|
364
|
-
token = request.headers['Authorization']&.split(' ')&.last
|
|
365
|
-
@current_user = User.find_by(api_token: token)
|
|
366
|
-
|
|
367
|
-
render_unauthorized unless @current_user
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
def render_success(data, status: :ok)
|
|
371
|
-
render json: { success: true, data: data }, status: status
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def render_error(message, status: :unprocessable_entity)
|
|
375
|
-
render json: { success: false, error: message }, status: status
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def render_unauthorized
|
|
379
|
-
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def not_found
|
|
383
|
-
render json: { error: 'Not found' }, status: :not_found
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def bad_request(exception)
|
|
387
|
-
render json: { error: exception.message }, status: :bad_request
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
# app/controllers/api/v1/users_controller.rb
|
|
394
|
-
module Api
|
|
395
|
-
module V1
|
|
396
|
-
class UsersController < BaseController
|
|
397
|
-
def index
|
|
398
|
-
users = UsersQuery.new.call(params)
|
|
399
|
-
render_success(users.map { |u| UserSerializer.new(u).as_json })
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
def show
|
|
403
|
-
user = User.find(params[:id])
|
|
404
|
-
render_success(UserSerializer.new(user).as_json)
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def create
|
|
408
|
-
result = Users::CreateService.call(user_params)
|
|
409
|
-
|
|
410
|
-
if result.success?
|
|
411
|
-
render_success(UserSerializer.new(result.user).as_json, status: :created)
|
|
412
|
-
else
|
|
413
|
-
render_error(result.errors.full_messages)
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
private
|
|
418
|
-
|
|
419
|
-
def user_params
|
|
420
|
-
params.require(:user).permit(:name, :email, :role)
|
|
421
|
-
end
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### 7. Background Jobs
|
|
428
|
-
|
|
429
|
-
```ruby
|
|
430
|
-
# app/jobs/user_notification_job.rb
|
|
431
|
-
class UserNotificationJob < ApplicationJob
|
|
432
|
-
queue_as :default
|
|
433
|
-
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
|
434
|
-
|
|
435
|
-
def perform(user_id, notification_type)
|
|
436
|
-
user = User.find(user_id)
|
|
437
|
-
|
|
438
|
-
case notification_type
|
|
439
|
-
when 'welcome'
|
|
440
|
-
UserMailer.welcome(user).deliver_now
|
|
441
|
-
when 'reminder'
|
|
442
|
-
UserMailer.reminder(user).deliver_now
|
|
443
|
-
end
|
|
444
|
-
rescue ActiveRecord::RecordNotFound
|
|
445
|
-
# User was deleted, skip
|
|
446
|
-
Rails.logger.warn("User #{user_id} not found for notification")
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
## Project Structure
|
|
452
|
-
|
|
453
|
-
```
|
|
454
|
-
app/
|
|
455
|
-
├── controllers/
|
|
456
|
-
│ ├── api/
|
|
457
|
-
│ │ └── v1/
|
|
458
|
-
│ ├── concerns/
|
|
459
|
-
│ └── application_controller.rb
|
|
460
|
-
├── models/
|
|
461
|
-
│ └── concerns/
|
|
462
|
-
├── services/
|
|
463
|
-
│ ├── concerns/
|
|
464
|
-
│ │ └── callable.rb
|
|
465
|
-
│ └── users/
|
|
466
|
-
│ └── create_service.rb
|
|
467
|
-
├── queries/
|
|
468
|
-
│ └── users_query.rb
|
|
469
|
-
├── forms/
|
|
470
|
-
│ └── user_registration_form.rb
|
|
471
|
-
├── serializers/
|
|
472
|
-
│ └── user_serializer.rb
|
|
473
|
-
├── jobs/
|
|
474
|
-
├── mailers/
|
|
475
|
-
└── views/
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
## Checklist
|
|
479
|
-
|
|
480
|
-
- [ ] Thin controllers, fat models (but not too fat)
|
|
481
|
-
- [ ] Use Service Objects for complex operations
|
|
482
|
-
- [ ] Use Query Objects for complex queries
|
|
483
|
-
- [ ] Use Form Objects for complex forms
|
|
484
|
-
- [ ] Use Strong Parameters
|
|
485
|
-
- [ ] Add proper validations
|
|
486
|
-
- [ ] Use scopes for reusable queries
|
|
487
|
-
- [ ] Background jobs for slow operations
|
|
488
|
-
- [ ] Proper error handling and logging
|
|
489
|
-
- [ ] N+1 query prevention (includes, preload)
|
|
1
|
+
# 💎 Ruby on Rails Quality Rules
|
|
2
|
+
|
|
3
|
+
## Core Principles (inherited from core)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ Single Responsibility (SRP)
|
|
7
|
+
✅ Don't Repeat Yourself (DRY)
|
|
8
|
+
✅ Reusability
|
|
9
|
+
✅ Low Complexity
|
|
10
|
+
✅ Methods ≤ 30 lines
|
|
11
|
+
✅ Nesting ≤ 3 levels
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Rails Architecture (MVC)
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────┐
|
|
19
|
+
│ Controller (Request Handling) │
|
|
20
|
+
│ - Thin controllers │
|
|
21
|
+
│ - Delegate to services │
|
|
22
|
+
├─────────────────────────────────────────────┤
|
|
23
|
+
│ Model (Business Logic + Data) │
|
|
24
|
+
│ - Validations, associations │
|
|
25
|
+
│ - Scopes, callbacks │
|
|
26
|
+
├─────────────────────────────────────────────┤
|
|
27
|
+
│ View (Presentation) │
|
|
28
|
+
│ - ERB, Slim, or ViewComponents │
|
|
29
|
+
│ - Helpers, partials │
|
|
30
|
+
└─────────────────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Rails Patterns
|
|
34
|
+
|
|
35
|
+
### 1. Controller Pattern
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# ✅ Thin Controller
|
|
39
|
+
class UsersController < ApplicationController
|
|
40
|
+
before_action :authenticate_user!
|
|
41
|
+
before_action :set_user, only: [:show, :edit, :update, :destroy]
|
|
42
|
+
|
|
43
|
+
def index
|
|
44
|
+
@users = User.active.order(created_at: :desc).page(params[:page])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def show
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create
|
|
51
|
+
result = Users::CreateService.call(user_params)
|
|
52
|
+
|
|
53
|
+
if result.success?
|
|
54
|
+
redirect_to result.user, notice: 'User created successfully.'
|
|
55
|
+
else
|
|
56
|
+
@user = result.user
|
|
57
|
+
render :new, status: :unprocessable_entity
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update
|
|
62
|
+
if @user.update(user_params)
|
|
63
|
+
redirect_to @user, notice: 'User updated successfully.'
|
|
64
|
+
else
|
|
65
|
+
render :edit, status: :unprocessable_entity
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def destroy
|
|
70
|
+
@user.destroy
|
|
71
|
+
redirect_to users_url, notice: 'User deleted.'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def set_user
|
|
77
|
+
@user = User.find(params[:id])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def user_params
|
|
81
|
+
params.require(:user).permit(:name, :email, :role)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ❌ Fat Controller (avoid)
|
|
86
|
+
class UsersController < ApplicationController
|
|
87
|
+
def create
|
|
88
|
+
@user = User.new(user_params)
|
|
89
|
+
@user.generate_token
|
|
90
|
+
@user.send_welcome_email
|
|
91
|
+
@user.notify_admins
|
|
92
|
+
@user.create_default_settings
|
|
93
|
+
# Too much logic in controller
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Model Pattern
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# ✅ Model with proper organization
|
|
102
|
+
class User < ApplicationRecord
|
|
103
|
+
# Constants
|
|
104
|
+
ROLES = %w[admin member guest].freeze
|
|
105
|
+
|
|
106
|
+
# Associations
|
|
107
|
+
belongs_to :organization
|
|
108
|
+
has_many :posts, dependent: :destroy
|
|
109
|
+
has_many :comments, dependent: :destroy
|
|
110
|
+
has_one :profile, dependent: :destroy
|
|
111
|
+
|
|
112
|
+
# Validations
|
|
113
|
+
validates :email, presence: true, uniqueness: { case_sensitive: false }
|
|
114
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
115
|
+
validates :role, inclusion: { in: ROLES }
|
|
116
|
+
|
|
117
|
+
# Scopes
|
|
118
|
+
scope :active, -> { where(active: true) }
|
|
119
|
+
scope :admins, -> { where(role: 'admin') }
|
|
120
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
121
|
+
scope :search, ->(query) { where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%") }
|
|
122
|
+
|
|
123
|
+
# Callbacks (use sparingly)
|
|
124
|
+
before_validation :normalize_email
|
|
125
|
+
after_create :send_welcome_email
|
|
126
|
+
|
|
127
|
+
# Instance methods
|
|
128
|
+
def full_name
|
|
129
|
+
"#{first_name} #{last_name}".strip
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def admin?
|
|
133
|
+
role == 'admin'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def normalize_email
|
|
139
|
+
self.email = email.downcase.strip if email.present?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def send_welcome_email
|
|
143
|
+
UserMailer.welcome(self).deliver_later
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. Service Object Pattern
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# app/services/users/create_service.rb
|
|
152
|
+
module Users
|
|
153
|
+
class CreateService
|
|
154
|
+
include Callable
|
|
155
|
+
|
|
156
|
+
def initialize(params, current_user: nil)
|
|
157
|
+
@params = params
|
|
158
|
+
@current_user = current_user
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def call
|
|
162
|
+
user = User.new(@params)
|
|
163
|
+
user.created_by = @current_user
|
|
164
|
+
|
|
165
|
+
if user.save
|
|
166
|
+
send_notifications(user)
|
|
167
|
+
Result.success(user: user)
|
|
168
|
+
else
|
|
169
|
+
Result.failure(user: user, errors: user.errors)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def send_notifications(user)
|
|
176
|
+
UserMailer.welcome(user).deliver_later
|
|
177
|
+
AdminNotifier.new_user(user).deliver_later
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# app/services/concerns/callable.rb
|
|
183
|
+
module Callable
|
|
184
|
+
extend ActiveSupport::Concern
|
|
185
|
+
|
|
186
|
+
class_methods do
|
|
187
|
+
def call(...)
|
|
188
|
+
new(...).call
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# app/services/result.rb
|
|
194
|
+
class Result
|
|
195
|
+
attr_reader :data, :errors
|
|
196
|
+
|
|
197
|
+
def initialize(success:, data: {}, errors: [])
|
|
198
|
+
@success = success
|
|
199
|
+
@data = data
|
|
200
|
+
@errors = errors
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def success?
|
|
204
|
+
@success
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def failure?
|
|
208
|
+
!success?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def method_missing(method_name, *args)
|
|
212
|
+
@data[method_name] || super
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.success(data = {})
|
|
216
|
+
new(success: true, data: data)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.failure(errors: [], **data)
|
|
220
|
+
new(success: false, data: data, errors: errors)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 4. Query Object Pattern
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# app/queries/users_query.rb
|
|
229
|
+
class UsersQuery
|
|
230
|
+
def initialize(relation = User.all)
|
|
231
|
+
@relation = relation
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def call(params = {})
|
|
235
|
+
@relation
|
|
236
|
+
.then { |r| filter_by_status(r, params[:status]) }
|
|
237
|
+
.then { |r| filter_by_role(r, params[:role]) }
|
|
238
|
+
.then { |r| search(r, params[:search]) }
|
|
239
|
+
.then { |r| sort(r, params[:sort]) }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def filter_by_status(relation, status)
|
|
245
|
+
return relation if status.blank?
|
|
246
|
+
|
|
247
|
+
case status
|
|
248
|
+
when 'active' then relation.active
|
|
249
|
+
when 'inactive' then relation.inactive
|
|
250
|
+
else relation
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def filter_by_role(relation, role)
|
|
255
|
+
return relation if role.blank?
|
|
256
|
+
|
|
257
|
+
relation.where(role: role)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def search(relation, query)
|
|
261
|
+
return relation if query.blank?
|
|
262
|
+
|
|
263
|
+
relation.search(query)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def sort(relation, sort_param)
|
|
267
|
+
return relation.recent if sort_param.blank?
|
|
268
|
+
|
|
269
|
+
case sort_param
|
|
270
|
+
when 'name' then relation.order(:name)
|
|
271
|
+
when 'email' then relation.order(:email)
|
|
272
|
+
when 'oldest' then relation.order(:created_at)
|
|
273
|
+
else relation.recent
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Usage in controller
|
|
279
|
+
def index
|
|
280
|
+
@users = UsersQuery.new.call(params).page(params[:page])
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 5. Form Object Pattern
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# app/forms/user_registration_form.rb
|
|
288
|
+
class UserRegistrationForm
|
|
289
|
+
include ActiveModel::Model
|
|
290
|
+
include ActiveModel::Attributes
|
|
291
|
+
|
|
292
|
+
attribute :name, :string
|
|
293
|
+
attribute :email, :string
|
|
294
|
+
attribute :password, :string
|
|
295
|
+
attribute :password_confirmation, :string
|
|
296
|
+
attribute :terms_accepted, :boolean
|
|
297
|
+
|
|
298
|
+
validates :name, presence: true, length: { minimum: 2 }
|
|
299
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
300
|
+
validates :password, presence: true, length: { minimum: 8 }
|
|
301
|
+
validates :password_confirmation, presence: true
|
|
302
|
+
validate :passwords_match
|
|
303
|
+
validate :terms_must_be_accepted
|
|
304
|
+
|
|
305
|
+
def save
|
|
306
|
+
return false unless valid?
|
|
307
|
+
|
|
308
|
+
ActiveRecord::Base.transaction do
|
|
309
|
+
@user = User.create!(
|
|
310
|
+
name: name,
|
|
311
|
+
email: email,
|
|
312
|
+
password: password
|
|
313
|
+
)
|
|
314
|
+
@user.create_profile!
|
|
315
|
+
send_welcome_email
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
true
|
|
319
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
320
|
+
errors.add(:base, e.message)
|
|
321
|
+
false
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def user
|
|
325
|
+
@user
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
private
|
|
329
|
+
|
|
330
|
+
def passwords_match
|
|
331
|
+
return if password == password_confirmation
|
|
332
|
+
|
|
333
|
+
errors.add(:password_confirmation, "doesn't match password")
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def terms_must_be_accepted
|
|
337
|
+
return if terms_accepted
|
|
338
|
+
|
|
339
|
+
errors.add(:terms_accepted, 'must be accepted')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def send_welcome_email
|
|
343
|
+
UserMailer.welcome(@user).deliver_later
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 6. API Controllers
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# app/controllers/api/v1/base_controller.rb
|
|
352
|
+
module Api
|
|
353
|
+
module V1
|
|
354
|
+
class BaseController < ApplicationController
|
|
355
|
+
skip_before_action :verify_authenticity_token
|
|
356
|
+
before_action :authenticate_api_user!
|
|
357
|
+
|
|
358
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
359
|
+
rescue_from ActionController::ParameterMissing, with: :bad_request
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
def authenticate_api_user!
|
|
364
|
+
token = request.headers['Authorization']&.split(' ')&.last
|
|
365
|
+
@current_user = User.find_by(api_token: token)
|
|
366
|
+
|
|
367
|
+
render_unauthorized unless @current_user
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def render_success(data, status: :ok)
|
|
371
|
+
render json: { success: true, data: data }, status: status
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def render_error(message, status: :unprocessable_entity)
|
|
375
|
+
render json: { success: false, error: message }, status: status
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def render_unauthorized
|
|
379
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def not_found
|
|
383
|
+
render json: { error: 'Not found' }, status: :not_found
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def bad_request(exception)
|
|
387
|
+
render json: { error: exception.message }, status: :bad_request
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# app/controllers/api/v1/users_controller.rb
|
|
394
|
+
module Api
|
|
395
|
+
module V1
|
|
396
|
+
class UsersController < BaseController
|
|
397
|
+
def index
|
|
398
|
+
users = UsersQuery.new.call(params)
|
|
399
|
+
render_success(users.map { |u| UserSerializer.new(u).as_json })
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def show
|
|
403
|
+
user = User.find(params[:id])
|
|
404
|
+
render_success(UserSerializer.new(user).as_json)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def create
|
|
408
|
+
result = Users::CreateService.call(user_params)
|
|
409
|
+
|
|
410
|
+
if result.success?
|
|
411
|
+
render_success(UserSerializer.new(result.user).as_json, status: :created)
|
|
412
|
+
else
|
|
413
|
+
render_error(result.errors.full_messages)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def user_params
|
|
420
|
+
params.require(:user).permit(:name, :email, :role)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 7. Background Jobs
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
# app/jobs/user_notification_job.rb
|
|
431
|
+
class UserNotificationJob < ApplicationJob
|
|
432
|
+
queue_as :default
|
|
433
|
+
retry_on StandardError, wait: :exponentially_longer, attempts: 3
|
|
434
|
+
|
|
435
|
+
def perform(user_id, notification_type)
|
|
436
|
+
user = User.find(user_id)
|
|
437
|
+
|
|
438
|
+
case notification_type
|
|
439
|
+
when 'welcome'
|
|
440
|
+
UserMailer.welcome(user).deliver_now
|
|
441
|
+
when 'reminder'
|
|
442
|
+
UserMailer.reminder(user).deliver_now
|
|
443
|
+
end
|
|
444
|
+
rescue ActiveRecord::RecordNotFound
|
|
445
|
+
# User was deleted, skip
|
|
446
|
+
Rails.logger.warn("User #{user_id} not found for notification")
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Project Structure
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
app/
|
|
455
|
+
├── controllers/
|
|
456
|
+
│ ├── api/
|
|
457
|
+
│ │ └── v1/
|
|
458
|
+
│ ├── concerns/
|
|
459
|
+
│ └── application_controller.rb
|
|
460
|
+
├── models/
|
|
461
|
+
│ └── concerns/
|
|
462
|
+
├── services/
|
|
463
|
+
│ ├── concerns/
|
|
464
|
+
│ │ └── callable.rb
|
|
465
|
+
│ └── users/
|
|
466
|
+
│ └── create_service.rb
|
|
467
|
+
├── queries/
|
|
468
|
+
│ └── users_query.rb
|
|
469
|
+
├── forms/
|
|
470
|
+
│ └── user_registration_form.rb
|
|
471
|
+
├── serializers/
|
|
472
|
+
│ └── user_serializer.rb
|
|
473
|
+
├── jobs/
|
|
474
|
+
├── mailers/
|
|
475
|
+
└── views/
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Checklist
|
|
479
|
+
|
|
480
|
+
- [ ] Thin controllers, fat models (but not too fat)
|
|
481
|
+
- [ ] Use Service Objects for complex operations
|
|
482
|
+
- [ ] Use Query Objects for complex queries
|
|
483
|
+
- [ ] Use Form Objects for complex forms
|
|
484
|
+
- [ ] Use Strong Parameters
|
|
485
|
+
- [ ] Add proper validations
|
|
486
|
+
- [ ] Use scopes for reusable queries
|
|
487
|
+
- [ ] Background jobs for slow operations
|
|
488
|
+
- [ ] Proper error handling and logging
|
|
489
|
+
- [ ] N+1 query prevention (includes, preload)
|