autoworkflow 3.1.4 → 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.
Files changed (123) hide show
  1. package/.claude/commands/analyze.md +19 -0
  2. package/.claude/commands/audit.md +174 -11
  3. package/.claude/commands/build.md +39 -0
  4. package/.claude/commands/commit.md +25 -0
  5. package/.claude/commands/fix.md +23 -0
  6. package/.claude/commands/plan.md +18 -0
  7. package/.claude/commands/suggest.md +23 -0
  8. package/.claude/commands/verify.md +18 -0
  9. package/.claude/hooks/post-bash-router.sh +20 -0
  10. package/.claude/hooks/post-commit.sh +140 -0
  11. package/.claude/hooks/pre-edit.sh +129 -0
  12. package/.claude/hooks/session-check.sh +79 -0
  13. package/.claude/settings.json +40 -6
  14. package/.claude/settings.local.json +3 -1
  15. package/.claude/skills/actix.md +337 -0
  16. package/.claude/skills/alembic.md +504 -0
  17. package/.claude/skills/angular.md +237 -0
  18. package/.claude/skills/api-design.md +187 -0
  19. package/.claude/skills/aspnet-core.md +377 -0
  20. package/.claude/skills/astro.md +245 -0
  21. package/.claude/skills/auth-clerk.md +327 -0
  22. package/.claude/skills/auth-firebase.md +367 -0
  23. package/.claude/skills/auth-nextauth.md +359 -0
  24. package/.claude/skills/auth-supabase.md +368 -0
  25. package/.claude/skills/axum.md +386 -0
  26. package/.claude/skills/blazor.md +456 -0
  27. package/.claude/skills/chi.md +348 -0
  28. package/.claude/skills/code-review.md +133 -0
  29. package/.claude/skills/csharp.md +296 -0
  30. package/.claude/skills/css-modules.md +325 -0
  31. package/.claude/skills/cypress.md +343 -0
  32. package/.claude/skills/debugging.md +133 -0
  33. package/.claude/skills/diesel.md +392 -0
  34. package/.claude/skills/django.md +301 -0
  35. package/.claude/skills/docker.md +319 -0
  36. package/.claude/skills/doctrine.md +473 -0
  37. package/.claude/skills/documentation.md +182 -0
  38. package/.claude/skills/dotnet.md +409 -0
  39. package/.claude/skills/drizzle.md +293 -0
  40. package/.claude/skills/echo.md +321 -0
  41. package/.claude/skills/eloquent.md +256 -0
  42. package/.claude/skills/emotion.md +426 -0
  43. package/.claude/skills/entity-framework.md +370 -0
  44. package/.claude/skills/express.md +316 -0
  45. package/.claude/skills/fastapi.md +329 -0
  46. package/.claude/skills/fastify.md +299 -0
  47. package/.claude/skills/fiber.md +315 -0
  48. package/.claude/skills/flask.md +322 -0
  49. package/.claude/skills/gin.md +342 -0
  50. package/.claude/skills/git.md +116 -0
  51. package/.claude/skills/github-actions.md +353 -0
  52. package/.claude/skills/go.md +377 -0
  53. package/.claude/skills/gorm.md +409 -0
  54. package/.claude/skills/graphql.md +478 -0
  55. package/.claude/skills/hibernate.md +379 -0
  56. package/.claude/skills/hono.md +306 -0
  57. package/.claude/skills/java.md +400 -0
  58. package/.claude/skills/jest.md +313 -0
  59. package/.claude/skills/jpa.md +282 -0
  60. package/.claude/skills/kotlin.md +347 -0
  61. package/.claude/skills/kubernetes.md +363 -0
  62. package/.claude/skills/laravel.md +414 -0
  63. package/.claude/skills/mcp-browser.md +320 -0
  64. package/.claude/skills/mcp-database.md +219 -0
  65. package/.claude/skills/mcp-fetch.md +241 -0
  66. package/.claude/skills/mcp-filesystem.md +204 -0
  67. package/.claude/skills/mcp-github.md +217 -0
  68. package/.claude/skills/mcp-memory.md +240 -0
  69. package/.claude/skills/mcp-search.md +218 -0
  70. package/.claude/skills/mcp-slack.md +262 -0
  71. package/.claude/skills/micronaut.md +388 -0
  72. package/.claude/skills/mongodb.md +319 -0
  73. package/.claude/skills/mongoose.md +355 -0
  74. package/.claude/skills/mysql.md +281 -0
  75. package/.claude/skills/nestjs.md +335 -0
  76. package/.claude/skills/nextjs-app-router.md +260 -0
  77. package/.claude/skills/nextjs-pages.md +172 -0
  78. package/.claude/skills/nuxt.md +202 -0
  79. package/.claude/skills/openapi.md +489 -0
  80. package/.claude/skills/performance.md +199 -0
  81. package/.claude/skills/php.md +398 -0
  82. package/.claude/skills/playwright.md +371 -0
  83. package/.claude/skills/postgresql.md +257 -0
  84. package/.claude/skills/prisma.md +293 -0
  85. package/.claude/skills/pydantic.md +304 -0
  86. package/.claude/skills/pytest.md +313 -0
  87. package/.claude/skills/python.md +272 -0
  88. package/.claude/skills/quarkus.md +377 -0
  89. package/.claude/skills/react.md +230 -0
  90. package/.claude/skills/redis.md +391 -0
  91. package/.claude/skills/refactoring.md +143 -0
  92. package/.claude/skills/remix.md +246 -0
  93. package/.claude/skills/rest-api.md +490 -0
  94. package/.claude/skills/rocket.md +366 -0
  95. package/.claude/skills/rust.md +341 -0
  96. package/.claude/skills/sass.md +380 -0
  97. package/.claude/skills/sea-orm.md +382 -0
  98. package/.claude/skills/security.md +167 -0
  99. package/.claude/skills/sequelize.md +395 -0
  100. package/.claude/skills/spring-boot.md +416 -0
  101. package/.claude/skills/sqlalchemy.md +269 -0
  102. package/.claude/skills/sqlx-rust.md +408 -0
  103. package/.claude/skills/state-jotai.md +346 -0
  104. package/.claude/skills/state-mobx.md +353 -0
  105. package/.claude/skills/state-pinia.md +431 -0
  106. package/.claude/skills/state-redux.md +337 -0
  107. package/.claude/skills/state-tanstack-query.md +434 -0
  108. package/.claude/skills/state-zustand.md +340 -0
  109. package/.claude/skills/styled-components.md +403 -0
  110. package/.claude/skills/svelte.md +238 -0
  111. package/.claude/skills/sveltekit.md +207 -0
  112. package/.claude/skills/symfony.md +437 -0
  113. package/.claude/skills/tailwind.md +279 -0
  114. package/.claude/skills/terraform.md +394 -0
  115. package/.claude/skills/testing-library.md +371 -0
  116. package/.claude/skills/trpc.md +426 -0
  117. package/.claude/skills/typeorm.md +368 -0
  118. package/.claude/skills/vitest.md +330 -0
  119. package/.claude/skills/vue.md +202 -0
  120. package/.claude/skills/warp.md +365 -0
  121. package/README.md +135 -52
  122. package/package.json +1 -1
  123. package/system/triggers.md +152 -11
@@ -0,0 +1,202 @@
1
+ # Vue 3 Skill (Composition API)
2
+
3
+ ## Component Structure
4
+ \`\`\`vue
5
+ <script setup lang="ts">
6
+ import { ref, computed, watch, onMounted } from 'vue';
7
+
8
+ // Props with defaults
9
+ const props = withDefaults(defineProps<{
10
+ userId: string;
11
+ initialCount?: number;
12
+ }>(), {
13
+ initialCount: 0
14
+ });
15
+
16
+ // Emits with validation
17
+ const emit = defineEmits<{
18
+ (e: 'update', value: number): void;
19
+ (e: 'submit', data: FormData): void;
20
+ }>();
21
+
22
+ // Reactive State
23
+ const count = ref(props.initialCount);
24
+ const user = ref<User | null>(null);
25
+
26
+ // Computed
27
+ const doubleCount = computed(() => count.value * 2);
28
+
29
+ // Methods
30
+ function increment() {
31
+ count.value++;
32
+ emit('update', count.value);
33
+ }
34
+
35
+ // Lifecycle
36
+ onMounted(async () => {
37
+ user.value = await fetchUser(props.userId);
38
+ });
39
+ </script>
40
+
41
+ <template>
42
+ <div>
43
+ <h2>{{ user?.name }}</h2>
44
+ <p>Count: {{ count }} (Double: {{ doubleCount }})</p>
45
+ <button @click="increment">Increment</button>
46
+ </div>
47
+ </template>
48
+ \`\`\`
49
+
50
+ ## Composables (Custom Hooks)
51
+ \`\`\`typescript
52
+ // composables/useUser.ts
53
+ import { ref, onMounted } from 'vue';
54
+
55
+ export function useUser(userId: Ref<string>) {
56
+ const user = ref<User | null>(null);
57
+ const loading = ref(true);
58
+ const error = ref<Error | null>(null);
59
+
60
+ async function fetchUser() {
61
+ loading.value = true;
62
+ try {
63
+ user.value = await api.getUser(userId.value);
64
+ } catch (e) {
65
+ error.value = e as Error;
66
+ } finally {
67
+ loading.value = false;
68
+ }
69
+ }
70
+
71
+ // Refetch when userId changes
72
+ watch(userId, fetchUser, { immediate: true });
73
+
74
+ return { user, loading, error, refetch: fetchUser };
75
+ }
76
+
77
+ // Usage in component
78
+ const { user, loading, error } = useUser(toRef(props, 'userId'));
79
+ \`\`\`
80
+
81
+ ## Watch & WatchEffect
82
+ \`\`\`typescript
83
+ // Watch specific ref
84
+ watch(count, (newVal, oldVal) => {
85
+ console.log(\`Count changed from \${oldVal} to \${newVal}\`);
86
+ });
87
+
88
+ // Watch multiple sources
89
+ watch([firstName, lastName], ([first, last]) => {
90
+ fullName.value = \`\${first} \${last}\`;
91
+ });
92
+
93
+ // Deep watch for objects
94
+ watch(user, (newUser) => {
95
+ saveToLocalStorage(newUser);
96
+ }, { deep: true });
97
+
98
+ // WatchEffect - auto-tracks dependencies
99
+ watchEffect(() => {
100
+ // Automatically re-runs when user.value or count.value changes
101
+ console.log(\`User: \${user.value?.name}, Count: \${count.value}\`);
102
+ });
103
+
104
+ // Cleanup
105
+ watchEffect((onCleanup) => {
106
+ const timer = setInterval(() => tick(), 1000);
107
+ onCleanup(() => clearInterval(timer));
108
+ });
109
+ \`\`\`
110
+
111
+ ## Provide/Inject (Dependency Injection)
112
+ \`\`\`typescript
113
+ // Parent component (provide)
114
+ import { provide, ref } from 'vue';
115
+
116
+ const theme = ref('dark');
117
+ const toggleTheme = () => {
118
+ theme.value = theme.value === 'dark' ? 'light' : 'dark';
119
+ };
120
+
121
+ provide('theme', { theme, toggleTheme });
122
+
123
+ // Child component (inject)
124
+ import { inject } from 'vue';
125
+
126
+ const { theme, toggleTheme } = inject('theme')!;
127
+
128
+ // With TypeScript - define injection key
129
+ // types/injection-keys.ts
130
+ import type { InjectionKey, Ref } from 'vue';
131
+
132
+ export const ThemeKey: InjectionKey<{
133
+ theme: Ref<string>;
134
+ toggleTheme: () => void;
135
+ }> = Symbol('theme');
136
+ \`\`\`
137
+
138
+ ## v-model Customization
139
+ \`\`\`vue
140
+ <script setup lang="ts">
141
+ // Single v-model
142
+ const modelValue = defineModel<string>();
143
+
144
+ // Named v-model with modifier
145
+ const [firstName, firstNameModifiers] = defineModel<string>('firstName');
146
+ const [lastName] = defineModel<string>('lastName');
147
+
148
+ // Usage: <MyInput v-model:firstName="first" v-model:lastName="last" />
149
+ </script>
150
+
151
+ <template>
152
+ <input :value="modelValue" @input="modelValue = $event.target.value" />
153
+ </template>
154
+ \`\`\`
155
+
156
+ ## Slots
157
+ \`\`\`vue
158
+ <!-- Parent -->
159
+ <Card>
160
+ <template #header>
161
+ <h1>Title</h1>
162
+ </template>
163
+
164
+ <template #default="{ item }">
165
+ <p>{{ item.name }}</p>
166
+ </template>
167
+
168
+ <template #footer>
169
+ <button>Submit</button>
170
+ </template>
171
+ </Card>
172
+
173
+ <!-- Card.vue -->
174
+ <template>
175
+ <div class="card">
176
+ <header>
177
+ <slot name="header" />
178
+ </header>
179
+ <main>
180
+ <slot :item="currentItem" />
181
+ </main>
182
+ <footer>
183
+ <slot name="footer" />
184
+ </footer>
185
+ </div>
186
+ </template>
187
+ \`\`\`
188
+
189
+ ## ❌ DON'T
190
+ - Mutate props directly
191
+ - Forget to use .value with refs in script
192
+ - Use reactive() for primitives (use ref)
193
+ - Create watchers without cleanup when needed
194
+ - Overuse provide/inject (prefer props for direct parent-child)
195
+
196
+ ## ✅ DO
197
+ - Use \`<script setup>\` for cleaner code
198
+ - Use TypeScript for props/emits
199
+ - Create composables for reusable logic
200
+ - Use computed for derived state
201
+ - Use watchEffect for side effects with auto-tracking
202
+ - Provide injection keys with TypeScript
@@ -0,0 +1,365 @@
1
+ # Warp Framework Skill
2
+
3
+ ## Application Setup
4
+ \`\`\`rust
5
+ use warp::Filter;
6
+ use std::net::SocketAddr;
7
+
8
+ #[tokio::main]
9
+ async fn main() {
10
+ // Initialize tracing
11
+ tracing_subscriber::fmt::init();
12
+
13
+ // Create shared state
14
+ let db_pool = create_pool().await.expect("Failed to create pool");
15
+ let state = AppState { db: db_pool };
16
+
17
+ // Build routes
18
+ let routes = api_routes(state)
19
+ .with(warp::trace::request());
20
+
21
+ let addr: SocketAddr = "127.0.0.1:3000".parse().unwrap();
22
+ tracing::info!("Listening on {}", addr);
23
+
24
+ warp::serve(routes).run(addr).await;
25
+ }
26
+
27
+ fn api_routes(state: AppState) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
28
+ let api = warp::path("api").and(warp::path("v1"));
29
+
30
+ api.and(
31
+ auth_routes(state.clone())
32
+ .or(user_routes(state.clone()))
33
+ .or(post_routes(state))
34
+ )
35
+ .recover(handle_rejection)
36
+ }
37
+ \`\`\`
38
+
39
+ ## Route Definitions with Filters
40
+ \`\`\`rust
41
+ use warp::{Filter, Reply, Rejection};
42
+
43
+ fn user_routes(state: AppState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
44
+ let users = warp::path("users");
45
+
46
+ let list = users
47
+ .and(warp::path::end())
48
+ .and(warp::get())
49
+ .and(with_state(state.clone()))
50
+ .and(warp::query::<PaginationQuery>())
51
+ .and_then(list_users);
52
+
53
+ let create = users
54
+ .and(warp::path::end())
55
+ .and(warp::post())
56
+ .and(with_state(state.clone()))
57
+ .and(warp::body::json())
58
+ .and_then(create_user);
59
+
60
+ let get = users
61
+ .and(warp::path::param::<String>())
62
+ .and(warp::path::end())
63
+ .and(warp::get())
64
+ .and(with_state(state.clone()))
65
+ .and_then(get_user);
66
+
67
+ let update = users
68
+ .and(warp::path::param::<String>())
69
+ .and(warp::path::end())
70
+ .and(warp::put())
71
+ .and(with_state(state.clone()))
72
+ .and(warp::body::json())
73
+ .and_then(update_user);
74
+
75
+ let delete = users
76
+ .and(warp::path::param::<String>())
77
+ .and(warp::path::end())
78
+ .and(warp::delete())
79
+ .and(with_state(state))
80
+ .and_then(delete_user);
81
+
82
+ list.or(create).or(get).or(update).or(delete)
83
+ }
84
+
85
+ // Protected routes with auth
86
+ fn protected_routes(state: AppState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
87
+ warp::path("protected")
88
+ .and(with_auth())
89
+ .and(with_state(state))
90
+ .and_then(protected_handler)
91
+ }
92
+ \`\`\`
93
+
94
+ ## Custom Filters
95
+ \`\`\`rust
96
+ use std::sync::Arc;
97
+
98
+ #[derive(Clone)]
99
+ pub struct AppState {
100
+ pub db: PgPool,
101
+ }
102
+
103
+ // State filter
104
+ fn with_state(state: AppState) -> impl Filter<Extract = (AppState,), Error = std::convert::Infallible> + Clone {
105
+ warp::any().map(move || state.clone())
106
+ }
107
+
108
+ // Auth filter
109
+ fn with_auth() -> impl Filter<Extract = (AuthUser,), Error = Rejection> + Clone {
110
+ warp::header::optional::<String>("Authorization")
111
+ .and_then(|auth_header: Option<String>| async move {
112
+ let token = auth_header
113
+ .as_ref()
114
+ .and_then(|h| h.strip_prefix("Bearer "))
115
+ .ok_or_else(|| warp::reject::custom(AppError::Unauthorized))?;
116
+
117
+ let claims = validate_token(token)
118
+ .map_err(|_| warp::reject::custom(AppError::Unauthorized))?;
119
+
120
+ Ok::<_, Rejection>(AuthUser {
121
+ user_id: claims.sub,
122
+ role: claims.role,
123
+ })
124
+ })
125
+ }
126
+
127
+ // Optional auth filter
128
+ fn with_optional_auth() -> impl Filter<Extract = (Option<AuthUser>,), Error = Rejection> + Clone {
129
+ warp::header::optional::<String>("Authorization")
130
+ .map(|auth_header: Option<String>| {
131
+ auth_header
132
+ .as_ref()
133
+ .and_then(|h| h.strip_prefix("Bearer "))
134
+ .and_then(|token| validate_token(token).ok())
135
+ .map(|claims| AuthUser {
136
+ user_id: claims.sub,
137
+ role: claims.role,
138
+ })
139
+ })
140
+ }
141
+
142
+ // Request body size limit
143
+ fn json_body<T: DeserializeOwned + Send>() -> impl Filter<Extract = (T,), Error = Rejection> + Clone {
144
+ warp::body::content_length_limit(1024 * 16) // 16KB
145
+ .and(warp::body::json())
146
+ }
147
+ \`\`\`
148
+
149
+ ## Handlers
150
+ \`\`\`rust
151
+ use serde::{Deserialize, Serialize};
152
+
153
+ #[derive(Deserialize)]
154
+ pub struct CreateUserRequest {
155
+ pub email: String,
156
+ pub name: String,
157
+ pub password: String,
158
+ }
159
+
160
+ #[derive(Deserialize, Default)]
161
+ pub struct PaginationQuery {
162
+ #[serde(default = "default_page")]
163
+ pub page: u32,
164
+ #[serde(default = "default_per_page")]
165
+ pub per_page: u32,
166
+ }
167
+
168
+ fn default_page() -> u32 { 1 }
169
+ fn default_per_page() -> u32 { 20 }
170
+
171
+ async fn list_users(
172
+ state: AppState,
173
+ query: PaginationQuery,
174
+ ) -> Result<impl Reply, Rejection> {
175
+ let offset = (query.page - 1) * query.per_page;
176
+
177
+ let users = sqlx::query_as!(User,
178
+ "SELECT * FROM users LIMIT $1 OFFSET $2",
179
+ query.per_page as i64,
180
+ offset as i64
181
+ )
182
+ .fetch_all(&state.db)
183
+ .await
184
+ .map_err(|e| warp::reject::custom(AppError::Database(e)))?;
185
+
186
+ Ok(warp::reply::json(&users))
187
+ }
188
+
189
+ async fn get_user(id: String, state: AppState) -> Result<impl Reply, Rejection> {
190
+ let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
191
+ .fetch_optional(&state.db)
192
+ .await
193
+ .map_err(|e| warp::reject::custom(AppError::Database(e)))?
194
+ .ok_or_else(|| warp::reject::custom(AppError::NotFound))?;
195
+
196
+ Ok(warp::reply::json(&user))
197
+ }
198
+
199
+ async fn create_user(
200
+ state: AppState,
201
+ body: CreateUserRequest,
202
+ ) -> Result<impl Reply, Rejection> {
203
+ // Validate
204
+ if body.email.is_empty() {
205
+ return Err(warp::reject::custom(AppError::Validation("Email required".into())));
206
+ }
207
+
208
+ let user = User::new(&body.email, &body.name, &body.password)
209
+ .map_err(|e| warp::reject::custom(e))?;
210
+
211
+ // Save user...
212
+
213
+ Ok(warp::reply::with_status(
214
+ warp::reply::json(&user),
215
+ warp::http::StatusCode::CREATED,
216
+ ))
217
+ }
218
+ \`\`\`
219
+
220
+ ## Error Handling with Rejections
221
+ \`\`\`rust
222
+ use warp::{reject::Reject, Rejection, Reply};
223
+ use thiserror::Error;
224
+
225
+ #[derive(Error, Debug)]
226
+ pub enum AppError {
227
+ #[error("Not found")]
228
+ NotFound,
229
+
230
+ #[error("Unauthorized")]
231
+ Unauthorized,
232
+
233
+ #[error("Validation error: {0}")]
234
+ Validation(String),
235
+
236
+ #[error("Database error")]
237
+ Database(#[from] sqlx::Error),
238
+ }
239
+
240
+ impl Reject for AppError {}
241
+
242
+ #[derive(Serialize)]
243
+ struct ErrorResponse {
244
+ error: String,
245
+ }
246
+
247
+ async fn handle_rejection(err: Rejection) -> Result<impl Reply, std::convert::Infallible> {
248
+ let (code, message) = if err.is_not_found() {
249
+ (warp::http::StatusCode::NOT_FOUND, "Not found".to_string())
250
+ } else if let Some(e) = err.find::<AppError>() {
251
+ match e {
252
+ AppError::NotFound => (warp::http::StatusCode::NOT_FOUND, e.to_string()),
253
+ AppError::Unauthorized => (warp::http::StatusCode::UNAUTHORIZED, e.to_string()),
254
+ AppError::Validation(_) => (warp::http::StatusCode::BAD_REQUEST, e.to_string()),
255
+ AppError::Database(_) => {
256
+ tracing::error!("Database error: {:?}", e);
257
+ (warp::http::StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string())
258
+ }
259
+ }
260
+ } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
261
+ (warp::http::StatusCode::METHOD_NOT_ALLOWED, "Method not allowed".to_string())
262
+ } else if err.find::<warp::reject::InvalidQuery>().is_some() {
263
+ (warp::http::StatusCode::BAD_REQUEST, "Invalid query".to_string())
264
+ } else if err.find::<warp::reject::PayloadTooLarge>().is_some() {
265
+ (warp::http::StatusCode::PAYLOAD_TOO_LARGE, "Payload too large".to_string())
266
+ } else {
267
+ tracing::error!("Unhandled rejection: {:?}", err);
268
+ (warp::http::StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string())
269
+ };
270
+
271
+ let json = warp::reply::json(&ErrorResponse { error: message });
272
+ Ok(warp::reply::with_status(json, code))
273
+ }
274
+ \`\`\`
275
+
276
+ ## CORS
277
+ \`\`\`rust
278
+ use warp::cors;
279
+
280
+ fn with_cors() -> warp::cors::Builder {
281
+ warp::cors()
282
+ .allow_any_origin()
283
+ .allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
284
+ .allow_headers(vec!["Authorization", "Content-Type"])
285
+ .max_age(3600)
286
+ }
287
+
288
+ // Apply to routes
289
+ let routes = api_routes(state)
290
+ .with(with_cors())
291
+ .with(warp::trace::request());
292
+ \`\`\`
293
+
294
+ ## Testing
295
+ \`\`\`rust
296
+ #[cfg(test)]
297
+ mod tests {
298
+ use super::*;
299
+ use warp::http::StatusCode;
300
+ use warp::test::request;
301
+
302
+ async fn create_test_state() -> AppState {
303
+ AppState {
304
+ db: create_test_pool().await,
305
+ }
306
+ }
307
+
308
+ #[tokio::test]
309
+ async fn test_get_user() {
310
+ let state = create_test_state().await;
311
+ let filter = user_routes(state);
312
+
313
+ let resp = request()
314
+ .method("GET")
315
+ .path("/users/123")
316
+ .reply(&filter)
317
+ .await;
318
+
319
+ assert_eq!(resp.status(), StatusCode::OK);
320
+ }
321
+
322
+ #[tokio::test]
323
+ async fn test_create_user() {
324
+ let state = create_test_state().await;
325
+ let filter = user_routes(state);
326
+
327
+ let resp = request()
328
+ .method("POST")
329
+ .path("/users")
330
+ .header("Content-Type", "application/json")
331
+ .body(r#"{"email":"test@example.com","name":"Test","password":"password123"}"#)
332
+ .reply(&filter)
333
+ .await;
334
+
335
+ assert_eq!(resp.status(), StatusCode::CREATED);
336
+ }
337
+
338
+ #[tokio::test]
339
+ async fn test_protected_without_auth() {
340
+ let state = create_test_state().await;
341
+ let filter = protected_routes(state).recover(handle_rejection);
342
+
343
+ let resp = request()
344
+ .method("GET")
345
+ .path("/protected")
346
+ .reply(&filter)
347
+ .await;
348
+
349
+ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
350
+ }
351
+ }
352
+ \`\`\`
353
+
354
+ ## ✅ DO
355
+ - Use filter combinators (\`.and()\`, \`.or()\`, \`.map()\`, \`.and_then()\`)
356
+ - Implement \`Reject\` for custom error types
357
+ - Use \`.recover()\` for centralized error handling
358
+ - Use \`warp::trace\` for request logging
359
+ - Clone state with \`Clone\` trait when composing routes
360
+
361
+ ## ❌ DON'T
362
+ - Don't forget to handle all rejection types in recovery
363
+ - Don't use \`.unwrap()\` in handlers
364
+ - Don't create filters that are not \`Clone\` + \`Send\` + \`Sync\`
365
+ - Don't expose internal errors to clients