@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: responsive-design
|
|
3
|
+
description: Responsive design patterns with mobile-first CSS, container queries, fluid typography, and breakpoint strategy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Responsive Design
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply to every web interface. Over 60% of web traffic is mobile. Responsive design ensures your UI works on phones (320px), tablets (768px), laptops (1024px), and ultrawide monitors (2560px) without separate codebases. Start mobile-first and progressively enhance.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Mobile-First Approach
|
|
14
|
+
|
|
15
|
+
Write base styles for the smallest screen, then add complexity with `min-width` media queries:
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
/* Base -- mobile (320px+) */
|
|
19
|
+
.card-grid {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: 1fr;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
padding: 1rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Tablet (640px+) */
|
|
27
|
+
@media (min-width: 640px) {
|
|
28
|
+
.card-grid {
|
|
29
|
+
grid-template-columns: repeat(2, 1fr);
|
|
30
|
+
gap: 1.5rem;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Desktop (1024px+) */
|
|
35
|
+
@media (min-width: 1024px) {
|
|
36
|
+
.card-grid {
|
|
37
|
+
grid-template-columns: repeat(3, 1fr);
|
|
38
|
+
gap: 2rem;
|
|
39
|
+
max-width: 1200px;
|
|
40
|
+
margin: 0 auto;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Never use `max-width` queries as your primary strategy. You end up overriding desktop styles for mobile, which is more code and more bugs.
|
|
46
|
+
|
|
47
|
+
### Container Queries
|
|
48
|
+
|
|
49
|
+
Media queries respond to the viewport. Container queries respond to the parent container -- critical for reusable components:
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
/* Define the container */
|
|
53
|
+
.sidebar {
|
|
54
|
+
container-type: inline-size;
|
|
55
|
+
container-name: sidebar;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Component adapts to its container, not the viewport */
|
|
59
|
+
@container sidebar (min-width: 300px) {
|
|
60
|
+
.user-card {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: row;
|
|
63
|
+
gap: 1rem;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@container sidebar (max-width: 299px) {
|
|
68
|
+
.user-card {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
text-align: center;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use container queries when a component lives in multiple layout contexts (sidebar, main content, modal).
|
|
77
|
+
|
|
78
|
+
### Fluid Typography
|
|
79
|
+
|
|
80
|
+
Scale text smoothly between breakpoints with `clamp()`:
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
:root {
|
|
84
|
+
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
|
85
|
+
--text-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
|
|
86
|
+
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
|
|
87
|
+
--text-lg: clamp(1.125rem, 1rem + 0.65vw, 1.375rem);
|
|
88
|
+
--text-xl: clamp(1.375rem, 1.1rem + 1.2vw, 2rem);
|
|
89
|
+
--text-2xl: clamp(1.75rem, 1.3rem + 2vw, 3rem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
h1 { font-size: var(--text-2xl); }
|
|
93
|
+
h2 { font-size: var(--text-xl); }
|
|
94
|
+
p { font-size: var(--text-base); }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The formula `clamp(min, preferred, max)` prevents text from being too small on phones or too large on 4K monitors.
|
|
98
|
+
|
|
99
|
+
### Breakpoint Strategy
|
|
100
|
+
|
|
101
|
+
Use content-driven breakpoints, not device-specific ones:
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
/* Tailwind defaults -- a solid starting point */
|
|
105
|
+
/* sm: 640px -- small tablets, large phones landscape */
|
|
106
|
+
/* md: 768px -- tablets portrait */
|
|
107
|
+
/* lg: 1024px -- tablets landscape, small laptops */
|
|
108
|
+
/* xl: 1280px -- standard laptops */
|
|
109
|
+
/* 2xl: 1536px -- large monitors */
|
|
110
|
+
|
|
111
|
+
/* Custom breakpoint only when content demands it */
|
|
112
|
+
@media (min-width: 480px) {
|
|
113
|
+
.pricing-card { flex-direction: row; }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Rules: use 3-5 breakpoints maximum. Name them by size (sm/md/lg), not by device.
|
|
118
|
+
|
|
119
|
+
### Touch Targets
|
|
120
|
+
|
|
121
|
+
Mobile users tap with fingers. Minimum touch target: 44x44px (WCAG), recommended 48x48px:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
.nav-link {
|
|
125
|
+
min-height: 44px;
|
|
126
|
+
padding: 12px 16px;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.button-group {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: 8px; /* minimum 8px between adjacent targets */
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.icon-button {
|
|
137
|
+
width: 48px;
|
|
138
|
+
height: 48px;
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Responsive Layout Patterns
|
|
147
|
+
|
|
148
|
+
```css
|
|
149
|
+
/* Sidebar layout -- collapses on mobile */
|
|
150
|
+
.app-layout {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: 1fr;
|
|
153
|
+
}
|
|
154
|
+
@media (min-width: 1024px) {
|
|
155
|
+
.app-layout { grid-template-columns: 280px 1fr; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Responsive table -- stacks on mobile */
|
|
159
|
+
@media (max-width: 639px) {
|
|
160
|
+
table, thead, tbody, th, td, tr { display: block; }
|
|
161
|
+
thead { position: absolute; width: 1px; height: 1px; overflow: hidden; }
|
|
162
|
+
td::before {
|
|
163
|
+
content: attr(data-label);
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
display: block;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Responsive images with art direction:
|
|
171
|
+
|
|
172
|
+
```html
|
|
173
|
+
<picture>
|
|
174
|
+
<source media="(min-width: 1024px)" srcset="/hero-wide.webp" />
|
|
175
|
+
<source media="(min-width: 640px)" srcset="/hero-medium.webp" />
|
|
176
|
+
<img src="/hero-mobile.webp" alt="Hero" loading="lazy" />
|
|
177
|
+
</picture>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Examples
|
|
181
|
+
|
|
182
|
+
| Problem | Solution | Benefit |
|
|
183
|
+
|---------|----------|---------|
|
|
184
|
+
| Cards overflow on 320px | Mobile-first grid, 1fr base | Content always fits |
|
|
185
|
+
| Component in sidebar and modal | Container queries | Adapts to container |
|
|
186
|
+
| Heading huge on mobile | `clamp()` fluid typography | Smooth scaling |
|
|
187
|
+
| Users mis-tap small buttons | 48px minimum touch targets | Fewer mis-taps |
|
|
188
|
+
| Data table unreadable on mobile | Stack cells with `data-label` | Readable at any width |
|
|
189
|
+
| Desktop images on mobile | `<picture>` with sources | 3x faster mobile load |
|
|
190
|
+
|
|
191
|
+
## Checklist
|
|
192
|
+
- [ ] Base styles target mobile (320px), enhanced with `min-width` queries
|
|
193
|
+
- [ ] Layout uses CSS Grid or Flexbox, not floats
|
|
194
|
+
- [ ] 3-5 breakpoints maximum, driven by content
|
|
195
|
+
- [ ] Typography uses `clamp()` for fluid scaling
|
|
196
|
+
- [ ] Reusable components use container queries
|
|
197
|
+
- [ ] Touch targets at least 44x44px with 8px minimum spacing
|
|
198
|
+
- [ ] Images use responsive sources (`<picture>`, `srcset`)
|
|
199
|
+
- [ ] Tested at 320px, 768px, 1024px, and 1440px widths
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ruby-rails-patterns
|
|
3
|
+
description: Apply idiomatic Ruby on Rails patterns for models, controllers, background jobs, and Hotwire/Turbo real-time UIs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ruby on Rails Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building or reviewing Rails applications. They cover
|
|
11
|
+
ActiveRecord modeling, concern extraction, service objects for complex business
|
|
12
|
+
logic, background jobs, and Hotwire/Turbo Streams for server-rendered real-time
|
|
13
|
+
UIs. Use this skill when standing up new features, refactoring fat controllers
|
|
14
|
+
or models, or adding real-time behavior without a JavaScript framework.
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
### 1. ActiveRecord — Keep Models Thin
|
|
19
|
+
|
|
20
|
+
Push queries into scopes. Push multi-step mutations into service objects.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class Order < ApplicationRecord
|
|
24
|
+
belongs_to :user
|
|
25
|
+
has_many :line_items, dependent: :destroy
|
|
26
|
+
|
|
27
|
+
scope :recent, -> { where("created_at > ?", 30.days.ago) }
|
|
28
|
+
scope :fulfilled, -> { where(status: :fulfilled) }
|
|
29
|
+
scope :expensive, ->(min) { where("total_cents >= ?", min) }
|
|
30
|
+
|
|
31
|
+
validates :total_cents, numericality: { greater_than: 0 }
|
|
32
|
+
|
|
33
|
+
# Callbacks are fine for simple lifecycle hooks — not business logic
|
|
34
|
+
after_create_commit :broadcast_to_dashboard
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Concerns — Shared Behavior Modules
|
|
39
|
+
|
|
40
|
+
Extract cross-cutting behavior into concerns. Keep each concern under 50 lines.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# app/models/concerns/sluggable.rb
|
|
44
|
+
module Sluggable
|
|
45
|
+
extend ActiveSupport::Concern
|
|
46
|
+
|
|
47
|
+
included do
|
|
48
|
+
before_validation :generate_slug, on: :create
|
|
49
|
+
validates :slug, presence: true, uniqueness: true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def generate_slug
|
|
55
|
+
self.slug = name&.parameterize
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Service Objects — Encapsulate Business Logic
|
|
61
|
+
|
|
62
|
+
One public method (`call`), explicit inputs, return a result object.
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class Orders::Checkout
|
|
66
|
+
def initialize(user:, cart:, payment_method:)
|
|
67
|
+
@user = user
|
|
68
|
+
@cart = cart
|
|
69
|
+
@payment_method = payment_method
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call
|
|
73
|
+
ActiveRecord::Base.transaction do
|
|
74
|
+
order = Order.create!(user: @user, total_cents: @cart.total_cents)
|
|
75
|
+
@cart.items.each { |item| order.line_items.create!(item.attributes) }
|
|
76
|
+
charge = PaymentGateway.charge(@payment_method, order.total_cents)
|
|
77
|
+
order.update!(payment_id: charge.id, status: :paid)
|
|
78
|
+
Result.new(success: true, order: order)
|
|
79
|
+
end
|
|
80
|
+
rescue PaymentGateway::Error => e
|
|
81
|
+
Result.new(success: false, error: e.message)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Result = Struct.new(:success, :order, :error, keyword_init: true)
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Background Jobs — Sidekiq/ActiveJob
|
|
89
|
+
|
|
90
|
+
Isolate slow work: emails, API calls, reports. Keep jobs idempotent.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
class SyncInventoryJob < ApplicationJob
|
|
94
|
+
queue_as :default
|
|
95
|
+
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
|
|
96
|
+
|
|
97
|
+
def perform(product_id)
|
|
98
|
+
product = Product.find(product_id)
|
|
99
|
+
stock = WarehouseApi.fetch_stock(product.sku)
|
|
100
|
+
product.update!(quantity: stock.available)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 5. Hotwire — Turbo Frames and Streams
|
|
106
|
+
|
|
107
|
+
Replace SPA complexity with server-rendered partials streamed over WebSocket.
|
|
108
|
+
|
|
109
|
+
```erb
|
|
110
|
+
<!-- app/views/orders/index.html.erb -->
|
|
111
|
+
<%= turbo_stream_from "orders" %>
|
|
112
|
+
|
|
113
|
+
<div id="orders">
|
|
114
|
+
<%= turbo_frame_tag "orders_list" do %>
|
|
115
|
+
<%= render @orders %>
|
|
116
|
+
<% end %>
|
|
117
|
+
</div>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# Broadcast from model or job
|
|
122
|
+
class Order < ApplicationRecord
|
|
123
|
+
after_create_commit -> {
|
|
124
|
+
broadcast_prepend_to "orders",
|
|
125
|
+
partial: "orders/order",
|
|
126
|
+
locals: { order: self }
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 6. Turbo Stream Actions
|
|
132
|
+
|
|
133
|
+
Use `turbo_stream` responses for form submissions without full page reloads.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# app/controllers/comments_controller.rb
|
|
137
|
+
def create
|
|
138
|
+
@comment = @post.comments.build(comment_params)
|
|
139
|
+
if @comment.save
|
|
140
|
+
respond_to do |format|
|
|
141
|
+
format.turbo_stream
|
|
142
|
+
format.html { redirect_to @post }
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
render :new, status: :unprocessable_entity
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```erb
|
|
151
|
+
<!-- app/views/comments/create.turbo_stream.erb -->
|
|
152
|
+
<%= turbo_stream.append "comments", @comment %>
|
|
153
|
+
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Examples
|
|
157
|
+
|
|
158
|
+
| Pattern | When | Benefit |
|
|
159
|
+
|---------|------|---------|
|
|
160
|
+
| Scopes | Repeated query logic | Composable, testable queries |
|
|
161
|
+
| Concerns | Shared model behavior | DRY without deep inheritance |
|
|
162
|
+
| Service objects | Multi-step transactions | Testable, single-responsibility |
|
|
163
|
+
| Background jobs | Slow external calls | Non-blocking, retry-safe |
|
|
164
|
+
| Turbo Frames | Inline editing, modals | No JS framework needed |
|
|
165
|
+
| Turbo Streams | Live updates | Real-time without polling |
|
|
166
|
+
|
|
167
|
+
## Checklist
|
|
168
|
+
|
|
169
|
+
- [ ] Models have no business logic beyond validations, scopes, and associations
|
|
170
|
+
- [ ] Multi-model mutations live in service objects with explicit inputs
|
|
171
|
+
- [ ] Concerns are under 50 lines and represent a single behavior
|
|
172
|
+
- [ ] Background jobs are idempotent — safe to retry
|
|
173
|
+
- [ ] Jobs use `retry_on` with backoff for transient failures
|
|
174
|
+
- [ ] Turbo Frames wrap independently-loadable page sections
|
|
175
|
+
- [ ] Turbo Streams broadcast from `after_commit` (not `after_save`) to avoid race conditions
|
|
176
|
+
- [ ] N+1 queries are caught with `strict_loading` or Bullet gem
|
|
177
|
+
- [ ] Database indexes exist for all foreign keys and columns used in scopes
|
|
178
|
+
- [ ] Controllers stay under 10 lines per action — delegate to services
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rust-patterns
|
|
3
|
+
description: Rust patterns for ownership, lifetimes, traits, error handling with thiserror/anyhow, async, and serde.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rust Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when writing Rust code. Use this skill for managing ownership
|
|
11
|
+
and lifetimes correctly, designing trait-based abstractions, handling errors with
|
|
12
|
+
thiserror (libraries) and anyhow (applications), writing async code with tokio, and
|
|
13
|
+
serializing data with serde.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Ownership and Borrowing
|
|
18
|
+
|
|
19
|
+
Pass by reference (`&T`) when you only need to read. Pass by `&mut T` when you
|
|
20
|
+
need to modify. Take ownership (`T`) when the function needs to store or consume
|
|
21
|
+
the value. Use `Clone` explicitly rather than fighting the borrow checker.
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
// Borrow for read-only access
|
|
25
|
+
fn word_count(text: &str) -> usize {
|
|
26
|
+
text.split_whitespace().count()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Take ownership when storing
|
|
30
|
+
struct Cache {
|
|
31
|
+
entries: HashMap<String, Vec<u8>>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl Cache {
|
|
35
|
+
fn insert(&mut self, key: String, value: Vec<u8>) {
|
|
36
|
+
self.entries.insert(key, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Lifetimes
|
|
42
|
+
|
|
43
|
+
Only annotate lifetimes when the compiler cannot infer them. The most common case
|
|
44
|
+
is a struct that borrows data.
|
|
45
|
+
|
|
46
|
+
```rust
|
|
47
|
+
struct Parser<'a> {
|
|
48
|
+
input: &'a str,
|
|
49
|
+
pos: usize,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl<'a> Parser<'a> {
|
|
53
|
+
fn new(input: &'a str) -> Self {
|
|
54
|
+
Self { input, pos: 0 }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn remaining(&self) -> &'a str {
|
|
58
|
+
&self.input[self.pos..]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Traits and Trait Objects
|
|
64
|
+
|
|
65
|
+
Use traits for polymorphism. Prefer generics (`impl Trait`) for static dispatch.
|
|
66
|
+
Use `dyn Trait` for dynamic dispatch when you need heterogeneous collections.
|
|
67
|
+
|
|
68
|
+
```rust
|
|
69
|
+
trait Renderer {
|
|
70
|
+
fn render(&self, data: &Document) -> String;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Static dispatch (monomorphized, zero-cost)
|
|
74
|
+
fn render_to_file(renderer: &impl Renderer, doc: &Document, path: &Path) -> io::Result<()> {
|
|
75
|
+
let output = renderer.render(doc);
|
|
76
|
+
fs::write(path, output)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Dynamic dispatch (when you need a collection of different types)
|
|
80
|
+
fn render_all(renderers: &[Box<dyn Renderer>], doc: &Document) -> Vec<String> {
|
|
81
|
+
renderers.iter().map(|r| r.render(doc)).collect()
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Error Handling: thiserror for Libraries
|
|
86
|
+
|
|
87
|
+
Use `thiserror` in library crates to define structured error types. Each variant
|
|
88
|
+
wraps an underlying error or carries context.
|
|
89
|
+
|
|
90
|
+
```rust
|
|
91
|
+
use thiserror::Error;
|
|
92
|
+
|
|
93
|
+
#[derive(Error, Debug)]
|
|
94
|
+
pub enum StorageError {
|
|
95
|
+
#[error("file not found: {path}")]
|
|
96
|
+
NotFound { path: String },
|
|
97
|
+
|
|
98
|
+
#[error("permission denied: {path}")]
|
|
99
|
+
PermissionDenied { path: String },
|
|
100
|
+
|
|
101
|
+
#[error("IO error")]
|
|
102
|
+
Io(#[from] std::io::Error),
|
|
103
|
+
|
|
104
|
+
#[error("deserialization failed")]
|
|
105
|
+
Parse(#[from] serde_json::Error),
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Error Handling: anyhow for Applications
|
|
110
|
+
|
|
111
|
+
Use `anyhow` in binaries and CLI tools. Attach context with `.context()`.
|
|
112
|
+
|
|
113
|
+
```rust
|
|
114
|
+
use anyhow::{Context, Result};
|
|
115
|
+
|
|
116
|
+
fn load_config(path: &Path) -> Result<Config> {
|
|
117
|
+
let content = fs::read_to_string(path)
|
|
118
|
+
.with_context(|| format!("failed to read config from {}", path.display()))?;
|
|
119
|
+
|
|
120
|
+
let config: Config = toml::from_str(&content)
|
|
121
|
+
.context("failed to parse config TOML")?;
|
|
122
|
+
|
|
123
|
+
Ok(config)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Async with Tokio
|
|
128
|
+
|
|
129
|
+
Use `tokio::spawn` for concurrent tasks. Use `tokio::select!` for racing futures.
|
|
130
|
+
Always use timeouts on network operations.
|
|
131
|
+
|
|
132
|
+
```rust
|
|
133
|
+
use tokio::time::{timeout, Duration};
|
|
134
|
+
|
|
135
|
+
async fn fetch_with_timeout(url: &str) -> Result<String> {
|
|
136
|
+
let result = timeout(Duration::from_secs(10), reqwest::get(url))
|
|
137
|
+
.await
|
|
138
|
+
.context("request timed out")?
|
|
139
|
+
.context("request failed")?
|
|
140
|
+
.text()
|
|
141
|
+
.await
|
|
142
|
+
.context("failed to read body")?;
|
|
143
|
+
Ok(result)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
|
|
147
|
+
let handles: Vec<_> = urls.into_iter()
|
|
148
|
+
.map(|url| tokio::spawn(async move { fetch_with_timeout(&url).await }))
|
|
149
|
+
.collect();
|
|
150
|
+
|
|
151
|
+
let mut results = Vec::new();
|
|
152
|
+
for handle in handles {
|
|
153
|
+
results.push(handle.await.unwrap_or_else(|e| Err(e.into())));
|
|
154
|
+
}
|
|
155
|
+
results
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Serde
|
|
160
|
+
|
|
161
|
+
Derive `Serialize`/`Deserialize`. Use `#[serde(rename_all = "camelCase")]` for
|
|
162
|
+
JSON APIs. Use `#[serde(default)]` for optional fields with defaults.
|
|
163
|
+
|
|
164
|
+
```rust
|
|
165
|
+
use serde::{Deserialize, Serialize};
|
|
166
|
+
|
|
167
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
168
|
+
#[serde(rename_all = "camelCase")]
|
|
169
|
+
struct ApiResponse {
|
|
170
|
+
request_id: String,
|
|
171
|
+
#[serde(default)]
|
|
172
|
+
items: Vec<Item>,
|
|
173
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
174
|
+
next_cursor: Option<String>,
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Examples
|
|
179
|
+
|
|
180
|
+
**Pattern: Builder for complex structs**
|
|
181
|
+
```rust
|
|
182
|
+
struct Request {
|
|
183
|
+
url: String,
|
|
184
|
+
timeout: Duration,
|
|
185
|
+
headers: HashMap<String, String>,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
struct RequestBuilder { inner: Request }
|
|
189
|
+
|
|
190
|
+
impl RequestBuilder {
|
|
191
|
+
fn new(url: impl Into<String>) -> Self {
|
|
192
|
+
Self { inner: Request { url: url.into(), timeout: Duration::from_secs(30), headers: HashMap::new() } }
|
|
193
|
+
}
|
|
194
|
+
fn timeout(mut self, d: Duration) -> Self { self.inner.timeout = d; self }
|
|
195
|
+
fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self { self.inner.headers.insert(k.into(), v.into()); self }
|
|
196
|
+
fn build(self) -> Request { self.inner }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Checklist
|
|
201
|
+
|
|
202
|
+
- [ ] Borrow (`&T`) by default; take ownership only when storing or consuming
|
|
203
|
+
- [ ] Lifetime annotations only where the compiler requires them
|
|
204
|
+
- [ ] `thiserror` for library error types, `anyhow` for application error handling
|
|
205
|
+
- [ ] Every `?` operator has context attached via `.context()` or descriptive error variant
|
|
206
|
+
- [ ] Traits are small (1-3 methods), defined at the consumer
|
|
207
|
+
- [ ] `impl Trait` for static dispatch, `dyn Trait` only for heterogeneous collections
|
|
208
|
+
- [ ] Async code uses `tokio::timeout` on all network operations
|
|
209
|
+
- [ ] Serde types use `rename_all`, `default`, and `skip_serializing_if`
|
|
210
|
+
- [ ] `clippy` passes with zero warnings (`cargo clippy -- -D warnings`)
|
|
211
|
+
- [ ] Tests use `#[test]` or `#[tokio::test]`, assertions use `assert_eq!`/`assert!`
|