autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Rocket Framework Skill
|
|
2
|
+
|
|
3
|
+
## Application Setup
|
|
4
|
+
\`\`\`rust
|
|
5
|
+
#[macro_use] extern crate rocket;
|
|
6
|
+
|
|
7
|
+
use rocket::{Build, Rocket, fairing::AdHoc};
|
|
8
|
+
|
|
9
|
+
#[launch]
|
|
10
|
+
fn rocket() -> Rocket<Build> {
|
|
11
|
+
rocket::build()
|
|
12
|
+
.attach(DbPool::init())
|
|
13
|
+
.attach(AdHoc::config::<AppConfig>())
|
|
14
|
+
.mount("/", routes![health])
|
|
15
|
+
.mount("/api/v1/auth", routes![login, register])
|
|
16
|
+
.mount("/api/v1/users", routes![
|
|
17
|
+
list_users,
|
|
18
|
+
create_user,
|
|
19
|
+
get_user,
|
|
20
|
+
update_user,
|
|
21
|
+
delete_user
|
|
22
|
+
])
|
|
23
|
+
.register("/", catchers![
|
|
24
|
+
not_found,
|
|
25
|
+
internal_error,
|
|
26
|
+
unauthorized
|
|
27
|
+
])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[get("/health")]
|
|
31
|
+
fn health() -> &'static str {
|
|
32
|
+
"OK"
|
|
33
|
+
}
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
## Database with rocket_db_pools
|
|
37
|
+
\`\`\`rust
|
|
38
|
+
use rocket_db_pools::{Database, Connection};
|
|
39
|
+
use rocket_db_pools::sqlx::{self, PgPool};
|
|
40
|
+
|
|
41
|
+
#[derive(Database)]
|
|
42
|
+
#[database("postgres")]
|
|
43
|
+
pub struct DbPool(PgPool);
|
|
44
|
+
|
|
45
|
+
// Access in handlers
|
|
46
|
+
#[get("/<id>")]
|
|
47
|
+
async fn get_user(
|
|
48
|
+
mut db: Connection<DbPool>,
|
|
49
|
+
id: &str,
|
|
50
|
+
) -> Result<Json<User>, AppError> {
|
|
51
|
+
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
52
|
+
.fetch_optional(&mut **db)
|
|
53
|
+
.await?
|
|
54
|
+
.ok_or(AppError::NotFound)?;
|
|
55
|
+
|
|
56
|
+
Ok(Json(user))
|
|
57
|
+
}
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
## Request Guards
|
|
61
|
+
\`\`\`rust
|
|
62
|
+
use rocket::{
|
|
63
|
+
request::{self, Request, FromRequest, Outcome},
|
|
64
|
+
http::Status,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
pub struct AuthUser {
|
|
68
|
+
pub user_id: String,
|
|
69
|
+
pub role: String,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[rocket::async_trait]
|
|
73
|
+
impl<'r> FromRequest<'r> for AuthUser {
|
|
74
|
+
type Error = AppError;
|
|
75
|
+
|
|
76
|
+
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
77
|
+
let auth_header = request.headers().get_one("Authorization");
|
|
78
|
+
|
|
79
|
+
let token = match auth_header {
|
|
80
|
+
Some(h) if h.starts_with("Bearer ") => &h[7..],
|
|
81
|
+
_ => return Outcome::Error((Status::Unauthorized, AppError::Unauthorized)),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
match validate_token(token) {
|
|
85
|
+
Ok(claims) => Outcome::Success(AuthUser {
|
|
86
|
+
user_id: claims.sub,
|
|
87
|
+
role: claims.role,
|
|
88
|
+
}),
|
|
89
|
+
Err(_) => Outcome::Error((Status::Unauthorized, AppError::Unauthorized)),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Use in protected routes
|
|
95
|
+
#[get("/profile")]
|
|
96
|
+
async fn get_profile(
|
|
97
|
+
auth: AuthUser,
|
|
98
|
+
mut db: Connection<DbPool>,
|
|
99
|
+
) -> Result<Json<User>, AppError> {
|
|
100
|
+
let user = find_user(&mut db, &auth.user_id).await?;
|
|
101
|
+
Ok(Json(user))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Optional auth
|
|
105
|
+
#[get("/posts")]
|
|
106
|
+
async fn list_posts(
|
|
107
|
+
auth: Option<AuthUser>,
|
|
108
|
+
mut db: Connection<DbPool>,
|
|
109
|
+
) -> Result<Json<Vec<Post>>, AppError> {
|
|
110
|
+
let posts = if let Some(user) = auth {
|
|
111
|
+
// Return user's posts
|
|
112
|
+
get_user_posts(&mut db, &user.user_id).await?
|
|
113
|
+
} else {
|
|
114
|
+
// Return public posts
|
|
115
|
+
get_public_posts(&mut db).await?
|
|
116
|
+
};
|
|
117
|
+
Ok(Json(posts))
|
|
118
|
+
}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
## Request Data and Forms
|
|
122
|
+
\`\`\`rust
|
|
123
|
+
use rocket::serde::{Deserialize, json::Json};
|
|
124
|
+
use rocket::form::Form;
|
|
125
|
+
|
|
126
|
+
// JSON body
|
|
127
|
+
#[derive(Deserialize)]
|
|
128
|
+
#[serde(crate = "rocket::serde")]
|
|
129
|
+
pub struct CreateUserRequest {
|
|
130
|
+
pub email: String,
|
|
131
|
+
pub name: String,
|
|
132
|
+
pub password: String,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[post("/", data = "<body>")]
|
|
136
|
+
async fn create_user(
|
|
137
|
+
mut db: Connection<DbPool>,
|
|
138
|
+
body: Json<CreateUserRequest>,
|
|
139
|
+
) -> Result<(Status, Json<User>), AppError> {
|
|
140
|
+
// Validate
|
|
141
|
+
if body.email.is_empty() {
|
|
142
|
+
return Err(AppError::Validation("Email is required".into()));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let user = User::new(&body.email, &body.name, &body.password)?;
|
|
146
|
+
save_user(&mut db, &user).await?;
|
|
147
|
+
|
|
148
|
+
Ok((Status::Created, Json(user)))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Form data
|
|
152
|
+
#[derive(FromForm)]
|
|
153
|
+
pub struct LoginForm {
|
|
154
|
+
email: String,
|
|
155
|
+
password: String,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[post("/login", data = "<form>")]
|
|
159
|
+
async fn login_form(form: Form<LoginForm>) -> Result<Json<TokenResponse>, AppError> {
|
|
160
|
+
// Process form...
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Query parameters
|
|
164
|
+
#[get("/?<page>&<per_page>")]
|
|
165
|
+
async fn list_users(
|
|
166
|
+
mut db: Connection<DbPool>,
|
|
167
|
+
page: Option<u32>,
|
|
168
|
+
per_page: Option<u32>,
|
|
169
|
+
) -> Result<Json<Vec<User>>, AppError> {
|
|
170
|
+
let page = page.unwrap_or(1);
|
|
171
|
+
let per_page = per_page.unwrap_or(20).min(100);
|
|
172
|
+
let offset = (page - 1) * per_page;
|
|
173
|
+
|
|
174
|
+
let users = sqlx::query_as!(User,
|
|
175
|
+
"SELECT * FROM users LIMIT $1 OFFSET $2",
|
|
176
|
+
per_page as i64,
|
|
177
|
+
offset as i64
|
|
178
|
+
)
|
|
179
|
+
.fetch_all(&mut **db)
|
|
180
|
+
.await?;
|
|
181
|
+
|
|
182
|
+
Ok(Json(users))
|
|
183
|
+
}
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
## Error Handling
|
|
187
|
+
\`\`\`rust
|
|
188
|
+
use rocket::{
|
|
189
|
+
response::{self, Responder, Response},
|
|
190
|
+
http::{Status, ContentType},
|
|
191
|
+
Request,
|
|
192
|
+
};
|
|
193
|
+
use thiserror::Error;
|
|
194
|
+
|
|
195
|
+
#[derive(Error, Debug)]
|
|
196
|
+
pub enum AppError {
|
|
197
|
+
#[error("Resource not found")]
|
|
198
|
+
NotFound,
|
|
199
|
+
|
|
200
|
+
#[error("Unauthorized")]
|
|
201
|
+
Unauthorized,
|
|
202
|
+
|
|
203
|
+
#[error("Validation error: {0}")]
|
|
204
|
+
Validation(String),
|
|
205
|
+
|
|
206
|
+
#[error("Database error")]
|
|
207
|
+
Database(#[from] sqlx::Error),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
impl<'r> Responder<'r, 'static> for AppError {
|
|
211
|
+
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
|
|
212
|
+
let (status, message) = match &self {
|
|
213
|
+
AppError::NotFound => (Status::NotFound, self.to_string()),
|
|
214
|
+
AppError::Unauthorized => (Status::Unauthorized, self.to_string()),
|
|
215
|
+
AppError::Validation(_) => (Status::BadRequest, self.to_string()),
|
|
216
|
+
AppError::Database(e) => {
|
|
217
|
+
eprintln!("Database error: {:?}", e);
|
|
218
|
+
(Status::InternalServerError, "Internal server error".into())
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let body = format!(r#"{{"error":"{}"}}"#, message);
|
|
223
|
+
|
|
224
|
+
Response::build()
|
|
225
|
+
.status(status)
|
|
226
|
+
.header(ContentType::JSON)
|
|
227
|
+
.sized_body(body.len(), std::io::Cursor::new(body))
|
|
228
|
+
.ok()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Error catchers
|
|
233
|
+
#[catch(404)]
|
|
234
|
+
fn not_found() -> Json<serde_json::Value> {
|
|
235
|
+
Json(serde_json::json!({
|
|
236
|
+
"error": "Resource not found"
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[catch(500)]
|
|
241
|
+
fn internal_error() -> Json<serde_json::Value> {
|
|
242
|
+
Json(serde_json::json!({
|
|
243
|
+
"error": "Internal server error"
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[catch(401)]
|
|
248
|
+
fn unauthorized() -> Json<serde_json::Value> {
|
|
249
|
+
Json(serde_json::json!({
|
|
250
|
+
"error": "Unauthorized"
|
|
251
|
+
}))
|
|
252
|
+
}
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
## Fairings (Middleware)
|
|
256
|
+
\`\`\`rust
|
|
257
|
+
use rocket::{
|
|
258
|
+
fairing::{self, Fairing, Info, Kind},
|
|
259
|
+
Data, Request, Response,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
pub struct RequestTimer;
|
|
263
|
+
|
|
264
|
+
#[rocket::async_trait]
|
|
265
|
+
impl Fairing for RequestTimer {
|
|
266
|
+
fn info(&self) -> Info {
|
|
267
|
+
Info {
|
|
268
|
+
name: "Request Timer",
|
|
269
|
+
kind: Kind::Request | Kind::Response,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
|
|
274
|
+
request.local_cache(|| std::time::Instant::now());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async fn on_response<'r>(&self, request: &'r Request<'_>, _: &mut Response<'r>) {
|
|
278
|
+
let start = request.local_cache(|| std::time::Instant::now());
|
|
279
|
+
let duration = start.elapsed();
|
|
280
|
+
println!("{} {} - {:?}", request.method(), request.uri(), duration);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// CORS fairing
|
|
285
|
+
use rocket::fairing::AdHoc;
|
|
286
|
+
|
|
287
|
+
pub fn cors_fairing() -> AdHoc {
|
|
288
|
+
AdHoc::on_response("CORS", |_, response| Box::pin(async move {
|
|
289
|
+
response.set_header(rocket::http::Header::new("Access-Control-Allow-Origin", "*"));
|
|
290
|
+
response.set_header(rocket::http::Header::new("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"));
|
|
291
|
+
response.set_header(rocket::http::Header::new("Access-Control-Allow-Headers", "Authorization, Content-Type"));
|
|
292
|
+
}))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Attach fairings
|
|
296
|
+
#[launch]
|
|
297
|
+
fn rocket() -> Rocket<Build> {
|
|
298
|
+
rocket::build()
|
|
299
|
+
.attach(RequestTimer)
|
|
300
|
+
.attach(cors_fairing())
|
|
301
|
+
// ...
|
|
302
|
+
}
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
## Testing
|
|
306
|
+
\`\`\`rust
|
|
307
|
+
#[cfg(test)]
|
|
308
|
+
mod tests {
|
|
309
|
+
use super::*;
|
|
310
|
+
use rocket::local::asynchronous::Client;
|
|
311
|
+
use rocket::http::{Status, ContentType};
|
|
312
|
+
|
|
313
|
+
async fn create_client() -> Client {
|
|
314
|
+
Client::tracked(rocket())
|
|
315
|
+
.await
|
|
316
|
+
.expect("valid rocket instance")
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[rocket::async_test]
|
|
320
|
+
async fn test_health() {
|
|
321
|
+
let client = create_client().await;
|
|
322
|
+
let response = client.get("/health").dispatch().await;
|
|
323
|
+
assert_eq!(response.status(), Status::Ok);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[rocket::async_test]
|
|
327
|
+
async fn test_create_user() {
|
|
328
|
+
let client = create_client().await;
|
|
329
|
+
let response = client
|
|
330
|
+
.post("/api/v1/users")
|
|
331
|
+
.header(ContentType::JSON)
|
|
332
|
+
.body(r#"{"email":"test@example.com","name":"Test","password":"password123"}"#)
|
|
333
|
+
.dispatch()
|
|
334
|
+
.await;
|
|
335
|
+
|
|
336
|
+
assert_eq!(response.status(), Status::Created);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[rocket::async_test]
|
|
340
|
+
async fn test_get_user_not_found() {
|
|
341
|
+
let client = create_client().await;
|
|
342
|
+
let response = client.get("/api/v1/users/nonexistent").dispatch().await;
|
|
343
|
+
assert_eq!(response.status(), Status::NotFound);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[rocket::async_test]
|
|
347
|
+
async fn test_protected_route_without_auth() {
|
|
348
|
+
let client = create_client().await;
|
|
349
|
+
let response = client.get("/api/v1/users/profile").dispatch().await;
|
|
350
|
+
assert_eq!(response.status(), Status::Unauthorized);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
\`\`\`
|
|
354
|
+
|
|
355
|
+
## ✅ DO
|
|
356
|
+
- Use request guards for auth and validation
|
|
357
|
+
- Implement \`Responder\` for custom error types
|
|
358
|
+
- Register error catchers with \`register\`
|
|
359
|
+
- Use fairings for cross-cutting concerns
|
|
360
|
+
- Use \`rocket_db_pools\` for database connections
|
|
361
|
+
|
|
362
|
+
## ❌ DON'T
|
|
363
|
+
- Don't use \`.unwrap()\` in handlers - return \`Result\`
|
|
364
|
+
- Don't forget to mount routes with \`mount\`
|
|
365
|
+
- Don't block async handlers with sync code
|
|
366
|
+
- Don't expose internal errors to clients
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Rust Skill
|
|
2
|
+
|
|
3
|
+
## Ownership & Borrowing
|
|
4
|
+
\`\`\`rust
|
|
5
|
+
// Ownership - values have a single owner
|
|
6
|
+
let s1 = String::from("hello");
|
|
7
|
+
let s2 = s1; // s1 is MOVED, can no longer be used
|
|
8
|
+
|
|
9
|
+
// Clone for explicit copy
|
|
10
|
+
let s1 = String::from("hello");
|
|
11
|
+
let s2 = s1.clone(); // Both valid
|
|
12
|
+
|
|
13
|
+
// Borrowing - references without ownership
|
|
14
|
+
fn print_length(s: &String) -> usize {
|
|
15
|
+
s.len() // Can read but not modify
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Mutable borrow - only ONE at a time
|
|
19
|
+
fn append(s: &mut String) {
|
|
20
|
+
s.push_str(" world");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let mut s = String::from("hello");
|
|
24
|
+
append(&mut s); // s is now "hello world"
|
|
25
|
+
|
|
26
|
+
// Lifetimes - ensure references are valid
|
|
27
|
+
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
|
|
28
|
+
if x.len() > y.len() { x } else { y }
|
|
29
|
+
}
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
## Error Handling with Result and Option
|
|
33
|
+
\`\`\`rust
|
|
34
|
+
use std::fs::File;
|
|
35
|
+
use std::io::{self, Read};
|
|
36
|
+
|
|
37
|
+
// Result<T, E> for operations that can fail
|
|
38
|
+
fn read_file(path: &str) -> Result<String, io::Error> {
|
|
39
|
+
let mut file = File::open(path)?; // ? propagates error
|
|
40
|
+
let mut contents = String::new();
|
|
41
|
+
file.read_to_string(&mut contents)?;
|
|
42
|
+
Ok(contents)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Option<T> for values that may not exist
|
|
46
|
+
fn find_user(id: u32) -> Option<User> {
|
|
47
|
+
users.iter().find(|u| u.id == id).cloned()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pattern matching on Result/Option
|
|
51
|
+
fn process_user(id: u32) -> Result<(), AppError> {
|
|
52
|
+
match find_user(id) {
|
|
53
|
+
Some(user) => {
|
|
54
|
+
println!("Found: {}", user.name);
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
None => Err(AppError::NotFound),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Combinators for cleaner code
|
|
62
|
+
fn get_user_email(id: u32) -> Option<String> {
|
|
63
|
+
find_user(id)
|
|
64
|
+
.filter(|u| u.is_active)
|
|
65
|
+
.map(|u| u.email.clone())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Converting between Option and Result
|
|
69
|
+
fn require_user(id: u32) -> Result<User, AppError> {
|
|
70
|
+
find_user(id).ok_or(AppError::NotFound)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// unwrap_or for defaults
|
|
74
|
+
let name = user.nickname.unwrap_or_else(|| user.name.clone());
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
## Custom Error Types with thiserror
|
|
78
|
+
\`\`\`rust
|
|
79
|
+
use thiserror::Error;
|
|
80
|
+
|
|
81
|
+
#[derive(Error, Debug)]
|
|
82
|
+
pub enum AppError {
|
|
83
|
+
#[error("User not found: {0}")]
|
|
84
|
+
NotFound(String),
|
|
85
|
+
|
|
86
|
+
#[error("Unauthorized access")]
|
|
87
|
+
Unauthorized,
|
|
88
|
+
|
|
89
|
+
#[error("Validation failed: {0}")]
|
|
90
|
+
Validation(String),
|
|
91
|
+
|
|
92
|
+
#[error("Database error")]
|
|
93
|
+
Database(#[from] sqlx::Error),
|
|
94
|
+
|
|
95
|
+
#[error("IO error")]
|
|
96
|
+
Io(#[from] std::io::Error),
|
|
97
|
+
|
|
98
|
+
#[error("External service error: {0}")]
|
|
99
|
+
External(String),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Using the error
|
|
103
|
+
fn get_user(id: &str) -> Result<User, AppError> {
|
|
104
|
+
let user = db.find_user(id)
|
|
105
|
+
.map_err(AppError::Database)?
|
|
106
|
+
.ok_or_else(|| AppError::NotFound(id.to_string()))?;
|
|
107
|
+
|
|
108
|
+
if !user.is_active {
|
|
109
|
+
return Err(AppError::Validation("User is inactive".into()));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Ok(user)
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Structs and Implementations
|
|
117
|
+
\`\`\`rust
|
|
118
|
+
use serde::{Deserialize, Serialize};
|
|
119
|
+
|
|
120
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
121
|
+
pub struct User {
|
|
122
|
+
pub id: String,
|
|
123
|
+
pub email: String,
|
|
124
|
+
pub name: String,
|
|
125
|
+
#[serde(skip_serializing)]
|
|
126
|
+
password_hash: String,
|
|
127
|
+
pub is_active: bool,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
impl User {
|
|
131
|
+
// Constructor
|
|
132
|
+
pub fn new(email: String, name: String, password: &str) -> Result<Self, AppError> {
|
|
133
|
+
let password_hash = hash_password(password)?;
|
|
134
|
+
Ok(Self {
|
|
135
|
+
id: Uuid::new_v4().to_string(),
|
|
136
|
+
email,
|
|
137
|
+
name,
|
|
138
|
+
password_hash,
|
|
139
|
+
is_active: true,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Method
|
|
144
|
+
pub fn verify_password(&self, password: &str) -> bool {
|
|
145
|
+
verify_password(password, &self.password_hash)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Builder pattern
|
|
149
|
+
pub fn with_id(mut self, id: String) -> Self {
|
|
150
|
+
self.id = id;
|
|
151
|
+
self
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Default trait implementation
|
|
156
|
+
impl Default for User {
|
|
157
|
+
fn default() -> Self {
|
|
158
|
+
Self {
|
|
159
|
+
id: String::new(),
|
|
160
|
+
email: String::new(),
|
|
161
|
+
name: String::from("Anonymous"),
|
|
162
|
+
password_hash: String::new(),
|
|
163
|
+
is_active: false,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Traits
|
|
170
|
+
\`\`\`rust
|
|
171
|
+
// Define a trait
|
|
172
|
+
pub trait Repository<T> {
|
|
173
|
+
fn find_by_id(&self, id: &str) -> Result<Option<T>, AppError>;
|
|
174
|
+
fn create(&self, entity: &T) -> Result<(), AppError>;
|
|
175
|
+
fn update(&self, entity: &T) -> Result<(), AppError>;
|
|
176
|
+
fn delete(&self, id: &str) -> Result<(), AppError>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Implement for a specific type
|
|
180
|
+
pub struct PostgresUserRepository {
|
|
181
|
+
pool: PgPool,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
impl Repository<User> for PostgresUserRepository {
|
|
185
|
+
fn find_by_id(&self, id: &str) -> Result<Option<User>, AppError> {
|
|
186
|
+
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
187
|
+
.fetch_optional(&self.pool)
|
|
188
|
+
.await
|
|
189
|
+
.map_err(AppError::Database)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ... other implementations
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Trait bounds in functions
|
|
196
|
+
fn process<T: Repository<User> + Send + Sync>(repo: &T) -> Result<(), AppError> {
|
|
197
|
+
let user = repo.find_by_id("123")?;
|
|
198
|
+
// ...
|
|
199
|
+
Ok(())
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Using impl Trait for return types
|
|
203
|
+
fn create_repository() -> impl Repository<User> {
|
|
204
|
+
PostgresUserRepository::new()
|
|
205
|
+
}
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
## Async/Await
|
|
209
|
+
\`\`\`rust
|
|
210
|
+
use tokio;
|
|
211
|
+
|
|
212
|
+
#[tokio::main]
|
|
213
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
214
|
+
let result = fetch_data().await?;
|
|
215
|
+
println!("Got: {:?}", result);
|
|
216
|
+
Ok(())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async fn fetch_data() -> Result<Data, AppError> {
|
|
220
|
+
let client = reqwest::Client::new();
|
|
221
|
+
let response = client
|
|
222
|
+
.get("https://api.example.com/data")
|
|
223
|
+
.send()
|
|
224
|
+
.await?
|
|
225
|
+
.json::<Data>()
|
|
226
|
+
.await?;
|
|
227
|
+
Ok(response)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Concurrent execution
|
|
231
|
+
async fn fetch_all(urls: Vec<String>) -> Vec<Result<Response, Error>> {
|
|
232
|
+
let futures: Vec<_> = urls.into_iter()
|
|
233
|
+
.map(|url| fetch_url(url))
|
|
234
|
+
.collect();
|
|
235
|
+
|
|
236
|
+
futures::future::join_all(futures).await
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// With timeout
|
|
240
|
+
use tokio::time::{timeout, Duration};
|
|
241
|
+
|
|
242
|
+
async fn fetch_with_timeout(url: &str) -> Result<Response, AppError> {
|
|
243
|
+
timeout(Duration::from_secs(5), fetch_url(url))
|
|
244
|
+
.await
|
|
245
|
+
.map_err(|_| AppError::External("Request timed out".into()))?
|
|
246
|
+
}
|
|
247
|
+
\`\`\`
|
|
248
|
+
|
|
249
|
+
## Collections and Iterators
|
|
250
|
+
\`\`\`rust
|
|
251
|
+
// Vector operations
|
|
252
|
+
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
|
|
253
|
+
|
|
254
|
+
// Map, filter, collect
|
|
255
|
+
let doubled: Vec<i32> = numbers.iter()
|
|
256
|
+
.filter(|&n| n % 2 == 0)
|
|
257
|
+
.map(|n| n * 2)
|
|
258
|
+
.collect();
|
|
259
|
+
|
|
260
|
+
// HashMap
|
|
261
|
+
use std::collections::HashMap;
|
|
262
|
+
|
|
263
|
+
let mut scores: HashMap<String, i32> = HashMap::new();
|
|
264
|
+
scores.insert("Alice".to_string(), 100);
|
|
265
|
+
scores.entry("Bob".to_string()).or_insert(0);
|
|
266
|
+
|
|
267
|
+
// Iterate with enumerate
|
|
268
|
+
for (index, value) in numbers.iter().enumerate() {
|
|
269
|
+
println!("{}: {}", index, value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fold/reduce
|
|
273
|
+
let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x);
|
|
274
|
+
|
|
275
|
+
// Find and position
|
|
276
|
+
let first_even = numbers.iter().find(|&n| n % 2 == 0);
|
|
277
|
+
let position = numbers.iter().position(|&n| n > 3);
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
## Testing
|
|
281
|
+
\`\`\`rust
|
|
282
|
+
#[cfg(test)]
|
|
283
|
+
mod tests {
|
|
284
|
+
use super::*;
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn test_user_creation() {
|
|
288
|
+
let user = User::new(
|
|
289
|
+
"test@example.com".to_string(),
|
|
290
|
+
"Test User".to_string(),
|
|
291
|
+
"password123"
|
|
292
|
+
).unwrap();
|
|
293
|
+
|
|
294
|
+
assert_eq!(user.email, "test@example.com");
|
|
295
|
+
assert!(user.is_active);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn test_password_verification() {
|
|
300
|
+
let user = User::new(
|
|
301
|
+
"test@example.com".to_string(),
|
|
302
|
+
"Test".to_string(),
|
|
303
|
+
"password123"
|
|
304
|
+
).unwrap();
|
|
305
|
+
|
|
306
|
+
assert!(user.verify_password("password123"));
|
|
307
|
+
assert!(!user.verify_password("wrong"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
#[should_panic(expected = "invalid email")]
|
|
312
|
+
fn test_invalid_email_panics() {
|
|
313
|
+
User::new("invalid".to_string(), "Test".to_string(), "pass").unwrap();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Async tests with tokio
|
|
317
|
+
#[tokio::test]
|
|
318
|
+
async fn test_fetch_user() {
|
|
319
|
+
let repo = MockUserRepository::new();
|
|
320
|
+
let user = repo.find_by_id("123").await.unwrap();
|
|
321
|
+
assert!(user.is_some());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
## ✅ DO
|
|
327
|
+
- Handle all \`Result\` and \`Option\` cases explicitly
|
|
328
|
+
- Use \`?\` operator for error propagation
|
|
329
|
+
- Use \`thiserror\` for library errors, \`anyhow\` for applications
|
|
330
|
+
- Prefer iterators over manual loops
|
|
331
|
+
- Use \`clippy\` and \`rustfmt\`
|
|
332
|
+
- Derive common traits: \`Debug\`, \`Clone\`, \`Serialize\`, \`Deserialize\`
|
|
333
|
+
- Use \`Arc<T>\` for shared ownership across threads
|
|
334
|
+
|
|
335
|
+
## ❌ DON'T
|
|
336
|
+
- Don't use \`.unwrap()\` in production code (use \`?\` or handle errors)
|
|
337
|
+
- Don't ignore compiler warnings
|
|
338
|
+
- Don't fight the borrow checker - redesign if needed
|
|
339
|
+
- Don't use \`unsafe\` without good reason
|
|
340
|
+
- Don't clone everything to avoid borrowing (refactor instead)
|
|
341
|
+
- Don't use \`String\` when \`&str\` suffices
|