diff --git a/docs/.agent/docs_coverage.md b/docs/.agent/docs_coverage.md index 424ed27..715cd30 100644 --- a/docs/.agent/docs_coverage.md +++ b/docs/.agent/docs_coverage.md @@ -1,32 +1,22 @@ # Docs Coverage Map -| Feature | Documentation | Code Location | Status | -|---------|---------------|---------------|--------| -| **Core** | | | | -| Routing | `concepts/handlers.md` | `rustapi-macros` | OK | -| Extractors | `concepts/handlers.md` | `rustapi-core/src/extract.rs` | OK | -| State | `concepts/handlers.md` | `rustapi-core/src/extract.rs` | OK | -| Validation | `crates/rustapi_validate.md` | `rustapi-validate` | OK | -| **HATEOAS** | | | | -| Pagination | `recipes/pagination.md` | `rustapi-core/src/hateoas.rs` | OK | -| Links | `recipes/pagination.md` | `rustapi-core/src/hateoas.rs` | OK | -| **Extras** | | | | -| Auth (JWT) | `recipes/jwt_auth.md` | `rustapi-extras/src/jwt` | OK | -| Auth (OAuth2) | `recipes/oauth2_client.md` | `rustapi-extras/src/oauth2` | OK | -| Security | `recipes/csrf_protection.md` | `rustapi-extras/src/security` | OK | -| Observability | `crates/rustapi_extras.md` | `rustapi-extras/src/telemetry` | OK | -| Audit Logging | `recipes/audit_logging.md` | `rustapi-extras/src/audit` | OK | -| Middleware (Advanced) | `recipes/advanced_middleware.md` | `rustapi-extras/src/{rate_limit, dedup, cache}` | OK | -| **Jobs** | | | | -| Job Queue (Crate) | `crates/rustapi_jobs.md` | `rustapi-jobs` | OK | -| Background Jobs (Recipe) | `recipes/background_jobs.md` | `rustapi-jobs` | OK | -| **Integrations** | | | | -| gRPC | `recipes/grpc_integration.md` | `rustapi-grpc` | OK | -| SSR | `recipes/server_side_rendering.md` | `rustapi-view` | OK | -| AI / TOON | `recipes/ai_integration.md` | `rustapi-toon` | OK | -| **Learning** | | | | -| Structured Path | `learning/curriculum.md` | N/A | OK | -| **Recipes** | | | | -| File Uploads | `recipes/file_uploads.md` | `rustapi-core` | OK | -| Deployment | `recipes/deployment.md` | `cargo-rustapi` | OK | -| Testing | `recipes/testing.md` | `rustapi-testing` | OK | +| Feature Area | Documentation Page | Source Code (Key Crates) | Status | Notes | +|--------------|-------------------|--------------------------|--------|-------| +| **Core Routing** | `concepts/handlers.md` | `rustapi-core` | OK | | +| **Extractors** | `concepts/handlers.md` | `rustapi-core` | OK | `Body`, `Json`, `Path`, `Query`, `State` | +| **Validation** | `crates/rustapi_validate.md` | `rustapi-validate` | OK | `#[derive(Validate)]`, `ValidatedJson` | +| **OpenAPI** | `crates/rustapi_openapi.md` | `rustapi-openapi` | OK | `#[derive(Schema)]` | +| **WebSocket** | `recipes/websockets.md` | `rustapi-ws` | OK | Upgrade handling, broadcasting | +| **Database** | `recipes/db_integration.md` | `rustapi-core` | OK | Added pooling, transactions, testing | +| **File Uploads** | `recipes/file_uploads.md` | `rustapi-core` | OK | Fixed code example | +| **Compression** | `recipes/compression.md` | `rustapi-core` | OK | Recipe created | +| **Authentication** | `recipes/jwt_auth.md`, `recipes/oauth2_client.md` | `rustapi-extras` | OK | JWT, OAuth2 | +| **Observability** | `crates/rustapi_extras.md`, `recipes/audit_logging.md` | `rustapi-extras` | OK | Tracing, Metrics, Audit | +| **Resilience** | `recipes/resilience.md` | `rustapi-extras` | OK | Circuit Breaker, Retry, Timeout | +| **Background Jobs** | `recipes/background_jobs.md` | `rustapi-jobs` | OK | Job queue, workers | +| **Testing** | `concepts/testing.md` | `rustapi-testing` | OK | `TestClient`, Mocking | +| **SSR** | `recipes/server_side_rendering.md` | `rustapi-view` | OK | Tera integration | +| **gRPC** | `recipes/grpc_integration.md` | `rustapi-grpc` | OK | Tonic integration | +| **AI / TOON** | `recipes/ai_integration.md` | `rustapi-toon` | OK | TOON format | +| **HTTP/3** | `recipes/http3_quic.md` | `rustapi-core` | OK | QUIC support | +| **OpenAPI Refs** | `recipes/openapi_refs.md` | `rustapi-openapi` | OK | Modular schemas, Refs created | diff --git a/docs/.agent/docs_inventory.md b/docs/.agent/docs_inventory.md index 8340a53..ef0785c 100644 --- a/docs/.agent/docs_inventory.md +++ b/docs/.agent/docs_inventory.md @@ -1,15 +1,17 @@ # Documentation Inventory -| File | Purpose | Owner Crate | Status | -|------|---------|-------------|--------| -| `README.md` | Project overview, key features, quick start | Root | OK | -| `docs/README.md` | Documentation landing page | Docs | OK | -| `docs/cookbook/src/SUMMARY.md` | Cookbook navigation structure | Docs | OK | -| `docs/cookbook/src/getting_started/installation.md` | Installation instructions | Docs | OK | -| `docs/cookbook/src/learning/README.md` | Learning path entry point | Docs | OK | -| `docs/cookbook/src/recipes/*.md` | Specific implementation guides | Docs | OK | -| `docs/cookbook/src/recipes/advanced_middleware.md` | Recipe for Rate Limit, Dedup, Cache | Docs | OK | -| `docs/cookbook/src/recipes/audit_logging.md` | Recipe for Audit Logging | Docs | OK | -| `docs/cookbook/src/recipes/oauth2_client.md` | Recipe for OAuth2 Client | Docs | OK | -| `crates/rustapi-core/src/hateoas.rs` | API Reference for HATEOAS | rustapi-core | OK | -| `crates/rustapi-core/src/extract.rs` | API Reference for Extractors | rustapi-core | OK | +| File Path | Purpose | Last Updated | Owner Crate | Gaps | +|-----------|---------|--------------|-------------|------| +| `docs/README.md` | Index / Entry Point | v0.1.335 | Workspace | None | +| `docs/GETTING_STARTED.md` | Quick start guide | v0.1.335 | Workspace | None | +| `docs/ARCHITECTURE.md` | High-level architecture | v0.1.335 | Workspace | None | +| `docs/FEATURES.md` | Feature list | v0.1.335 | Workspace | None | +| `docs/PHILOSOPHY.md` | Design philosophy | v0.1.335 | Workspace | None | +| `docs/native_openapi.md` | OpenAPI implementation details | v0.1.335 | rustapi-openapi | None | +| `docs/cookbook/src/introduction.md` | Cookbook Intro | v0.1.335 | Workspace | None | +| `docs/cookbook/src/troubleshooting.md` | Troubleshooting | v0.1.335 | Workspace | None | +| `docs/cookbook/src/learning/curriculum.md` | Learning Path | v0.1.335 | Workspace | Updated with Modules 4.5, 5.5, 6.5, 14 | +| `docs/cookbook/src/recipes/db_integration.md` | DB Recipe | v0.1.335 | rustapi-core | Expanded (pooling, testing) | +| `docs/cookbook/src/recipes/file_uploads.md` | Upload Recipe | v0.1.335 | rustapi-core | Code example fixed | +| `docs/cookbook/src/recipes/compression.md` | Compression Recipe | v0.1.335 | rustapi-core | Created | +| `docs/cookbook/src/recipes/openapi_refs.md` | OpenAPI Refs Recipe | v0.1.335 | rustapi-openapi | Created | diff --git a/docs/.agent/last_run.json b/docs/.agent/last_run.json index 5ca8a9a..03e3efe 100644 --- a/docs/.agent/last_run.json +++ b/docs/.agent/last_run.json @@ -1,5 +1,5 @@ { "last_processed_ref": "v0.1.335", - "date": "2026-02-16", - "notes": "Added recipes for Advanced Middleware, Audit Logging, and OAuth2 Client. Updated Learning Path to include these features." + "date": "2026-02-17", + "notes": "Added recipes for Compression and OpenAPI Refs. Expanded DB Integration and File Uploads. Updated Learning Path with new modules." } diff --git a/docs/.agent/run_report_2026-02-17.md b/docs/.agent/run_report_2026-02-17.md new file mode 100644 index 0000000..c747fd2 --- /dev/null +++ b/docs/.agent/run_report_2026-02-17.md @@ -0,0 +1,45 @@ +# Docs Maintenance Run Report: 2026-02-17 + +**Agent:** Documentation Maintainer +**Target Version:** v0.1.335 +**Previous Run:** 2026-02-16 + +## 1. Version Detection +- **Detected Version:** `v0.1.335` (No change since last run) +- **Scope:** Continuous Improvement (Cookbook & Learning Path) + +## 2. Documentation Updates + +### New Recipes +- **[Response Compression](../cookbook/src/recipes/compression.md):** Added guide for `CompressionLayer` configuration and usage. +- **[Modular OpenAPI Schemas](../cookbook/src/recipes/openapi_refs.md):** Added guide for `#[derive(Schema)]` reference generation and manual registration. + +### Recipe Improvements +- **[File Uploads](../cookbook/src/recipes/file_uploads.md):** + - **Fix:** Corrected code example to use `.body_limit()` instead of incorrect layer usage. + - **Improvement:** Clarified default body limits. +- **[Database Integration](../cookbook/src/recipes/db_integration.md):** + - **Expansion:** Added sections on Connection Pooling configuration. + - **Expansion:** Added Transaction handling example. + - **Expansion:** Added Repository Pattern and testing strategy. + +### Learning Path (`curriculum.md`) +- **Added Module 4.5:** Database Integration (Connection pooling, State sharing). +- **Added Module 5.5:** Error Handling (Custom error types, mapping). +- **Added Module 6.5:** File Uploads & Multipart (Streaming, Security). +- **Updated Module 14:** Added Compression task. +- **Updated Module 6:** Added modular schema references task. + +### Inventory & Coverage +- **Created:** `docs/.agent/docs_inventory.md` - Complete inventory of documentation files. +- **Created:** `docs/.agent/docs_coverage.md` - Mapping of features to documentation status. + +## 3. Coverage Status +- **Missing Documentation:** None identified. +- **Needs Update:** None identified. +- **Coverage Map:** Updated to "OK" for all newly added features. + +## 4. Next Steps +- Expand "Error Handling" recipe (referenced in Module 5.5 but not yet created as standalone recipe, covered in concepts). +- Add more examples for "Advanced Middleware" (e.g., custom rate limiting). +- Verify "Module 15: Server-Side Rendering" against latest `rustapi-view` changes if any. diff --git a/docs/cookbook/src/SUMMARY.md b/docs/cookbook/src/SUMMARY.md index bab3611..cd51cdc 100644 --- a/docs/cookbook/src/SUMMARY.md +++ b/docs/cookbook/src/SUMMARY.md @@ -30,6 +30,7 @@ - [Part IV: Recipes](recipes/README.md) - [Creating Resources](recipes/crud_resource.md) - [Pagination & HATEOAS](recipes/pagination.md) + - [Modular OpenAPI Schemas](recipes/openapi_refs.md) - [JWT Authentication](recipes/jwt_auth.md) - [OAuth2 Client](recipes/oauth2_client.md) - [CSRF Protection](recipes/csrf_protection.md) @@ -43,6 +44,7 @@ - [Server-Side Rendering (SSR)](recipes/server_side_rendering.md) - [AI Integration (TOON)](recipes/ai_integration.md) - [Production Tuning](recipes/high_performance.md) + - [Response Compression](recipes/compression.md) - [Resilience Patterns](recipes/resilience.md) - [Audit Logging](recipes/audit_logging.md) - [Time-Travel Debugging (Replay)](recipes/replay.md) diff --git a/docs/cookbook/src/learning/curriculum.md b/docs/cookbook/src/learning/curriculum.md index 530ac14..5139c4e 100644 --- a/docs/cookbook/src/learning/curriculum.md +++ b/docs/cookbook/src/learning/curriculum.md @@ -69,6 +69,18 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u 2. Which extractor retrieves the application state? 3. Why should you use `Arc` for shared state? +### Module 4.5: Database Integration +- **Prerequisites:** Module 4. +- **Reading:** [Database Integration](../recipes/db_integration.md). +- **Task:** Replace the in-memory `Vec` with a real database (Postgres or SQLite) using `sqlx`. +- **Expected Output:** Data persists across server restarts. +- **Pitfalls:** Creating a new connection pool for every request instead of sharing one via State. + +#### 🧠 Knowledge Check +1. Why is connection pooling important? +2. How do you share the DB pool across handlers? +3. What is the benefit of compile-time query checking with sqlx? + ### Module 5: Validation - **Prerequisites:** Module 4. - **Reading:** [Validation](../crates/rustapi_validation.md). @@ -81,18 +93,42 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u 2. What HTTP status code is returned on validation failure? 3. How do you combine JSON extraction and validation? +### Module 5.5: Error Handling +- **Prerequisites:** Module 5. +- **Reading:** [Error Handling](../concepts/handlers.md). +- **Task:** Define a custom `AppError` enum that implements `IntoResponse`. Convert DB and Validation errors to this type. +- **Expected Output:** Consistent error responses (e.g. `{"error": "user_not_found"}`) instead of 500s or plain text. +- **Pitfalls:** Exposing internal implementation details (stack traces) to the client. + +#### 🧠 Knowledge Check +1. Which trait allows a type to be returned from a handler? +2. Why should you map external errors (like DB errors) to your own error type? +3. How can you log the internal error while returning a generic message to the user? + ### Module 6: OpenAPI & HATEOAS - **Prerequisites:** Module 5. -- **Reading:** [OpenAPI](../crates/rustapi_openapi.md), [Pagination Recipe](../recipes/pagination.md). -- **Task:** Add `#[derive(Schema)]` to all DTOs. Implement pagination for `GET /users`. +- **Reading:** [OpenAPI](../crates/rustapi_openapi.md), [OpenAPI Refs](../recipes/openapi_refs.md), [Pagination Recipe](../recipes/pagination.md). +- **Task:** Add `#[derive(Schema)]` to all DTOs. Refactor large schemas to use `#[schema(reference = "...")]` for reusability. Implement pagination for `GET /users`. - **Expected Output:** Swagger UI at `/docs` showing full schema. Paginated responses with `_links`. - **Pitfalls:** Using types that don't implement `Schema` (like raw `serde_json::Value`) inside response structs. #### 🧠 Knowledge Check 1. What does `#[derive(Schema)]` do? -2. Where is the Swagger UI served by default? +2. How do you reuse a schema definition across multiple endpoints? 3. What is HATEOAS and why is it useful? +### Module 6.5: File Uploads & Multipart +- **Prerequisites:** Module 2. +- **Reading:** [File Uploads](../recipes/file_uploads.md). +- **Task:** Create an endpoint that accepts a profile picture upload and saves it to disk. +- **Expected Output:** `POST /upload` saves the file and returns the filename. +- **Pitfalls:** Loading large files entirely into memory instead of streaming. + +#### 🧠 Knowledge Check +1. Which extractor is used for file uploads? +2. Why should you stream uploads instead of buffering them? +3. How do you prevent path traversal attacks with filenames? + ### 🏆 Phase 2 Capstone: "The Secure Blog Engine" **Objective:** Enhance the Todo API into a Blog Engine. **Requirements:** @@ -224,18 +260,19 @@ This curriculum is designed to take you from a RustAPI beginner to an advanced u ### Module 14: High Performance - **Prerequisites:** Phase 3. -- **Reading:** [HTTP/3 (QUIC)](../recipes/http3_quic.md), [Performance Tuning](../recipes/high_performance.md). +- **Reading:** [HTTP/3 (QUIC)](../recipes/http3_quic.md), [Performance Tuning](../recipes/high_performance.md), [Compression](../recipes/compression.md). - **Task:** 1. Enable `http3` feature and generate self-signed certs. 2. Serve traffic over QUIC. - 3. Implement response caching for a heavy computation endpoint. -- **Expected Output:** Browser/Client connects via HTTP/3. Repeated requests are served instantly from cache. -- **Pitfalls:** Caching private user data without proper keys. + 3. Enable `CompressionLayer` for response compression. + 4. Implement response caching for a heavy computation endpoint. +- **Expected Output:** Browser/Client connects via HTTP/3. Responses are gzipped. Repeated requests are served instantly from cache. +- **Pitfalls:** Compressing already compressed data (like images). #### 🧠 Knowledge Check 1. What transport protocol does HTTP/3 use? -2. How does `simd-json` improve performance? -3. When should you *not* use caching? +2. How do you enable GZIP compression for API responses? +3. How does `simd-json` improve performance? ### 🏆 Phase 4 Capstone: "The High-Scale Event Platform" **Objective:** Architect a system capable of handling thousands of events per second. diff --git a/docs/cookbook/src/recipes/compression.md b/docs/cookbook/src/recipes/compression.md new file mode 100644 index 0000000..8ecd252 --- /dev/null +++ b/docs/cookbook/src/recipes/compression.md @@ -0,0 +1,85 @@ +# Response Compression + +Response compression reduces payload size, improving load times for clients on slow networks. RustAPI provides a `CompressionLayer` that supports Gzip, Deflate, and optional Brotli compression. + +## Dependencies + +Enable the `compression` feature in `Cargo.toml`. For Brotli support, enable `compression-brotli`. + +```toml +[dependencies] +rustapi-rs = { version = "0.1", features = ["compression"] } +# For Brotli support: +# rustapi-rs = { version = "0.1", features = ["compression-brotli"] } +``` + +## Basic Usage + +The easiest way to enable compression is via the `RustApi` builder. + +```rust +use rustapi_rs::prelude::*; + +#[tokio::main] +async fn main() { + RustApi::new() + // Enable default compression (Gzip/Deflate, Level 6, Min size 1KB) + .compression() + .route("/users", get(list_users)) + .run("127.0.0.1:8080") + .await + .unwrap(); +} + +async fn list_users() -> Json> { + // Large response will be compressed automatically + Json(vec!["user1".to_string(); 1000]) +} +``` + +## Custom Configuration + +You can fine-tune the compression settings using `CompressionConfig`. + +```rust +use rustapi_rs::prelude::*; +use rustapi_core::middleware::CompressionConfig; + +#[tokio::main] +async fn main() { + let config = CompressionConfig::new() + .level(9) // Maximum compression + .min_size(512) // Compress responses > 512 bytes + .gzip(true) // Enable Gzip + .deflate(false) // Disable Deflate + .add_content_type("application/vnd.api+json"); // Add custom type + + RustApi::new() + .compression_with_config(config) + .route("/", get(handler)) + .run("127.0.0.1:8080") + .await + .unwrap(); +} +``` + +## How It Works + +The middleware: +1. Checks the `Accept-Encoding` header sent by the client. +2. Selects the best supported algorithm (Brotli > Gzip > Deflate). +3. Checks if the response `Content-Type` is compressible (e.g., JSON, HTML, XML). +4. Checks if the response body size exceeds `min_size`. +5. Compresses the body and sets `Content-Encoding` header. +6. Removes `Content-Length` header as it changes. + +## Supported Algorithms + +- **Gzip**: Standard, widely supported. Good balance of speed and ratio. +- **Deflate**: Slightly faster, less common. +- **Brotli**: (Optional) Better compression ratio than Gzip, but slower to compress. Requires `compression-brotli` feature. + +## Pitfalls + +- **Double Compression**: Do not compress already compressed data like images (JPEG, PNG) or archives (ZIP). The middleware excludes common image types by default, but be careful with custom binary formats. +- **BREACH Attack**: Compressing encrypted secrets (like CSRF tokens) in the same response as user-controlled data can lead to security vulnerabilities. Avoid compressing responses containing secrets. diff --git a/docs/cookbook/src/recipes/db_integration.md b/docs/cookbook/src/recipes/db_integration.md index 804829f..1159913 100644 --- a/docs/cookbook/src/recipes/db_integration.md +++ b/docs/cookbook/src/recipes/db_integration.md @@ -12,6 +12,8 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } dotenvy = "0.15" +# Make sure async-trait is enabled if using traits for repositories +async-trait = "0.1" ``` ## 1. Setup Connection Pool @@ -21,7 +23,9 @@ Create the pool once at startup and share it via `State`. ```rust use sqlx::postgres::PgPoolOptions; use std::sync::Arc; +use rustapi_rs::prelude::*; +#[derive(Clone)] pub struct AppState { pub db: sqlx::PgPool, } @@ -31,9 +35,11 @@ async fn main() { dotenvy::dotenv().ok(); let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - // Create a connection pool + // Create a connection pool with proper configuration let pool = PgPoolOptions::new() - .max_connections(5) + .max_connections(50) // Adjust based on your DB capabilities + .min_connections(5) + .acquire_timeout(std::time::Duration::from_secs(3)) .connect(&db_url) .await .expect("Failed to connect to DB"); @@ -44,13 +50,14 @@ async fn main() { .await .expect("Failed to migrate"); - let state = Arc::new(AppState { db: pool }); + let state = AppState { db: pool }; - let app = RustApi::new() + RustApi::new() + .state(state) // Inject state .route("/users", post(create_user)) - .with_state(state); - - RustApi::serve("0.0.0.0:3000", app).await.unwrap(); + .run("0.0.0.0:3000") + .await + .unwrap(); } ``` @@ -61,13 +68,13 @@ Extract the `State` to get access to the pool. ```rust use rustapi_rs::prelude::*; -#[derive(Deserialize)] +#[derive(Deserialize, Schema)] struct CreateUser { username: String, email: String, } -#[derive(Serialize)] +#[derive(Serialize, Schema)] struct User { id: i32, username: String, @@ -75,9 +82,9 @@ struct User { } async fn create_user( - State(state): State>, + State(state): State, Json(payload): Json, -) -> Result<(StatusCode, Json), ApiError> { +) -> Result, ApiError> { // SQLx query macro performs compile-time checking! let record = sqlx::query_as!( @@ -88,15 +95,69 @@ async fn create_user( ) .fetch_one(&state.db) .await - .map_err(|e| ApiError::InternalServerError(e.to_string()))?; + .map_err(map_sql_error)?; + + Ok(Json(record)) +} + +fn map_sql_error(err: sqlx::Error) -> ApiError { + match err { + sqlx::Error::RowNotFound => ApiError::not_found("Resource not found"), + sqlx::Error::Database(db_err) => { + // Check for unique constraint violation (Postgres error 23505) + if db_err.code().as_deref() == Some("23505") { + return ApiError::conflict("Resource already exists"); + } + ApiError::internal(db_err.message()) + } + _ => { + tracing::error!("Database error: {:?}", err); + ApiError::internal("Internal Server Error") + } + } +} +``` + +## 3. Transactions + +For operations that modify multiple tables, use transactions to ensure data integrity. + +```rust +async fn transfer_funds( + State(state): State, + Json(payload): Json, +) -> Result, ApiError> { - Ok((StatusCode::CREATED, Json(record))) + // Start a transaction + let mut tx = state.db.begin().await.map_err(map_sql_error)?; + + // Deduct from sender + let sender = sqlx::query!("UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance", payload.amount, payload.from_id) + .fetch_one(&mut *tx) + .await + .map_err(map_sql_error)?; + + if sender.balance < 0 { + // Rollback implicitly by returning error (tx dropped) + return Err(ApiError::bad_request("Insufficient funds")); + } + + // Add to receiver + sqlx::query!("UPDATE accounts SET balance = balance + $1 WHERE id = $2", payload.amount, payload.to_id) + .execute(&mut *tx) + .await + .map_err(map_sql_error)?; + + // Commit transaction + tx.commit().await.map_err(map_sql_error)?; + + Ok(Json(TransferResult { success: true })) } ``` -## 3. Dependency Injection for Testing +## 4. Repository Pattern (Advanced) -To make testing easier, define a trait for your database operations. This allows you to swap the real DB for a mock. +To isolate DB logic and make testing easier, define a trait. ```rust #[async_trait] @@ -110,36 +171,34 @@ pub struct PostgresRepo(sqlx::PgPool); #[async_trait] impl UserRepository for PostgresRepo { async fn create(&self, username: &str, email: &str) -> anyhow::Result { - // ... impl ... + let user = sqlx::query_as!( + User, + "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id, username, email", + username, + email + ) + .fetch_one(&self.0) + .await?; + Ok(user) } } -``` - -Then update your state to hold the trait object: -```rust +// In your AppState +#[derive(Clone)] struct AppState { - // Dyn dispatch allows swapping impls at runtime - db: Arc, + users: Arc, } ``` -## Error Handling - -Don't expose raw SQL errors to users. Map them to your `ApiError` type. +This allows you to implement a `MockUserRepository` for unit tests without spinning up a real database. ```rust -impl From for ApiError { - fn from(err: sqlx::Error) -> Self { - match err { - sqlx::Error::RowNotFound => ApiError::NotFound("Resource not found".into()), - _ => { - // Log the real error internally - tracing::error!("Database error: {:?}", err); - // Return generic error to user - ApiError::InternalServerError - } - } +struct MockUserRepository; + +#[async_trait] +impl UserRepository for MockUserRepository { + async fn create(&self, _u: &str, _e: &str) -> anyhow::Result { + Ok(User { id: 1, username: "mock".into(), email: "mock@test.com".into() }) } } ``` diff --git a/docs/cookbook/src/recipes/file_uploads.md b/docs/cookbook/src/recipes/file_uploads.md index 0cad0ef..dc753aa 100644 --- a/docs/cookbook/src/recipes/file_uploads.md +++ b/docs/cookbook/src/recipes/file_uploads.md @@ -19,7 +19,7 @@ Here is a complete, runnable example of a file upload server that streams files ```rust use rustapi_rs::prelude::*; -use rustapi_rs::extract::{Multipart, DefaultBodyLimit}; +use rustapi_rs::extract::Multipart; use tokio::fs::File; use tokio::io::AsyncWriteExt; use std::path::Path; @@ -32,9 +32,9 @@ async fn main() -> Result<(), Box> { println!("Starting Upload Server at http://127.0.0.1:8080"); RustApi::new() + // Increase body limit to 1GB (default is 1MB) + .body_limit(1024 * 1024 * 1024) .route("/upload", post(upload_handler)) - // Increase body limit to 1GB (default is usually 2MB) - .layer(DefaultBodyLimit::max(1024 * 1024 * 1024)) .run("127.0.0.1:8080") .await } @@ -100,7 +100,7 @@ By default, some frameworks load the entire file into RAM. RustAPI's `Multipart` - **Streaming**: `field.chunk().await` (Load small chunks - scalable) ### 2. Body Limits -The default request body limit is often small (e.g., 2MB) to prevent DoS attacks. You must explicitly increase this limit for file upload routes using `DefaultBodyLimit::max(size)`. +The default request body limit is often small (e.g., 1MB) to prevent DoS attacks. You must explicitly increase this limit for file upload routes using `.body_limit(size)`. ### 3. Security - **Path Traversal**: Malicious users can send filenames like `../../system32/cmd.exe`. Always rename files or sanitize filenames strictly. diff --git a/docs/cookbook/src/recipes/openapi_refs.md b/docs/cookbook/src/recipes/openapi_refs.md new file mode 100644 index 0000000..d5df417 --- /dev/null +++ b/docs/cookbook/src/recipes/openapi_refs.md @@ -0,0 +1,111 @@ +# Modular OpenAPI Schemas + +RustAPI's OpenAPI generator is designed to produce modular, reusable schemas using JSON Schema 2020-12 references (`$ref`). This keeps your API specification clean and reduces duplication. + +## Automatic Reference Generation + +When you use `#[derive(Schema)]` on a struct or enum, RustAPI automatically: +1. Generates a unique name for the schema (e.g., `User`, `CreateUserRequest`). +2. Registers the full schema definition in `components/schemas`. +3. Uses a reference (`$ref: "#/components/schemas/User"`) wherever that type is used. + +### Example + +```rust +use rustapi_rs::prelude::*; + +#[derive(Serialize, Schema)] +struct Address { + street: String, + city: String, +} + +#[derive(Serialize, Schema)] +struct User { + id: i64, + username: String, + // This will be a reference to the Address schema + address: Address, +} + +#[derive(Serialize, Schema)] +struct Company { + name: String, + // Reusing the same Address schema + hq_address: Address, +} +``` + +The generated OpenAPI JSON will look like this (simplified): + +```json +{ + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "street": { "type": "string" }, + "city": { "type": "string" } + }, + "required": ["street", "city"] + }, + "User": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64" }, + "username": { "type": "string" }, + "address": { "$ref": "#/components/schemas/Address" } + } + }, + "Company": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "hq_address": { "$ref": "#/components/schemas/Address" } + } + } + } + } +} +``` + +## Generic Types + +RustAPI handles generic types by generating unique names based on the type parameters. + +```rust +#[derive(Serialize, Schema)] +struct Page { + items: Vec, + total: u64, +} + +// Used as Page -> Schema name: "Page_User" +// Used as Page -> Schema name: "Page_Company" +``` + +## Circular References + +Because `derive(Schema)` registers the name *before* building the full schema (if encountered recursively), it supports recursive types naturally. + +```rust +#[derive(Serialize, Schema)] +struct Category { + name: String, + // Recursive reference + sub_categories: Vec, +} +``` + +This works because `Vec` calls `Category::schema()`, which sees that `Category` is already being visited/registered and returns a `$ref` immediately. + +## Manual Registration + +If you need to register a schema manually (e.g., for a type you don't control), you can implement `RustApiSchema` yourself, or use `RustApi::register_schema::()` to ensure it appears in components even if not used in any route. + +```rust +RustApi::new() + .register_schema::() + // ... +```