docs: add validation recipe and improve curriculum#136
Conversation
- Added `docs/cookbook/src/recipes/validation.md` covering synchronous, cross-field, and asynchronous validation patterns. - Updated `docs/cookbook/src/recipes/file_uploads.md` to clarify `.body_limit()` usage. - Fixed `docs/cookbook/src/recipes/testing.md` missing import. - Enhanced `docs/cookbook/src/learning/curriculum.md` with a new mini-project and better instructions. - Updated agent run report and inventory.
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Pull request overview
This PR expands the RustAPI cookbook/learning materials by adding an advanced validation recipe, correcting some existing cookbook examples, and updating internal documentation-agent tracking files.
Changes:
- Added a new “Advanced Validation Patterns” cookbook recipe and linked it from the learning curriculum and SUMMARY.
- Updated the File Uploads recipe to remove confusing body-limit configuration guidance and adjusted the stated default limit.
- Updated the Testing recipe example imports and refreshed learning curriculum items (WebSockets mini-project + clearer HTTP/3 setup), plus updated
.agentstate files.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/cookbook/src/recipes/validation.md | New advanced validation recipe (custom validators, cross-field, async checks). |
| docs/cookbook/src/recipes/testing.md | Fixes/updates MockServer example import/usage. |
| docs/cookbook/src/recipes/file_uploads.md | Simplifies body limit configuration guidance for uploads and updates default limit text. |
| docs/cookbook/src/learning/curriculum.md | Links new validation recipe; adds WS mini-project; clarifies HTTP/3 feature setup. |
| docs/cookbook/src/SUMMARY.md | Adds the new validation recipe to cookbook navigation. |
| docs/.agent/run_report_2026-03-01.md | New documentation agent run report entry. |
| docs/.agent/last_run.json | Updates the “last run” metadata for the docs agent. |
| docs/.agent/docs_inventory.md | Adds the new recipe to the docs inventory list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .body_limit(1024 * 1024 * 1024) | ||
| .route("/upload", post(upload_handler)) | ||
| .run("127.0.0.1:8080") | ||
| .await |
There was a problem hiding this comment.
main returns Result<..., Box<dyn Error>>, but the snippet ends with .run(...).await without ?/unwrap() and without returning Ok(()). As written, the example won’t compile; propagate the error from run and return success at the end of main.
| .await | |
| .await?; | |
| Ok(()) |
| - **Mitigation**: Set a reasonable `DefaultBodyLimit` (e.g., 10MB - 100MB) to prevent DoS attacks. | ||
|
|
||
| ### 2. Body Limits | ||
| The default request body limit is small (2MB) to prevent attacks. You **must** explicitly increase this limit for file upload routes using `.layer(DefaultBodyLimit::max(size_in_bytes))`. | ||
| The default request body limit is small (1MB) to prevent attacks. You **must** explicitly increase this limit for file upload routes using the `.body_limit(size_in_bytes)` method on the `RustApi` builder. | ||
|
|
There was a problem hiding this comment.
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).
| mock_server.expect( | ||
| rustapi_testing::RequestMatcher::new() | ||
| RequestMatcher::new() | ||
| .method("GET") | ||
| .path("/external-data") |
There was a problem hiding this comment.
RequestMatcher::method takes an http::Method, so .method("GET") won’t compile. Update the snippet to use Method::GET (and import http::Method), or adjust the API call to match the actual RequestMatcher signature.
| #[derive(Debug, Deserialize, Validate)] | ||
| pub struct SignupRequest { | ||
| #[validate(custom = "validate_password_strength")] | ||
| pub password: String, |
There was a problem hiding this comment.
The example mixes the v2 derive macro (use rustapi_macros::Validate) with the legacy #[validate(custom = "...")] attribute. The v2 macro in this repo only supports built-in rules plus custom_async, so this snippet won’t compile as written. Either switch the whole example to the legacy validator crate (and import validator::Validate / validator::ValidationError), or update it to a v2-compatible approach (e.g., manual impl rustapi_validate::v2::Validate / use existing v2 rules).
| fn validate_password_strength(password: &String) -> Result<(), ValidationError> { | ||
| if password.len() < 8 { | ||
| return Err(ValidationError::new("password_too_short")); | ||
| } |
There was a problem hiding this comment.
rustapi_validate::ValidationError doesn’t support ValidationError::new("code") and isn’t the per-field error type used by the derive macro. As a result, validate_password_strength (return type and ValidationError::new(...) calls) won’t compile. Use validator::ValidationError for legacy custom sync validators, or use v2’s RuleError / ValidationErrors patterns instead.
| return Err(ApiError::unprocessable_entity( | ||
| "start_date_after_end_date", | ||
| "Start date must be before end date" | ||
| )); |
There was a problem hiding this comment.
ApiError::unprocessable_entity(...) doesn’t exist in this codebase (the provided constructors are things like ApiError::validation(...), bad_request(...), etc.). This cross-field validation example won’t compile; consider constructing a 422 via ApiError::validation(vec![FieldError { ... }]) or ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, ...).
| 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", | |
| )); |
| async fn check_email_unique(email: &String, ctx: &ValidationContext) -> Result<(), RuleError> { | ||
| // 1. Retrieve the database connection from the context | ||
| // The context wraps the AppState you provided to the server | ||
| let state = ctx.get::<Arc<AppState>>() | ||
| .ok_or_else(|| RuleError::new("internal", "Database not available"))?; | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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) |
| 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)) |
There was a problem hiding this comment.
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 via BodyLimitLayer when only uploads need it.
| For custom validators, the `ValidationError` or `RuleError` constructor takes a code and a message: | ||
|
|
||
| ```rust | ||
| ValidationError::new("custom_code").with_message("Friendly error message"); | ||
| RuleError::new("custom_code", "Friendly error message"); | ||
| ``` |
There was a problem hiding this comment.
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).
This PR adds a new comprehensive recipe for "Advanced Validation Patterns" to the cookbook, covering custom validators, cross-field validation, and async database checks.
It also:
PR created automatically by Jules for task 16809744749603811864 started by @Tuntii