-
-
Notifications
You must be signed in to change notification settings - Fork 2
docs: add validation recipe and improve curriculum #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "last_processed_ref": "v0.1.335", | ||
| "date": "2026-02-23", | ||
| "notes": "Fixed MockServer examples in testing recipe. Added Graceful Shutdown recipe. Enhanced Learning Path with 'The Email Worker' mini-project." | ||
| "date": "2026-03-01", | ||
| "notes": "Added Advanced Validation Patterns recipe. Enhanced Learning Path with 'The Live Chat Room' mini-project. Fixed docs for File Uploads and Testing." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Documentation Run Report: 2026-03-01 | ||
|
|
||
| **Status**: Success | ||
| **Detected Version**: v0.1.335 (No change) | ||
| **Focus**: Cookbook Expansion & Learning Path Improvement | ||
|
|
||
| ## Changes | ||
|
|
||
| ### 1. Fixes | ||
| - **`docs/cookbook/src/recipes/file_uploads.md`**: Clarified usage of `.body_limit()` vs `DefaultBodyLimit` middleware. Removed confusing double-configuration in the example. | ||
| - **`docs/cookbook/src/recipes/testing.md`**: Fixed missing `RequestMatcher` import in `MockServer` example. | ||
|
|
||
| ### 2. New Recipes | ||
| - **`docs/cookbook/src/recipes/validation.md`**: Added "Advanced Validation Patterns" covering custom validators, cross-field validation, and error customization. | ||
|
|
||
| ### 3. Learning Path Improvements | ||
| - **`docs/cookbook/src/learning/curriculum.md`**: | ||
| - **Module 9 (WebSockets)**: Added "The Live Chat Room" mini-project. | ||
| - **Module 14 (High Performance)**: Added explicit instruction to enable `http3` feature. | ||
| - **Module 5 (Validation)**: Linked to the new Validation recipe. | ||
|
|
||
| ### 4. Index Updates | ||
| - Added "Advanced Validation" to `docs/cookbook/src/SUMMARY.md`. | ||
|
|
||
| ## Next Steps | ||
| - Consider a recipe for "Structured Logging with Tracing". | ||
| - Review "Module 12: Observability" for potential mini-project additions. |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,12 +31,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { | |||||||||
|
|
||||||||||
| RustApi::new() | ||||||||||
| // Increase body limit to 1GB (default is usually 1MB) | ||||||||||
| .body_limit(1024 * 1024 * 1024) | ||||||||||
| .route("/upload", post(upload_handler)) | ||||||||||
| // Increase body limit to 50MB (default is usually 2MB) | ||||||||||
| // ⚠️ IMPORTANT: Since Multipart buffers the whole body, | ||||||||||
| // setting this too high can exhaust server memory. | ||||||||||
| .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) | ||||||||||
| .body_limit(1024 * 1024 * 1024) | ||||||||||
| .route("/upload", post(upload_handler)) | ||||||||||
| .run("127.0.0.1:8080") | ||||||||||
| .await | ||||||||||
|
||||||||||
| .await | |
| .await?; | |
| Ok(()) |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section still references DefaultBodyLimit as the mitigation, but the recipe now recommends configuring body limits via .body_limit(...). To avoid conflicting guidance, update the mitigation text to match the actual mechanism shown in the example (or explicitly recommend BodyLimitLayer and remove the DefaultBodyLimit naming if it’s no longer part of the public API).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,7 +84,7 @@ When your API calls external services (e.g., payment gateways, third-party APIs) | |
| `rustapi-testing` provides `MockServer` for this purpose. | ||
|
|
||
| ```rust | ||
| use rustapi_testing::{MockServer, MockResponse}; | ||
| use rustapi_testing::{MockServer, MockResponse, RequestMatcher}; | ||
|
|
||
| #[tokio::test] | ||
| async fn test_external_integration() { | ||
|
|
@@ -93,7 +93,7 @@ async fn test_external_integration() { | |
|
|
||
| // 2. Define an expectation | ||
| mock_server.expect( | ||
| rustapi_testing::RequestMatcher::new() | ||
| RequestMatcher::new() | ||
| .method("GET") | ||
| .path("/external-data") | ||
|
Comment on lines
95
to
98
|
||
| ).respond_with( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,173 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Advanced Validation Patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| While simple validation (length, range, email) is straightforward with `#[derive(Validate)]`, real-world applications often require complex logic, such as cross-field checks, custom business rules, and asynchronous database lookups. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Custom Synchronous Validators | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| You can define custom validation logic by writing a function and referencing it with `#[validate(custom = "...")]`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ### Example: Password Strength | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```rust | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use rustapi_macros::Validate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use rustapi_validate::ValidationError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize, Validate)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct SignupRequest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[validate(custom = "validate_password_strength")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub password: String, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+18
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_password_strength(password: &String) -> Result<(), ValidationError> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if password.len() < 8 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(ValidationError::new("password_too_short")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+24
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let has_uppercase = password.chars().any(|c| c.is_uppercase()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let has_number = password.chars().any(|c| c.is_numeric()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !has_uppercase || !has_number { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(ValidationError::new("password_too_weak")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Cross-Field Validation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Sometimes validation depends on multiple fields (e.g., "start date must be before end date" or "password confirmation must match"). Since the `Validate` macro works on individual fields, cross-field validation is typically done on the struct level. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Currently, `rustapi-validate` focuses on field-level validation. For struct-level checks, you can implement a custom method and call it manually, or use a "virtual" field strategy. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| A common pattern is to validate the struct *after* extraction: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```rust | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use rustapi_rs::prelude::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[derive(Debug, Deserialize, Validate)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub struct DateRange { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub start: chrono::NaiveDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub end: chrono::NaiveDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| impl DateRange { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn validate_logical(&self) -> Result<(), ApiError> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.start > self.end { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(ApiError::unprocessable_entity( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "start_date_after_end_date", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Start date must be before end date" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+60
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(ApiError::unprocessable_entity( | |
| "start_date_after_end_date", | |
| "Start date must be before end date" | |
| )); | |
| return Err(ApiError::new( | |
| StatusCode::UNPROCESSABLE_ENTITY, | |
| "Start date must be before end date", | |
| )); |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidationContext in rustapi_validate::v2 has no get::<T>() method, so ctx.get::<Arc<AppState>>() won’t compile. In this repo, ValidationContext is for validators (e.g., ctx.database(), ctx.http(), ctx.custom(...)), so the example should either use the built-in async_unique rule or show how to plug in a DatabaseValidator via ValidationContextBuilder and then use ctx.database() inside the custom validator.
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Registering the Context” section is inaccurate for RustAPI’s current extractor behavior: AsyncValidatedJson looks up a ValidationContext directly from request state (req.state().get::<ValidationContext>()). Calling .state(Arc<AppState>) won’t make it available in ValidationContext automatically; the app needs to insert a ValidationContext (typically built via ValidationContextBuilder) into state explicitly.
| For async validation to work, you must ensure your application state is available to the validator. `AsyncValidatedJson` attempts to extract `ValidationContext` from the request state. | |
| Typically, if you use `RustApi::new().state(...)`, the state is automatically available. | |
| ```rust | |
| use rustapi_rs::prelude::*; | |
| #[tokio::main] | |
| async fn main() { | |
| let state = Arc::new(AppState { /* ... */ }); | |
| RustApi::new() | |
| .state(state) // Injected into ValidationContext automatically | |
| For async validation to work, you must ensure a `ValidationContext` is available to the validator. `AsyncValidatedJson` attempts to extract a `ValidationContext` from the request state. | |
| This means you need to build a `ValidationContext` (typically via `ValidationContextBuilder`) and register it as the server state. Your application state (such as `Arc<AppState>`) can be stored inside the `ValidationContext` and later retrieved in your validators with `ctx.get::<Arc<AppState>>()`. | |
| ```rust | |
| use rustapi_rs::prelude::*; | |
| use rustapi_validate::{ValidationContext, ValidationContextBuilder}; | |
| #[tokio::main] | |
| async fn main() { | |
| let state = Arc::new(AppState { /* ... */ }); | |
| // Build a ValidationContext that holds your application state | |
| let validation_ctx: ValidationContext = ValidationContextBuilder::new() | |
| .with_state(state.clone()) | |
| .build(); | |
| RustApi::new() | |
| // Register the ValidationContext so AsyncValidatedJson can find it | |
| .state(validation_ctx) |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidationError::new("custom_code").with_message(...) is not a valid API for rustapi_validate::ValidationError in this repo (new takes a list of field errors, and with_message is an associated constructor, not a chainable setter). Consider updating this section to show the correct constructors (e.g., v2 RuleError::new(code, message) for rule errors, or rustapi_validate::ValidationError::field(field, code, message) for legacy-style aggregated errors).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example sets
.body_limit(1024 * 1024 * 1024)(1GB) even though the recipe notes Multipart buffers the full body into memory. This is a risky default in docs because it materially increases DoS/memory-exhaustion risk; consider using a smaller, “reasonable” value (e.g., 10–100MB) and mentioning per-route limiting viaBodyLimitLayerwhen only uploads need it.