red64-cli 0.3.0 → 0.6.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 +194 -338
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -13
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/types.d.ts +0 -2
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/HelpScreen.d.ts.map +1 -1
- package/dist/components/screens/HelpScreen.js +0 -2
- package/dist/components/screens/HelpScreen.js.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +5 -8
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +29 -8
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/screens/StatusScreen.d.ts.map +1 -1
- package/dist/components/screens/StatusScreen.js +16 -1
- package/dist/components/screens/StatusScreen.js.map +1 -1
- package/dist/services/AgentInvoker.d.ts.map +1 -1
- package/dist/services/AgentInvoker.js +76 -37
- package/dist/services/AgentInvoker.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.d.ts +1 -1
- package/dist/services/ClaudeErrorDetector.d.ts.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +1 -0
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ClaudeHealthCheck.d.ts +7 -0
- package/dist/services/ClaudeHealthCheck.d.ts.map +1 -1
- package/dist/services/ClaudeHealthCheck.js +76 -12
- package/dist/services/ClaudeHealthCheck.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/DockerRunner.js +1 -1
- package/dist/services/DockerRunner.js.map +1 -1
- package/dist/services/PhaseExecutor.d.ts.map +1 -1
- package/dist/services/PhaseExecutor.js +2 -1
- package/dist/services/PhaseExecutor.js.map +1 -1
- package/dist/services/TaskRunner.d.ts.map +1 -1
- package/dist/services/TaskRunner.js +2 -1
- package/dist/services/TaskRunner.js.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/stacks/c/code-quality.md +326 -0
- package/framework/stacks/c/coding-style.md +347 -0
- package/framework/stacks/c/conventions.md +513 -0
- package/framework/stacks/c/error-handling.md +350 -0
- package/framework/stacks/c/feedback.md +158 -0
- package/framework/stacks/c/memory-safety.md +408 -0
- package/framework/stacks/c/tech.md +122 -0
- package/framework/stacks/c/testing.md +472 -0
- package/framework/stacks/cpp/code-quality.md +282 -0
- package/framework/stacks/cpp/coding-style.md +363 -0
- package/framework/stacks/cpp/conventions.md +420 -0
- package/framework/stacks/cpp/error-handling.md +264 -0
- package/framework/stacks/cpp/feedback.md +104 -0
- package/framework/stacks/cpp/memory-safety.md +351 -0
- package/framework/stacks/cpp/tech.md +160 -0
- package/framework/stacks/cpp/testing.md +323 -0
- package/framework/stacks/java/code-quality.md +357 -0
- package/framework/stacks/java/coding-style.md +400 -0
- package/framework/stacks/java/conventions.md +437 -0
- package/framework/stacks/java/error-handling.md +408 -0
- package/framework/stacks/java/feedback.md +180 -0
- package/framework/stacks/java/tech.md +126 -0
- package/framework/stacks/java/testing.md +485 -0
- package/framework/stacks/javascript/async-patterns.md +216 -0
- package/framework/stacks/javascript/code-quality.md +182 -0
- package/framework/stacks/javascript/coding-style.md +293 -0
- package/framework/stacks/javascript/conventions.md +268 -0
- package/framework/stacks/javascript/error-handling.md +216 -0
- package/framework/stacks/javascript/feedback.md +80 -0
- package/framework/stacks/javascript/tech.md +114 -0
- package/framework/stacks/javascript/testing.md +209 -0
- package/framework/stacks/loco/code-quality.md +156 -0
- package/framework/stacks/loco/coding-style.md +247 -0
- package/framework/stacks/loco/error-handling.md +225 -0
- package/framework/stacks/loco/feedback.md +35 -0
- package/framework/stacks/loco/loco.md +342 -0
- package/framework/stacks/loco/structure.md +193 -0
- package/framework/stacks/loco/tech.md +129 -0
- package/framework/stacks/loco/testing.md +211 -0
- package/framework/stacks/rust/code-quality.md +370 -0
- package/framework/stacks/rust/coding-style.md +475 -0
- package/framework/stacks/rust/conventions.md +430 -0
- package/framework/stacks/rust/error-handling.md +399 -0
- package/framework/stacks/rust/feedback.md +152 -0
- package/framework/stacks/rust/memory-safety.md +398 -0
- package/framework/stacks/rust/tech.md +121 -0
- package/framework/stacks/rust/testing.md +528 -0
- package/package.json +14 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Error Handling
|
|
2
|
+
|
|
3
|
+
Error handling patterns for Loco applications. Leverage Rust's type system and Loco's error infrastructure for predictable, user-friendly error responses.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Use `Result<T>` everywhere**: No panics in production code
|
|
10
|
+
- **`?` operator for propagation**: Flat, readable error chains
|
|
11
|
+
- **Typed errors for domain logic**: Custom error enums for business rules
|
|
12
|
+
- **Consistent HTTP error responses**: Map errors to appropriate status codes
|
|
13
|
+
- **Log context, not secrets**: Structured logging with tracing spans
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Loco Error Types
|
|
18
|
+
|
|
19
|
+
### Built-in Error Hierarchy
|
|
20
|
+
|
|
21
|
+
Loco provides `loco_rs::Error` as the primary error type. Controllers return `Result<impl IntoResponse>` which auto-maps errors to HTTP responses.
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
use loco_rs::prelude::*;
|
|
25
|
+
|
|
26
|
+
// Loco maps these automatically:
|
|
27
|
+
// - ModelError::EntityNotFound -> 404
|
|
28
|
+
// - Error::Unauthorized -> 401
|
|
29
|
+
// - Error::BadRequest -> 400
|
|
30
|
+
// - Error::InternalServerError -> 500
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Model Errors
|
|
34
|
+
|
|
35
|
+
Use `ModelResult<T>` for model-layer operations:
|
|
36
|
+
|
|
37
|
+
```rust
|
|
38
|
+
impl super::_entities::users::ActiveModel {
|
|
39
|
+
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
|
|
40
|
+
let user = users::Entity::find()
|
|
41
|
+
.filter(users::Column::Email.eq(email))
|
|
42
|
+
.one(db)
|
|
43
|
+
.await?;
|
|
44
|
+
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Controller Error Handling
|
|
52
|
+
|
|
53
|
+
### Pattern: Early Returns with `?`
|
|
54
|
+
|
|
55
|
+
```rust
|
|
56
|
+
// GOOD: Flat, each line can fail independently
|
|
57
|
+
async fn update(
|
|
58
|
+
auth: auth::JWT,
|
|
59
|
+
State(ctx): State<AppContext>,
|
|
60
|
+
Path(id): Path<i32>,
|
|
61
|
+
Json(params): Json<UpdatePostParams>,
|
|
62
|
+
) -> Result<Json<PostResponse>> {
|
|
63
|
+
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
|
64
|
+
let post = posts::Model::find_by_id_and_user(&ctx.db, id, user.id).await?;
|
|
65
|
+
let updated = post.update(&ctx.db, ¶ms).await?;
|
|
66
|
+
Ok(Json(PostResponse::from(updated)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// BAD: Nested error handling
|
|
70
|
+
async fn update(/* ... */) -> Result<Json<PostResponse>> {
|
|
71
|
+
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
|
|
72
|
+
Ok(user) => match posts::Model::find_by_id(&ctx.db, id).await {
|
|
73
|
+
Ok(Some(post)) => {
|
|
74
|
+
if post.user_id == user.id {
|
|
75
|
+
// deeply nested...
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// more nesting...
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Custom Error Responses
|
|
85
|
+
|
|
86
|
+
For domain-specific errors, return explicit HTTP responses:
|
|
87
|
+
|
|
88
|
+
```rust
|
|
89
|
+
async fn publish(
|
|
90
|
+
State(ctx): State<AppContext>,
|
|
91
|
+
Path(id): Path<i32>,
|
|
92
|
+
) -> Result<Json<PostResponse>> {
|
|
93
|
+
let post = posts::Entity::find_by_id(id)
|
|
94
|
+
.one(&ctx.db)
|
|
95
|
+
.await?
|
|
96
|
+
.ok_or_else(|| Error::NotFound)?;
|
|
97
|
+
|
|
98
|
+
if post.status == "published" {
|
|
99
|
+
return Err(Error::BadRequest("Post is already published".into()));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let published = post.publish(&ctx.db).await?;
|
|
103
|
+
Ok(Json(PostResponse::from(published)))
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Validation Errors
|
|
110
|
+
|
|
111
|
+
### Using `validator` Crate
|
|
112
|
+
|
|
113
|
+
Return structured validation errors with field-level detail:
|
|
114
|
+
|
|
115
|
+
```rust
|
|
116
|
+
use loco_rs::controller::views::json_validate::JsonValidateWithMessage;
|
|
117
|
+
|
|
118
|
+
async fn create(
|
|
119
|
+
State(ctx): State<AppContext>,
|
|
120
|
+
JsonValidateWithMessage(params): JsonValidateWithMessage<CreatePostParams>,
|
|
121
|
+
) -> Result<Json<PostResponse>> {
|
|
122
|
+
// Validation happens automatically via extractor
|
|
123
|
+
// Returns 422 with field-level errors on failure
|
|
124
|
+
let post = posts::ActiveModel::create(&ctx.db, ¶ms).await?;
|
|
125
|
+
Ok(Json(PostResponse::from(post)))
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Model-Level Validation
|
|
130
|
+
|
|
131
|
+
```rust
|
|
132
|
+
impl Validatable for super::_entities::posts::ActiveModel {
|
|
133
|
+
fn validator(&self) -> Box<dyn Validate> {
|
|
134
|
+
Box::new(PostValidator {
|
|
135
|
+
title: self.title.clone().into_value().unwrap_or_default(),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// In model method:
|
|
141
|
+
pub async fn create(db: &DatabaseConnection, params: &CreatePostParams) -> ModelResult<Model> {
|
|
142
|
+
let mut item = ActiveModel { ..Default::default() };
|
|
143
|
+
item.title = Set(params.title.clone());
|
|
144
|
+
item.validate()?; // Returns validation errors before hitting DB
|
|
145
|
+
item.insert(db).await.map_err(|e| ModelError::from(e))
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Worker Error Handling
|
|
152
|
+
|
|
153
|
+
Workers should handle errors gracefully and log context:
|
|
154
|
+
|
|
155
|
+
```rust
|
|
156
|
+
async fn perform(&self, args: ReportArgs) -> Result<()> {
|
|
157
|
+
let user = users::Entity::find_by_id(args.user_id)
|
|
158
|
+
.one(&self.ctx.db)
|
|
159
|
+
.await?
|
|
160
|
+
.ok_or_else(|| {
|
|
161
|
+
tracing::error!(user_id = args.user_id, "User not found for report");
|
|
162
|
+
Error::NotFound
|
|
163
|
+
})?;
|
|
164
|
+
|
|
165
|
+
match generate_report(&user).await {
|
|
166
|
+
Ok(report) => {
|
|
167
|
+
tracing::info!(user_id = args.user_id, "Report generated");
|
|
168
|
+
Ok(())
|
|
169
|
+
}
|
|
170
|
+
Err(e) => {
|
|
171
|
+
tracing::error!(user_id = args.user_id, error = %e, "Report generation failed");
|
|
172
|
+
Err(e.into())
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Logging with Context
|
|
181
|
+
|
|
182
|
+
### Structured Tracing
|
|
183
|
+
|
|
184
|
+
```rust
|
|
185
|
+
use tracing;
|
|
186
|
+
|
|
187
|
+
// GOOD: Structured fields for queryability
|
|
188
|
+
tracing::info!(user_id = %user.id, action = "login", "User authenticated");
|
|
189
|
+
tracing::error!(post_id = id, error = %e, "Failed to publish post");
|
|
190
|
+
|
|
191
|
+
// BAD: Unstructured string interpolation
|
|
192
|
+
tracing::info!("User {} logged in", user.id);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Span Context in Controllers
|
|
196
|
+
|
|
197
|
+
```rust
|
|
198
|
+
async fn create(
|
|
199
|
+
State(ctx): State<AppContext>,
|
|
200
|
+
Json(params): Json<CreatePostParams>,
|
|
201
|
+
) -> Result<Json<PostResponse>> {
|
|
202
|
+
let _span = tracing::info_span!("create_post", title = %params.title).entered();
|
|
203
|
+
// All logs within this scope include the span context
|
|
204
|
+
let post = posts::ActiveModel::create(&ctx.db, ¶ms).await?;
|
|
205
|
+
Ok(Json(PostResponse::from(post)))
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `.unwrap()` in handlers | Panics crash the request | Use `?` or `.ok_or_else()` |
|
|
216
|
+
| Swallowing errors silently | Hides bugs, makes debugging impossible | Log and propagate with `?` |
|
|
217
|
+
| Generic "Internal Error" for everything | Poor DX for API consumers | Map to specific HTTP status codes |
|
|
218
|
+
| Logging sensitive data | Security risk | Use structured fields, redact secrets |
|
|
219
|
+
| Validation in controllers | Logic duplication across endpoints | Use `Validatable` trait on models |
|
|
220
|
+
| `Box<dyn Error>` as return type | Loses type information | Use Loco's `Error` / `ModelError` types |
|
|
221
|
+
| Ignoring worker failures | Silent data loss | Log errors with context, use retry policies |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
_Rust's type system makes error handling explicit. Use `?` for propagation, typed errors for domain logic, and structured tracing for observability._
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Feedback Guidelines
|
|
2
|
+
|
|
3
|
+
When reviewing or generating code for a Loco application, apply these checks.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Always Check
|
|
8
|
+
|
|
9
|
+
1. **Fat models, slim controllers**: Is business logic in models, not controllers?
|
|
10
|
+
2. **Generator compliance**: Were new files created via `cargo loco generate`?
|
|
11
|
+
3. **Entity files untouched**: Are `_entities/` files unmodified?
|
|
12
|
+
4. **Config per environment**: Are dangerous flags disabled in production config?
|
|
13
|
+
5. **No `.unwrap()` in handlers**: All error paths handled with `?`?
|
|
14
|
+
6. **Worker isolation**: Workers self-contained with serializable args?
|
|
15
|
+
7. **Validation on models**: `Validatable` implemented for user-input models?
|
|
16
|
+
8. **View structs for responses**: Not returning raw SeaORM entities?
|
|
17
|
+
|
|
18
|
+
## Red Flags
|
|
19
|
+
|
|
20
|
+
- Direct SQL in controllers
|
|
21
|
+
- `dangerously_*` flags in `config/production.yaml`
|
|
22
|
+
- Editing files in `src/models/_entities/`
|
|
23
|
+
- Workers referencing controller state
|
|
24
|
+
- Missing `down()` in migrations
|
|
25
|
+
- Hardcoded configuration values
|
|
26
|
+
- `.unwrap()` or `.expect()` in request handlers
|
|
27
|
+
|
|
28
|
+
## Encourage
|
|
29
|
+
|
|
30
|
+
- Using generators for all scaffolding
|
|
31
|
+
- Domain methods on models with descriptive names
|
|
32
|
+
- Structured tracing with typed fields
|
|
33
|
+
- Snapshot testing with `insta`
|
|
34
|
+
- Request tests for all controller endpoints
|
|
35
|
+
- Tagged workers for job categorization
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Loco Framework Conventions
|
|
2
|
+
|
|
3
|
+
Conventions and patterns for Loco -- the Rust on Rails framework. Convention over configuration; decisions are made for you.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Framework Stack
|
|
8
|
+
|
|
9
|
+
### Core Technologies
|
|
10
|
+
- **Loco**: Rails-inspired Rust web framework
|
|
11
|
+
- **Axum**: HTTP routing and Tower middleware
|
|
12
|
+
- **SeaORM**: Async ActiveRecord-style ORM
|
|
13
|
+
- **Tokio**: Async runtime
|
|
14
|
+
- **sidekiq-rs / Tokio**: Background workers (Redis-backed or in-process)
|
|
15
|
+
- **JWT**: Stateless authentication (built-in)
|
|
16
|
+
|
|
17
|
+
### Database
|
|
18
|
+
- **PostgreSQL** for production (recommended)
|
|
19
|
+
- **SQLite** for development and lightweight deployments
|
|
20
|
+
- **MySQL** supported
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Application Architecture
|
|
25
|
+
|
|
26
|
+
### MVC Patterns
|
|
27
|
+
|
|
28
|
+
**Models** (`src/models/`)
|
|
29
|
+
- Fat models, slim controllers: domain logic lives on models
|
|
30
|
+
- Two-file pattern: `_entities/model.rs` (auto-generated, read-only) + `model.rs` (your ActiveRecord logic)
|
|
31
|
+
- Implement domain operations as methods on `ActiveModel`
|
|
32
|
+
- `User::create` creates a user; `user.buy(product)` buys a product
|
|
33
|
+
|
|
34
|
+
```rust
|
|
35
|
+
// Pattern: Domain logic on the model
|
|
36
|
+
impl super::_entities::users::ActiveModel {
|
|
37
|
+
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
|
|
38
|
+
let user = users::Entity::find()
|
|
39
|
+
.filter(users::Column::Email.eq(email))
|
|
40
|
+
.one(db)
|
|
41
|
+
.await?;
|
|
42
|
+
user.ok_or_else(|| ModelError::EntityNotFound)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub async fn verify_password(&self, password: &str) -> ModelResult<bool> {
|
|
46
|
+
Ok(hash::verify_password(password, &self.password_hash))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Controllers** (`src/controllers/`)
|
|
52
|
+
- Thin controllers: validate input, call model, return view
|
|
53
|
+
- Use Axum extractors for request data
|
|
54
|
+
- Return `Result<impl IntoResponse>` consistently
|
|
55
|
+
- Group routes by resource with `.prefix()`
|
|
56
|
+
|
|
57
|
+
```rust
|
|
58
|
+
// Pattern: Thin controller delegating to model
|
|
59
|
+
async fn create(
|
|
60
|
+
State(ctx): State<AppContext>,
|
|
61
|
+
Json(params): Json<CreatePostParams>,
|
|
62
|
+
) -> Result<Json<PostResponse>> {
|
|
63
|
+
let post = models::posts::ActiveModel::create(&ctx.db, ¶ms).await?;
|
|
64
|
+
Ok(Json(PostResponse::from(post)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn routes() -> Routes {
|
|
68
|
+
Routes::new()
|
|
69
|
+
.prefix("posts")
|
|
70
|
+
.add("/", post(create))
|
|
71
|
+
.add("/", get(list))
|
|
72
|
+
.add("/:id", get(show))
|
|
73
|
+
.add("/:id", put(update))
|
|
74
|
+
.add("/:id", delete(destroy))
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Views** (`src/views/`)
|
|
79
|
+
- JSON serialization structs for API responses
|
|
80
|
+
- Keep response shaping separate from models
|
|
81
|
+
- Use `serde::Serialize` derive
|
|
82
|
+
|
|
83
|
+
```rust
|
|
84
|
+
// Pattern: View struct for API response
|
|
85
|
+
#[derive(Serialize)]
|
|
86
|
+
pub struct PostResponse {
|
|
87
|
+
pub id: i32,
|
|
88
|
+
pub title: String,
|
|
89
|
+
pub content: String,
|
|
90
|
+
pub created_at: DateTime,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl From<posts::Model> for PostResponse {
|
|
94
|
+
fn from(post: posts::Model) -> Self {
|
|
95
|
+
Self {
|
|
96
|
+
id: post.id,
|
|
97
|
+
title: post.title,
|
|
98
|
+
content: post.content,
|
|
99
|
+
created_at: post.created_at,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Business Logic Patterns
|
|
108
|
+
|
|
109
|
+
### Fat Models
|
|
110
|
+
Place domain operations on models. This enables reuse across controllers, workers, and tasks:
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
// src/models/users.rs
|
|
114
|
+
impl super::_entities::users::ActiveModel {
|
|
115
|
+
pub async fn register(db: &DatabaseConnection, params: &RegisterParams) -> ModelResult<Self> {
|
|
116
|
+
// Validation, password hashing, email verification token
|
|
117
|
+
// All in one place, testable via model tests
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub async fn reset_password(db: &DatabaseConnection, token: &str, new_password: &str) -> ModelResult<()> {
|
|
121
|
+
// Token verification, password update
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### When to Extract Services
|
|
127
|
+
Extract standalone service modules only when logic:
|
|
128
|
+
- Spans multiple unrelated models
|
|
129
|
+
- Integrates external APIs
|
|
130
|
+
- Has no natural "home" model
|
|
131
|
+
|
|
132
|
+
Place in `src/services/` (create as needed).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Background Workers
|
|
137
|
+
|
|
138
|
+
### Worker Definition
|
|
139
|
+
Implement `BackgroundWorker` trait with `perform` function:
|
|
140
|
+
|
|
141
|
+
```rust
|
|
142
|
+
#[async_trait]
|
|
143
|
+
impl BackgroundWorker<ReportArgs> for ReportWorker {
|
|
144
|
+
fn build(ctx: &AppContext) -> Self {
|
|
145
|
+
Self { ctx: ctx.clone() }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async fn perform(&self, args: ReportArgs) -> Result<()> {
|
|
149
|
+
// Job logic -- self-contained, no shared controller state
|
|
150
|
+
Ok(())
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Enqueue Jobs
|
|
156
|
+
```rust
|
|
157
|
+
ReportWorker::perform_later(&ctx, ReportArgs { user_id: 42 }).await?;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Worker Modes
|
|
161
|
+
- **BackgroundAsync**: In-process Tokio tasks (development, single-server)
|
|
162
|
+
- **BackgroundQueue**: Redis/Postgres/SQLite backed (production, horizontal scaling)
|
|
163
|
+
- **ForegroundBlocking**: Synchronous (testing only)
|
|
164
|
+
|
|
165
|
+
### Anti-Pattern: Shared State
|
|
166
|
+
Workers must be self-contained. Do not share state between controllers and workers -- workers may run in separate processes.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Tasks
|
|
171
|
+
|
|
172
|
+
CLI-invokable operations for administrative work:
|
|
173
|
+
|
|
174
|
+
```rust
|
|
175
|
+
#[async_trait]
|
|
176
|
+
impl Task for SeedData {
|
|
177
|
+
fn task(&self) -> TaskInfo {
|
|
178
|
+
TaskInfo {
|
|
179
|
+
name: "seed_data".to_string(),
|
|
180
|
+
detail: "Seed the database with initial data".to_string(),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
|
|
185
|
+
// Business logic -- no manual DB access needed
|
|
186
|
+
Ok(())
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Run with: `cargo loco task seed_data`
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Authentication
|
|
196
|
+
|
|
197
|
+
### Built-in JWT Auth (SaaS Starter)
|
|
198
|
+
Loco's SaaS starter includes complete auth flow:
|
|
199
|
+
- `POST /api/auth/register` -- registration with email verification
|
|
200
|
+
- `POST /api/auth/login` -- returns JWT token
|
|
201
|
+
- `POST /api/auth/forgot` / `POST /api/auth/reset` -- password reset
|
|
202
|
+
- `GET /api/auth/verify` -- email verification
|
|
203
|
+
|
|
204
|
+
### Protecting Routes
|
|
205
|
+
Use `auth::JWT` extractor for authenticated endpoints:
|
|
206
|
+
|
|
207
|
+
```rust
|
|
208
|
+
async fn current(
|
|
209
|
+
auth: auth::JWT,
|
|
210
|
+
State(ctx): State<AppContext>,
|
|
211
|
+
) -> Result<Json<UserResponse>> {
|
|
212
|
+
let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
|
|
213
|
+
Ok(Json(UserResponse::from(user)))
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Token Configuration
|
|
218
|
+
```yaml
|
|
219
|
+
auth:
|
|
220
|
+
jwt:
|
|
221
|
+
secret: <change-in-production>
|
|
222
|
+
expiration: 604800 # seconds
|
|
223
|
+
location:
|
|
224
|
+
from: Bearer # or Cookie, Query
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Support multiple auth methods via fallback chains (Bearer -> Cookie -> Query).
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Database Conventions
|
|
232
|
+
|
|
233
|
+
### Migrations
|
|
234
|
+
Migration names auto-detect operations:
|
|
235
|
+
- `CreatePosts` -- new table
|
|
236
|
+
- `AddNameToUsers` -- column addition
|
|
237
|
+
- `RemoveAgeFromUsers` -- column removal
|
|
238
|
+
- `AddUserRefToPosts` -- foreign key
|
|
239
|
+
- `CreateJoinTableUsersAndGroups` -- link table
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
cargo loco generate model posts title:string! content:text user:references
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Field suffixes: `!` = NOT NULL, `^` = UNIQUE, none = nullable.
|
|
246
|
+
|
|
247
|
+
### Entity Generation
|
|
248
|
+
After migrations, regenerate entities:
|
|
249
|
+
```bash
|
|
250
|
+
cargo loco db entities
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Never edit files in `src/models/_entities/` -- they are overwritten.
|
|
254
|
+
|
|
255
|
+
### Seeding
|
|
256
|
+
Define fixtures in `src/fixtures/` as YAML. Load via `Hooks::seed()`:
|
|
257
|
+
```bash
|
|
258
|
+
cargo loco db seed --reset
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Configuration Environments
|
|
264
|
+
|
|
265
|
+
### Development (`config/development.yaml`)
|
|
266
|
+
```yaml
|
|
267
|
+
database:
|
|
268
|
+
auto_migrate: true
|
|
269
|
+
dangerously_truncate: false
|
|
270
|
+
dangerously_recreate: false
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Test (`config/test.yaml`)
|
|
274
|
+
```yaml
|
|
275
|
+
database:
|
|
276
|
+
dangerously_truncate: true # Clean slate per test run
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Production (`config/production.yaml`)
|
|
280
|
+
- Disable all `dangerously_*` flags
|
|
281
|
+
- Set secure JWT secret
|
|
282
|
+
- Configure Redis URI for queue workers
|
|
283
|
+
- Set appropriate log level and format (JSON recommended)
|
|
284
|
+
- Run `cargo loco doctor --production` to validate
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Deployment
|
|
289
|
+
|
|
290
|
+
### Single Binary
|
|
291
|
+
Build and deploy -- no Rust toolchain needed on server:
|
|
292
|
+
```bash
|
|
293
|
+
cargo build --release
|
|
294
|
+
# Copy binary + config/ to server
|
|
295
|
+
./myapp start
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Docker
|
|
299
|
+
```bash
|
|
300
|
+
cargo loco generate deployment # Generates Dockerfile
|
|
301
|
+
docker build -t myapp .
|
|
302
|
+
docker run -p 5150:5150 myapp
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Health Check
|
|
306
|
+
```bash
|
|
307
|
+
cargo loco doctor --production
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Generators
|
|
313
|
+
|
|
314
|
+
Use generators to maintain consistency:
|
|
315
|
+
|
|
316
|
+
| Generator | Command | Creates |
|
|
317
|
+
|-----------|---------|---------|
|
|
318
|
+
| Model | `cargo loco generate model <name> <fields>` | Migration + entity + model file |
|
|
319
|
+
| Controller | `cargo loco generate controller <name>` | Controller + test file |
|
|
320
|
+
| Scaffold | `cargo loco generate scaffold <name> <fields>` | Full CRUD (model + controller + view + tests) |
|
|
321
|
+
| Worker | `cargo loco generate worker <name>` | Worker file |
|
|
322
|
+
| Task | `cargo loco generate task <name>` | Task file |
|
|
323
|
+
| Deployment | `cargo loco generate deployment` | Docker/Nginx/Shuttle configs |
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Anti-Patterns to Avoid
|
|
328
|
+
|
|
329
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| Logic in controllers | Untestable, not reusable | Move domain logic to model methods |
|
|
332
|
+
| Editing `_entities/` files | Overwritten on regeneration | Add logic in `models/<name>.rs` impl blocks |
|
|
333
|
+
| Shared controller/worker state | Breaks horizontal scaling | Workers are self-contained with serializable args |
|
|
334
|
+
| `dangerously_*` flags in production | Data loss risk | Only enable in dev/test environments |
|
|
335
|
+
| Skipping generators | Inconsistent structure | Use `cargo loco generate` for all scaffolding |
|
|
336
|
+
| Raw SQL in controllers | Bypass ORM protections | Use SeaORM queries in model methods |
|
|
337
|
+
| Hardcoded config values | Environment-specific failures | Use `config/*.yaml` per environment |
|
|
338
|
+
| Nullable everything | Weak data integrity | Use `!` suffix for required fields in generators |
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
_Loco is "Rust on Rails" -- lean into conventions, use generators, keep models fat and controllers thin._
|