red64-cli 0.5.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 +64 -58
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +2 -2
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/AgentInvoker.js +4 -4
- package/dist/services/AgentInvoker.js.map +1 -1
- package/dist/services/ClaudeHealthCheck.d.ts +5 -0
- package/dist/services/ClaudeHealthCheck.d.ts.map +1 -1
- package/dist/services/ClaudeHealthCheck.js +43 -5
- package/dist/services/ClaudeHealthCheck.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/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,209 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive Vitest patterns for modern Node.js projects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Fast feedback**: Unit tests run in milliseconds, no I/O
|
|
10
|
+
- **Realistic integration**: Test with real HTTP calls and database when it matters
|
|
11
|
+
- **Readable tests**: Each test tells a story with arrange-act-assert
|
|
12
|
+
- **Native ESM**: Vitest runs ESM natively, no transpilation step
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Test Organization
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
tests/
|
|
20
|
+
setup.js # Global test setup
|
|
21
|
+
unit/
|
|
22
|
+
services/
|
|
23
|
+
user-service.test.js
|
|
24
|
+
utils/
|
|
25
|
+
validation.test.js
|
|
26
|
+
integration/
|
|
27
|
+
api/
|
|
28
|
+
users.test.js
|
|
29
|
+
repositories/
|
|
30
|
+
user-repo.test.js
|
|
31
|
+
fixtures/
|
|
32
|
+
users.js
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Pattern**: Mirror `src/` structure. Suffix all test files with `.test.js`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Vitest Configuration
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// vitest.config.js
|
|
43
|
+
import { defineConfig } from 'vitest/config';
|
|
44
|
+
|
|
45
|
+
export default defineConfig({
|
|
46
|
+
test: {
|
|
47
|
+
globals: true,
|
|
48
|
+
environment: 'node',
|
|
49
|
+
include: ['tests/**/*.test.js'],
|
|
50
|
+
coverage: {
|
|
51
|
+
provider: 'v8',
|
|
52
|
+
include: ['src/**/*.js'],
|
|
53
|
+
thresholds: { lines: 80, functions: 80, branches: 75 },
|
|
54
|
+
},
|
|
55
|
+
testTimeout: 5000,
|
|
56
|
+
hookTimeout: 10000,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Basic Test Patterns
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
import { describe, it, expect } from 'vitest';
|
|
67
|
+
import { calculateTotal } from '../../src/services/pricing.js';
|
|
68
|
+
|
|
69
|
+
describe('calculateTotal', () => {
|
|
70
|
+
it('sums item prices', () => {
|
|
71
|
+
const items = [{ price: 1000 }, { price: 2500 }];
|
|
72
|
+
expect(calculateTotal(items)).toBe(3500);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns 0 for empty array', () => {
|
|
76
|
+
expect(calculateTotal([])).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Mocking
|
|
84
|
+
|
|
85
|
+
### vi.fn() and vi.mock()
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
89
|
+
import { createUser } from '../../src/services/user-service.js';
|
|
90
|
+
|
|
91
|
+
vi.mock('../../src/repositories/user-repo.js', () => ({
|
|
92
|
+
userRepo: { findByEmail: vi.fn(), save: vi.fn() },
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
import { userRepo } from '../../src/repositories/user-repo.js';
|
|
96
|
+
|
|
97
|
+
describe('createUser', () => {
|
|
98
|
+
it('saves user when email is available', async () => {
|
|
99
|
+
userRepo.findByEmail.mockResolvedValue(null);
|
|
100
|
+
userRepo.save.mockResolvedValue({ id: 1, name: 'Alice' });
|
|
101
|
+
|
|
102
|
+
const user = await createUser({ name: 'Alice', email: 'alice@test.com', password: 'secret' });
|
|
103
|
+
|
|
104
|
+
expect(user.id).toBe(1);
|
|
105
|
+
expect(userRepo.save).toHaveBeenCalledOnce();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('throws ConflictError when email exists', async () => {
|
|
109
|
+
userRepo.findByEmail.mockResolvedValue({ id: 99 });
|
|
110
|
+
|
|
111
|
+
await expect(
|
|
112
|
+
createUser({ name: 'Bob', email: 'taken@test.com', password: 'secret' }),
|
|
113
|
+
).rejects.toThrow('Email already registered');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Testing Async Code
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
it('rejects with NotFoundError for missing user', async () => {
|
|
124
|
+
await expect(userService.getUser(999)).rejects.toThrow('User not found');
|
|
125
|
+
await expect(userService.getUser(999)).rejects.toBeInstanceOf(NotFoundError);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Integration Testing with Supertest
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
import { describe, it, expect } from 'vitest';
|
|
135
|
+
import request from 'supertest';
|
|
136
|
+
import { app } from '../../src/app.js';
|
|
137
|
+
|
|
138
|
+
describe('POST /api/users', () => {
|
|
139
|
+
it('creates a new user', async () => {
|
|
140
|
+
const response = await request(app)
|
|
141
|
+
.post('/api/users')
|
|
142
|
+
.send({ name: 'Alice', email: 'alice@test.com', password: 'secure123' })
|
|
143
|
+
.expect(201);
|
|
144
|
+
|
|
145
|
+
expect(response.body.email).toBe('alice@test.com');
|
|
146
|
+
expect(response.body).not.toHaveProperty('password');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns 422 for invalid data', async () => {
|
|
150
|
+
const response = await request(app)
|
|
151
|
+
.post('/api/users')
|
|
152
|
+
.send({ name: '' })
|
|
153
|
+
.expect(422);
|
|
154
|
+
|
|
155
|
+
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Test Fixtures
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// tests/fixtures/users.js
|
|
166
|
+
export function buildUser(overrides = {}) {
|
|
167
|
+
return {
|
|
168
|
+
id: 1,
|
|
169
|
+
name: 'Alice',
|
|
170
|
+
email: 'alice@test.com',
|
|
171
|
+
role: 'user',
|
|
172
|
+
isActive: true,
|
|
173
|
+
createdAt: new Date('2025-01-01'),
|
|
174
|
+
...overrides,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Parameterized Tests
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
describe('isValidEmail', () => {
|
|
185
|
+
it.each([
|
|
186
|
+
['user@example.com', true],
|
|
187
|
+
['invalid', false],
|
|
188
|
+
['', false],
|
|
189
|
+
['@example.com', false],
|
|
190
|
+
])('validates "%s" as %s', (email, expected) => {
|
|
191
|
+
expect(isValidEmail(email)).toBe(expected);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Test Commands
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npx vitest run tests/unit/ --reporter=dot # Unit tests, minimal output
|
|
202
|
+
npx vitest # Watch mode with HMR
|
|
203
|
+
npx vitest run --coverage # All tests + coverage
|
|
204
|
+
npx vitest run --reporter=junit --outputFile=results.xml # CI
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
_Tests document behavior. Each test should read as a specification of what the code does._
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Code Quality
|
|
2
|
+
|
|
3
|
+
Code quality standards and tooling for Loco applications. Automated enforcement where possible, conventions where not.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tooling
|
|
8
|
+
|
|
9
|
+
### Required in CI
|
|
10
|
+
|
|
11
|
+
| Tool | Purpose | Command |
|
|
12
|
+
|------|---------|---------|
|
|
13
|
+
| `cargo fmt` | Code formatting | `cargo fmt -- --check` |
|
|
14
|
+
| `cargo clippy` | Linting | `cargo clippy -- -D warnings` |
|
|
15
|
+
| `cargo test` | Test suite | `cargo test` or `cargo nextest run` |
|
|
16
|
+
| `cargo audit` | Dependency vulnerabilities | `cargo audit` |
|
|
17
|
+
| `cargo loco doctor` | Environment validation | `cargo loco doctor --production` |
|
|
18
|
+
|
|
19
|
+
### Recommended
|
|
20
|
+
|
|
21
|
+
| Tool | Purpose | Command |
|
|
22
|
+
|------|---------|---------|
|
|
23
|
+
| `cargo nextest` | Faster parallel tests | `cargo nextest run` |
|
|
24
|
+
| `cargo deny` | License and advisory checks | `cargo deny check` |
|
|
25
|
+
| `cargo machete` | Unused dependency detection | `cargo machete` |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Formatting
|
|
30
|
+
|
|
31
|
+
Run `cargo fmt` and move on. Configure in `rustfmt.toml` if needed:
|
|
32
|
+
|
|
33
|
+
```toml
|
|
34
|
+
# rustfmt.toml
|
|
35
|
+
edition = "2021"
|
|
36
|
+
max_width = 100
|
|
37
|
+
use_field_init_shorthand = true
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Linting
|
|
43
|
+
|
|
44
|
+
### Clippy Configuration
|
|
45
|
+
|
|
46
|
+
Deny warnings in CI, allow iterative development locally:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# CI: strict
|
|
50
|
+
cargo clippy -- -D warnings
|
|
51
|
+
|
|
52
|
+
# Local: advisory
|
|
53
|
+
cargo clippy
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Common Clippy lints to enforce:
|
|
57
|
+
- `clippy::unwrap_used` -- no `.unwrap()` in production code
|
|
58
|
+
- `clippy::expect_used` -- prefer `?` or `.ok_or()`
|
|
59
|
+
- `clippy::large_enum_variant` -- box large variants
|
|
60
|
+
- `clippy::needless_pass_by_value` -- borrow instead of clone
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Code Review Checklist
|
|
65
|
+
|
|
66
|
+
### Controllers
|
|
67
|
+
- [ ] Thin: validate, delegate, respond (under 15 lines)
|
|
68
|
+
- [ ] Uses Axum extractors for request data
|
|
69
|
+
- [ ] Returns `Result<impl IntoResponse>`
|
|
70
|
+
- [ ] Auth-protected routes use `auth::JWT` extractor
|
|
71
|
+
|
|
72
|
+
### Models
|
|
73
|
+
- [ ] Domain logic lives here, not in controllers
|
|
74
|
+
- [ ] `_entities/` files are untouched
|
|
75
|
+
- [ ] Validation implemented via `Validatable` trait
|
|
76
|
+
- [ ] Queries use SeaORM, not raw SQL
|
|
77
|
+
|
|
78
|
+
### Workers
|
|
79
|
+
- [ ] Self-contained, no shared controller state
|
|
80
|
+
- [ ] Args are `Serialize + Deserialize`
|
|
81
|
+
- [ ] Error handling with logging context
|
|
82
|
+
- [ ] Registered in `app.rs` `connect_workers`
|
|
83
|
+
|
|
84
|
+
### Migrations
|
|
85
|
+
- [ ] Descriptive names matching auto-detection patterns
|
|
86
|
+
- [ ] `down()` implemented for rollback
|
|
87
|
+
- [ ] Required fields use NOT NULL
|
|
88
|
+
- [ ] Foreign keys and indexes added
|
|
89
|
+
|
|
90
|
+
### Tests
|
|
91
|
+
- [ ] Model tests for domain logic
|
|
92
|
+
- [ ] Request tests for controller endpoints
|
|
93
|
+
- [ ] Auth tested on protected routes
|
|
94
|
+
- [ ] `ForegroundBlocking` mode in test config
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Dependency Management
|
|
99
|
+
|
|
100
|
+
### Adding Dependencies
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
# Cargo.toml -- pin major versions
|
|
104
|
+
[dependencies]
|
|
105
|
+
loco-rs = "0.x"
|
|
106
|
+
sea-orm = { version = "1", features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
|
107
|
+
serde = { version = "1", features = ["derive"] }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Rules
|
|
111
|
+
- Pin major version, allow minor/patch updates
|
|
112
|
+
- Enable only needed feature flags
|
|
113
|
+
- Audit new dependencies: `cargo audit`, `cargo deny`
|
|
114
|
+
- Prefer well-maintained crates with recent activity
|
|
115
|
+
- Remove unused dependencies: `cargo machete`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Performance Guidelines
|
|
120
|
+
|
|
121
|
+
### Database
|
|
122
|
+
- Use `includes` / eager loading for associations (avoid N+1)
|
|
123
|
+
- Add indexes for frequently queried columns
|
|
124
|
+
- Configure connection pool sizes per environment
|
|
125
|
+
- Use `explain` for slow queries
|
|
126
|
+
|
|
127
|
+
### HTTP
|
|
128
|
+
- Enable compression middleware in production
|
|
129
|
+
- Use Loco's built-in caching layer for repeated queries
|
|
130
|
+
- Return only needed fields in view structs (not full entities)
|
|
131
|
+
|
|
132
|
+
### Workers
|
|
133
|
+
- Use `BackgroundQueue` (Redis/Postgres) for production horizontal scaling
|
|
134
|
+
- Tag workers for resource-intensive jobs
|
|
135
|
+
- Keep job args small -- pass IDs, not full objects
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Security
|
|
140
|
+
|
|
141
|
+
### Built-in Protections
|
|
142
|
+
- JWT authentication with configurable token location
|
|
143
|
+
- Secure headers middleware (enabled by default)
|
|
144
|
+
- CORS configuration via YAML
|
|
145
|
+
|
|
146
|
+
### Practices
|
|
147
|
+
- Never commit secrets -- use environment config or Loco's config system
|
|
148
|
+
- Rotate JWT secrets in production
|
|
149
|
+
- Enable `secure_headers` middleware with appropriate presets
|
|
150
|
+
- Validate all user input via `Validatable` or `JsonValidate`
|
|
151
|
+
- Use parameterized SeaORM queries (never string interpolation in queries)
|
|
152
|
+
- Run `cargo audit` in CI
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
_Automate quality: fmt for formatting, clippy for linting, tests for correctness, audit for security. Human review for architecture and domain logic._
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Loco Coding Style
|
|
2
|
+
|
|
3
|
+
Coding style conventions for idiomatic Loco applications. Combines Rust best practices with Loco's Rails-inspired conventions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Convention over configuration**: Follow Loco's prescribed patterns
|
|
10
|
+
- **Fat models, slim controllers**: Business logic belongs on models
|
|
11
|
+
- **Use generators**: Scaffolding maintains consistency
|
|
12
|
+
- **Leverage the type system**: Encode invariants in types, not runtime checks
|
|
13
|
+
- **Clippy is law**: If Clippy warns, fix it
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Naming Conventions
|
|
18
|
+
|
|
19
|
+
### Loco-Specific Naming
|
|
20
|
+
|
|
21
|
+
| Element | Convention | Example |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Controller functions | snake_case verbs | `create`, `list`, `show`, `update`, `destroy` |
|
|
24
|
+
| Route prefixes | plural resource name | `"posts"`, `"users"`, `"auth"` |
|
|
25
|
+
| Model methods | domain verbs | `register`, `verify_email`, `reset_password` |
|
|
26
|
+
| Worker structs | PascalCase + Worker | `ReportWorker`, `EmailWorker` |
|
|
27
|
+
| Worker args | PascalCase + Args | `ReportArgs`, `EmailArgs` |
|
|
28
|
+
| Task structs | PascalCase | `SeedData`, `SyncUsers` |
|
|
29
|
+
| View structs | PascalCase + Response | `PostResponse`, `UserResponse` |
|
|
30
|
+
| Migration files | timestamp + description | `m20231001_000001_create_users` |
|
|
31
|
+
|
|
32
|
+
### Standard Rust Naming
|
|
33
|
+
|
|
34
|
+
| Element | Convention | Example |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Variables, functions | `snake_case` | `user_count`, `get_user` |
|
|
37
|
+
| Types, traits, enums | `PascalCase` | `UserService`, `Serialize` |
|
|
38
|
+
| Constants | `SCREAMING_SNAKE_CASE` | `MAX_RETRIES`, `DEFAULT_PORT` |
|
|
39
|
+
| Modules | `snake_case` | `user_service`, `auth_utils` |
|
|
40
|
+
| Lifetimes | Short lowercase with `'` | `'a`, `'ctx` |
|
|
41
|
+
|
|
42
|
+
### Boolean Naming
|
|
43
|
+
|
|
44
|
+
Prefix with `is_`, `has_`, `can_`, `should_`:
|
|
45
|
+
|
|
46
|
+
```rust
|
|
47
|
+
struct User {
|
|
48
|
+
is_active: bool,
|
|
49
|
+
has_verified_email: bool,
|
|
50
|
+
can_publish: bool,
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Controller Style
|
|
57
|
+
|
|
58
|
+
### Thin Controllers
|
|
59
|
+
|
|
60
|
+
```rust
|
|
61
|
+
// GOOD: Delegates to model
|
|
62
|
+
async fn create(
|
|
63
|
+
State(ctx): State<AppContext>,
|
|
64
|
+
Json(params): Json<CreatePostParams>,
|
|
65
|
+
) -> Result<Json<PostResponse>> {
|
|
66
|
+
let post = posts::ActiveModel::create(&ctx.db, ¶ms).await?;
|
|
67
|
+
Ok(Json(PostResponse::from(post)))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// BAD: Logic in controller
|
|
71
|
+
async fn create(
|
|
72
|
+
State(ctx): State<AppContext>,
|
|
73
|
+
Json(params): Json<CreatePostParams>,
|
|
74
|
+
) -> Result<Json<PostResponse>> {
|
|
75
|
+
// Validation logic here...
|
|
76
|
+
// Business rules here...
|
|
77
|
+
// Direct SQL here...
|
|
78
|
+
// Email sending here...
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Consistent Route Structure
|
|
83
|
+
|
|
84
|
+
```rust
|
|
85
|
+
// GOOD: RESTful, predictable
|
|
86
|
+
pub fn routes() -> Routes {
|
|
87
|
+
Routes::new()
|
|
88
|
+
.prefix("posts")
|
|
89
|
+
.add("/", get(list))
|
|
90
|
+
.add("/", post(create))
|
|
91
|
+
.add("/:id", get(show))
|
|
92
|
+
.add("/:id", put(update))
|
|
93
|
+
.add("/:id", delete(destroy))
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Model Style
|
|
100
|
+
|
|
101
|
+
### Domain Methods
|
|
102
|
+
|
|
103
|
+
```rust
|
|
104
|
+
// GOOD: Rich domain model
|
|
105
|
+
impl super::_entities::users::ActiveModel {
|
|
106
|
+
pub async fn register(db: &DatabaseConnection, params: &RegisterParams) -> ModelResult<Self> {
|
|
107
|
+
// Complete workflow in one place
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
|
|
111
|
+
// Reusable query
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// BAD: Anemic model, logic scattered elsewhere
|
|
116
|
+
// (query in controller, validation in middleware, email in separate service)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Validation
|
|
120
|
+
|
|
121
|
+
Use the `validator` crate with `Validatable` trait:
|
|
122
|
+
|
|
123
|
+
```rust
|
|
124
|
+
impl Validatable for super::_entities::users::ActiveModel {
|
|
125
|
+
fn validator(&self) -> Box<dyn Validate> {
|
|
126
|
+
Box::new(UserValidator {
|
|
127
|
+
email: self.email.clone().into_value().unwrap_or_default(),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Always validate before persistence
|
|
133
|
+
user.validate()?;
|
|
134
|
+
user.insert(db).await?;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Error Handling Style
|
|
140
|
+
|
|
141
|
+
### Use `?` Operator Consistently
|
|
142
|
+
|
|
143
|
+
```rust
|
|
144
|
+
// GOOD: Flat with ? operator
|
|
145
|
+
async fn publish(ctx: &AppContext, id: i32, user_id: i32) -> Result<Post> {
|
|
146
|
+
let post = posts::Entity::find_by_id(id)
|
|
147
|
+
.one(&ctx.db)
|
|
148
|
+
.await?
|
|
149
|
+
.ok_or_else(|| Error::NotFound)?;
|
|
150
|
+
|
|
151
|
+
if post.user_id != user_id {
|
|
152
|
+
return Err(Error::Unauthorized("Not your post".into()));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
update_status(&ctx.db, id, Status::Published).await
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// BAD: Deeply nested match/if chains
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## View Style
|
|
164
|
+
|
|
165
|
+
### Explicit Response Structs
|
|
166
|
+
|
|
167
|
+
```rust
|
|
168
|
+
// GOOD: Dedicated view struct with From impl
|
|
169
|
+
#[derive(Serialize)]
|
|
170
|
+
pub struct PostResponse {
|
|
171
|
+
pub id: i32,
|
|
172
|
+
pub title: String,
|
|
173
|
+
pub author_name: String, // Derived, not raw FK
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
impl PostResponse {
|
|
177
|
+
pub fn from_model_with_user(post: posts::Model, user: users::Model) -> Self {
|
|
178
|
+
Self {
|
|
179
|
+
id: post.id,
|
|
180
|
+
title: post.title,
|
|
181
|
+
author_name: user.name,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// BAD: Returning raw entity models as JSON
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Worker Style
|
|
192
|
+
|
|
193
|
+
### Self-Contained with Serializable Args
|
|
194
|
+
|
|
195
|
+
```rust
|
|
196
|
+
// GOOD: Serializable args, no shared state
|
|
197
|
+
#[derive(Serialize, Deserialize)]
|
|
198
|
+
pub struct EmailArgs {
|
|
199
|
+
pub user_id: i32,
|
|
200
|
+
pub template: String,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[async_trait]
|
|
204
|
+
impl BackgroundWorker<EmailArgs> for EmailWorker {
|
|
205
|
+
fn build(ctx: &AppContext) -> Self { Self { ctx: ctx.clone() } }
|
|
206
|
+
|
|
207
|
+
async fn perform(&self, args: EmailArgs) -> Result<()> {
|
|
208
|
+
let user = users::Entity::find_by_id(args.user_id)
|
|
209
|
+
.one(&self.ctx.db).await?
|
|
210
|
+
.ok_or(Error::NotFound)?;
|
|
211
|
+
// Send email using args.template
|
|
212
|
+
Ok(())
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// BAD: Passing non-serializable types, referencing controller state
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## File and Type Size
|
|
222
|
+
|
|
223
|
+
| Element | Guideline |
|
|
224
|
+
|---|---|
|
|
225
|
+
| Controller function | Under 15 lines (validate, delegate, respond) |
|
|
226
|
+
| Model method | Under 30 lines of logic |
|
|
227
|
+
| View struct | Under 50 lines |
|
|
228
|
+
| Worker perform | Under 40 lines |
|
|
229
|
+
| Module file | Under 300 lines, max 500 |
|
|
230
|
+
| Parameters | Max 5 per function; use struct for more |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Anti-Patterns
|
|
235
|
+
|
|
236
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| Fat controllers | Untestable, unreusable logic | Move to model methods |
|
|
239
|
+
| `.unwrap()` in handlers | Panics crash the request | Use `?` and proper error types |
|
|
240
|
+
| Raw entity as response | Exposes internal schema | Use view structs with `From` |
|
|
241
|
+
| Business logic in workers | Duplicates model logic | Call model methods from workers |
|
|
242
|
+
| Ignoring generators | Inconsistent file placement | Always use `cargo loco generate` |
|
|
243
|
+
| `clone()` on large structs | Hidden performance cost | Borrow with `&` or use `Arc` |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
_Follow Loco conventions: generators for scaffolding, models for logic, controllers for routing, views for shaping. Let the framework guide structure._
|