autoworkflow 3.1.5 → 3.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/.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/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -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 +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Actix Web Skill
|
|
2
|
+
|
|
3
|
+
## Application Setup
|
|
4
|
+
\`\`\`rust
|
|
5
|
+
use actix_web::{web, App, HttpServer, middleware};
|
|
6
|
+
use actix_cors::Cors;
|
|
7
|
+
|
|
8
|
+
#[actix_web::main]
|
|
9
|
+
async fn main() -> std::io::Result<()> {
|
|
10
|
+
// Initialize logging
|
|
11
|
+
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
|
12
|
+
|
|
13
|
+
// Create shared state
|
|
14
|
+
let db_pool = create_pool().await.expect("Failed to create pool");
|
|
15
|
+
let app_state = web::Data::new(AppState { db: db_pool });
|
|
16
|
+
|
|
17
|
+
HttpServer::new(move || {
|
|
18
|
+
// Configure CORS
|
|
19
|
+
let cors = Cors::default()
|
|
20
|
+
.allow_any_origin()
|
|
21
|
+
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
|
|
22
|
+
.allowed_headers(vec!["Authorization", "Content-Type"])
|
|
23
|
+
.max_age(3600);
|
|
24
|
+
|
|
25
|
+
App::new()
|
|
26
|
+
.app_data(app_state.clone())
|
|
27
|
+
.wrap(middleware::Logger::default())
|
|
28
|
+
.wrap(middleware::Compress::default())
|
|
29
|
+
.wrap(cors)
|
|
30
|
+
.configure(configure_routes)
|
|
31
|
+
})
|
|
32
|
+
.bind("127.0.0.1:8080")?
|
|
33
|
+
.run()
|
|
34
|
+
.await
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|
38
|
+
cfg.service(
|
|
39
|
+
web::scope("/api/v1")
|
|
40
|
+
.service(
|
|
41
|
+
web::scope("/auth")
|
|
42
|
+
.route("/login", web::post().to(login))
|
|
43
|
+
.route("/register", web::post().to(register))
|
|
44
|
+
)
|
|
45
|
+
.service(
|
|
46
|
+
web::scope("/users")
|
|
47
|
+
.wrap(AuthMiddleware)
|
|
48
|
+
.route("", web::get().to(list_users))
|
|
49
|
+
.route("", web::post().to(create_user))
|
|
50
|
+
.route("/{id}", web::get().to(get_user))
|
|
51
|
+
.route("/{id}", web::put().to(update_user))
|
|
52
|
+
.route("/{id}", web::delete().to(delete_user))
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Shared State
|
|
59
|
+
\`\`\`rust
|
|
60
|
+
use sqlx::PgPool;
|
|
61
|
+
|
|
62
|
+
pub struct AppState {
|
|
63
|
+
pub db: PgPool,
|
|
64
|
+
pub config: AppConfig,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Access in handlers
|
|
68
|
+
async fn get_user(
|
|
69
|
+
state: web::Data<AppState>,
|
|
70
|
+
path: web::Path<String>,
|
|
71
|
+
) -> Result<HttpResponse, AppError> {
|
|
72
|
+
let id = path.into_inner();
|
|
73
|
+
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
74
|
+
.fetch_optional(&state.db)
|
|
75
|
+
.await?
|
|
76
|
+
.ok_or(AppError::NotFound)?;
|
|
77
|
+
|
|
78
|
+
Ok(HttpResponse::Ok().json(user))
|
|
79
|
+
}
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## Request Extractors
|
|
83
|
+
\`\`\`rust
|
|
84
|
+
use actix_web::{web, HttpResponse};
|
|
85
|
+
use serde::Deserialize;
|
|
86
|
+
use validator::Validate;
|
|
87
|
+
|
|
88
|
+
// JSON body
|
|
89
|
+
#[derive(Deserialize, Validate)]
|
|
90
|
+
pub struct CreateUserRequest {
|
|
91
|
+
#[validate(email)]
|
|
92
|
+
pub email: String,
|
|
93
|
+
#[validate(length(min = 2, max = 100))]
|
|
94
|
+
pub name: String,
|
|
95
|
+
#[validate(length(min = 8))]
|
|
96
|
+
pub password: String,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async fn create_user(
|
|
100
|
+
state: web::Data<AppState>,
|
|
101
|
+
body: web::Json<CreateUserRequest>,
|
|
102
|
+
) -> Result<HttpResponse, AppError> {
|
|
103
|
+
// Validate request
|
|
104
|
+
body.validate().map_err(|e| AppError::Validation(e.to_string()))?;
|
|
105
|
+
|
|
106
|
+
let user = User::new(body.email.clone(), body.name.clone(), &body.password)?;
|
|
107
|
+
// ... save user
|
|
108
|
+
|
|
109
|
+
Ok(HttpResponse::Created().json(user))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Query parameters
|
|
113
|
+
#[derive(Deserialize)]
|
|
114
|
+
pub struct PaginationQuery {
|
|
115
|
+
#[serde(default = "default_page")]
|
|
116
|
+
pub page: u32,
|
|
117
|
+
#[serde(default = "default_per_page")]
|
|
118
|
+
pub per_page: u32,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn default_page() -> u32 { 1 }
|
|
122
|
+
fn default_per_page() -> u32 { 20 }
|
|
123
|
+
|
|
124
|
+
async fn list_users(
|
|
125
|
+
state: web::Data<AppState>,
|
|
126
|
+
query: web::Query<PaginationQuery>,
|
|
127
|
+
) -> Result<HttpResponse, AppError> {
|
|
128
|
+
let offset = (query.page - 1) * query.per_page;
|
|
129
|
+
let users = sqlx::query_as!(User,
|
|
130
|
+
"SELECT * FROM users LIMIT $1 OFFSET $2",
|
|
131
|
+
query.per_page as i64,
|
|
132
|
+
offset as i64
|
|
133
|
+
)
|
|
134
|
+
.fetch_all(&state.db)
|
|
135
|
+
.await?;
|
|
136
|
+
|
|
137
|
+
Ok(HttpResponse::Ok().json(users))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Path parameters
|
|
141
|
+
async fn get_user(path: web::Path<(String,)>) -> Result<HttpResponse, AppError> {
|
|
142
|
+
let (id,) = path.into_inner();
|
|
143
|
+
// ...
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Multiple path params
|
|
147
|
+
async fn get_post_comment(
|
|
148
|
+
path: web::Path<(String, String)>,
|
|
149
|
+
) -> Result<HttpResponse, AppError> {
|
|
150
|
+
let (post_id, comment_id) = path.into_inner();
|
|
151
|
+
// ...
|
|
152
|
+
}
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
## Custom Middleware
|
|
156
|
+
\`\`\`rust
|
|
157
|
+
use actix_web::{dev::{ServiceRequest, ServiceResponse, Transform, Service}, Error, HttpMessage};
|
|
158
|
+
use futures::future::{ok, Ready, LocalBoxFuture};
|
|
159
|
+
use std::rc::Rc;
|
|
160
|
+
|
|
161
|
+
pub struct AuthMiddleware;
|
|
162
|
+
|
|
163
|
+
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
|
|
164
|
+
where
|
|
165
|
+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
|
166
|
+
B: 'static,
|
|
167
|
+
{
|
|
168
|
+
type Response = ServiceResponse<B>;
|
|
169
|
+
type Error = Error;
|
|
170
|
+
type Transform = AuthMiddlewareService<S>;
|
|
171
|
+
type InitError = ();
|
|
172
|
+
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
|
173
|
+
|
|
174
|
+
fn new_transform(&self, service: S) -> Self::Future {
|
|
175
|
+
ok(AuthMiddlewareService {
|
|
176
|
+
service: Rc::new(service),
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pub struct AuthMiddlewareService<S> {
|
|
182
|
+
service: Rc<S>,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
|
|
186
|
+
where
|
|
187
|
+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
|
188
|
+
B: 'static,
|
|
189
|
+
{
|
|
190
|
+
type Response = ServiceResponse<B>;
|
|
191
|
+
type Error = Error;
|
|
192
|
+
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
193
|
+
|
|
194
|
+
fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
|
|
195
|
+
self.service.poll_ready(ctx)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn call(&self, req: ServiceRequest) -> Self::Future {
|
|
199
|
+
let service = Rc::clone(&self.service);
|
|
200
|
+
|
|
201
|
+
Box::pin(async move {
|
|
202
|
+
// Extract and validate token
|
|
203
|
+
let auth_header = req.headers()
|
|
204
|
+
.get("Authorization")
|
|
205
|
+
.and_then(|h| h.to_str().ok());
|
|
206
|
+
|
|
207
|
+
let token = match auth_header {
|
|
208
|
+
Some(h) if h.starts_with("Bearer ") => &h[7..],
|
|
209
|
+
_ => return Err(AppError::Unauthorized.into()),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let claims = validate_token(token)
|
|
213
|
+
.map_err(|_| AppError::Unauthorized)?;
|
|
214
|
+
|
|
215
|
+
// Store claims in request extensions
|
|
216
|
+
req.extensions_mut().insert(claims);
|
|
217
|
+
|
|
218
|
+
service.call(req).await
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Access auth data in handler
|
|
224
|
+
async fn protected_handler(req: HttpRequest) -> Result<HttpResponse, AppError> {
|
|
225
|
+
let claims = req.extensions().get::<Claims>()
|
|
226
|
+
.ok_or(AppError::Unauthorized)?;
|
|
227
|
+
|
|
228
|
+
println!("User ID: {}", claims.user_id);
|
|
229
|
+
Ok(HttpResponse::Ok().finish())
|
|
230
|
+
}
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
## Error Handling
|
|
234
|
+
\`\`\`rust
|
|
235
|
+
use actix_web::{HttpResponse, ResponseError, http::StatusCode};
|
|
236
|
+
use thiserror::Error;
|
|
237
|
+
|
|
238
|
+
#[derive(Error, Debug)]
|
|
239
|
+
pub enum AppError {
|
|
240
|
+
#[error("Resource not found")]
|
|
241
|
+
NotFound,
|
|
242
|
+
|
|
243
|
+
#[error("Unauthorized")]
|
|
244
|
+
Unauthorized,
|
|
245
|
+
|
|
246
|
+
#[error("Validation error: {0}")]
|
|
247
|
+
Validation(String),
|
|
248
|
+
|
|
249
|
+
#[error("Database error")]
|
|
250
|
+
Database(#[from] sqlx::Error),
|
|
251
|
+
|
|
252
|
+
#[error("Internal error")]
|
|
253
|
+
Internal(String),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
impl ResponseError for AppError {
|
|
257
|
+
fn status_code(&self) -> StatusCode {
|
|
258
|
+
match self {
|
|
259
|
+
AppError::NotFound => StatusCode::NOT_FOUND,
|
|
260
|
+
AppError::Unauthorized => StatusCode::UNAUTHORIZED,
|
|
261
|
+
AppError::Validation(_) => StatusCode::BAD_REQUEST,
|
|
262
|
+
AppError::Database(_) | AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn error_response(&self) -> HttpResponse {
|
|
267
|
+
let status = self.status_code();
|
|
268
|
+
let message = match self {
|
|
269
|
+
AppError::Database(_) | AppError::Internal(_) => "Internal server error".to_string(),
|
|
270
|
+
_ => self.to_string(),
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
HttpResponse::build(status).json(serde_json::json!({
|
|
274
|
+
"error": message
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
## Testing
|
|
281
|
+
\`\`\`rust
|
|
282
|
+
#[cfg(test)]
|
|
283
|
+
mod tests {
|
|
284
|
+
use super::*;
|
|
285
|
+
use actix_web::{test, App};
|
|
286
|
+
|
|
287
|
+
#[actix_web::test]
|
|
288
|
+
async fn test_create_user() {
|
|
289
|
+
let app = test::init_service(
|
|
290
|
+
App::new()
|
|
291
|
+
.app_data(web::Data::new(create_test_state().await))
|
|
292
|
+
.configure(configure_routes)
|
|
293
|
+
).await;
|
|
294
|
+
|
|
295
|
+
let req = test::TestRequest::post()
|
|
296
|
+
.uri("/api/v1/users")
|
|
297
|
+
.set_json(serde_json::json!({
|
|
298
|
+
"email": "test@example.com",
|
|
299
|
+
"name": "Test User",
|
|
300
|
+
"password": "password123"
|
|
301
|
+
}))
|
|
302
|
+
.to_request();
|
|
303
|
+
|
|
304
|
+
let resp = test::call_service(&app, req).await;
|
|
305
|
+
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[actix_web::test]
|
|
309
|
+
async fn test_get_user_not_found() {
|
|
310
|
+
let app = test::init_service(
|
|
311
|
+
App::new()
|
|
312
|
+
.app_data(web::Data::new(create_test_state().await))
|
|
313
|
+
.configure(configure_routes)
|
|
314
|
+
).await;
|
|
315
|
+
|
|
316
|
+
let req = test::TestRequest::get()
|
|
317
|
+
.uri("/api/v1/users/nonexistent")
|
|
318
|
+
.to_request();
|
|
319
|
+
|
|
320
|
+
let resp = test::call_service(&app, req).await;
|
|
321
|
+
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
\`\`\`
|
|
325
|
+
|
|
326
|
+
## ✅ DO
|
|
327
|
+
- Use \`web::Data<T>\` for shared application state
|
|
328
|
+
- Implement \`ResponseError\` for custom error types
|
|
329
|
+
- Use extractors (\`web::Json\`, \`web::Query\`, \`web::Path\`)
|
|
330
|
+
- Configure routes in separate function with \`ServiceConfig\`
|
|
331
|
+
- Use \`#[actix_web::test]\` for async tests
|
|
332
|
+
|
|
333
|
+
## ❌ DON'T
|
|
334
|
+
- Don't clone \`web::Data\` unnecessarily (it's already \`Arc\`)
|
|
335
|
+
- Don't block the async runtime with sync operations
|
|
336
|
+
- Don't expose internal error details to clients
|
|
337
|
+
- Don't forget to handle all error cases in \`ResponseError\`
|