Manage Apple Reminders via the `remindctl` CLI on macOS (list, add, edit, complete, delete)....
npx skills add salvo-rs/salvo-skills --skill "salvo-data-extraction"
Install specific skill from multi-skill repository
# Description
Extract and validate data from requests including JSON, forms, query parameters, and path parameters. Use for handling user input and API payloads.
# SKILL.md
name: salvo-data-extraction
description: Extract and validate data from requests including JSON, forms, query parameters, and path parameters. Use for handling user input and API payloads.
Salvo Data Extraction
This skill helps extract and validate data from HTTP requests in Salvo applications.
Manual Extraction (Simplest)
For simple cases, extract directly from Request:
use salvo::prelude::*;
#[handler]
async fn handler(req: &mut Request) -> String {
// Query parameter
let name = req.query::<String>("name").unwrap_or_default();
// Path parameter (requires route like /users/{id})
let id = req.param::<i64>("id").unwrap();
// Header
let token = req.header::<String>("Authorization");
// Parse JSON body
let body: UserData = req.parse_json().await.unwrap();
// Parse form data
let form: LoginForm = req.parse_form().await.unwrap();
// Parse query parameters as struct
let pagination: Pagination = req.parse_queries().unwrap();
format!("Processed request")
}
Using JsonBody Extractor
use salvo::prelude::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[handler]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
let user = body.into_inner();
println!("Name: {}, Email: {}", user.name, user.email);
StatusCode::CREATED
}
Extractible Trait
The Extractible derive macro enables automatic data extraction from requests.
Basic Usage
use salvo::prelude::*;
use serde::Deserialize;
#[derive(Extractible, Deserialize, Debug)]
#[salvo(extract(default_source(from = "body")))]
struct CreateUser {
name: String,
email: String,
}
#[handler]
async fn create_user(user: CreateUser) -> String {
format!("Created user: {:?}", user)
}
Data Sources
JSON Body
#[derive(Extractible, Deserialize)]
#[salvo(extract(default_source(from = "body")))]
struct UserData {
name: String,
email: String,
}
Query Parameters
#[derive(Extractible, Deserialize)]
#[salvo(extract(default_source(from = "query")))]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
#[handler]
async fn list_items(query: Pagination) -> String {
let page = query.page.unwrap_or(1);
let per_page = query.per_page.unwrap_or(20);
format!("Page {} with {} items", page, per_page)
}
Path Parameters
#[derive(Extractible, Deserialize)]
#[salvo(extract(default_source(from = "param")))]
struct UserId {
id: i64,
}
#[handler]
async fn show_user(params: UserId) -> String {
format!("User ID: {}", params.id)
}
Form Data
#[derive(Extractible, Deserialize)]
#[salvo(extract(default_source(from = "body"), default_format = "form"))]
struct LoginForm {
username: String,
password: String,
}
#[handler]
async fn login(form: LoginForm) -> Result<String, StatusError> {
Ok(format!("Login: {}", form.username))
}
Mixed Sources
Extract from multiple sources simultaneously:
#[derive(Extractible, Deserialize)]
struct UpdateUser {
#[salvo(extract(source(from = "param")))]
id: i64,
#[salvo(extract(source(from = "body")))]
name: String,
#[salvo(extract(source(from = "body")))]
email: String,
}
#[handler]
async fn update_user(data: UpdateUser) -> StatusCode {
// data.id from path, name and email from body
println!("Update user {}: {} {}", data.id, data.name, data.email);
StatusCode::OK
}
Validation with validator Crate
use salvo::prelude::*;
use serde::Deserialize;
use validator::Validate;
#[derive(Extractible, Deserialize, Validate)]
#[salvo(extract(default_source(from = "body")))]
struct CreateUser {
#[validate(length(min = 1, max = 100))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 18, max = 120))]
age: u8,
}
#[handler]
async fn create_user(user: CreateUser) -> Result<StatusCode, StatusError> {
// Validate input
if let Err(errors) = user.validate() {
return Err(StatusError::bad_request().brief(errors.to_string()));
}
Ok(StatusCode::CREATED)
}
Custom Validation Rules
use validator::{Validate, ValidationError};
fn validate_username(username: &str) -> Result<(), ValidationError> {
if username.contains("admin") {
return Err(ValidationError::new("forbidden_username"));
}
Ok(())
}
#[derive(Deserialize, Validate)]
struct User {
#[validate(custom(function = "validate_username"))]
username: String,
}
Nested Structures
use salvo::prelude::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct Address {
street: String,
city: String,
country: String,
}
#[derive(Extractible, Deserialize)]
#[salvo(extract(default_source(from = "body")))]
struct CreateUserWithAddress {
name: String,
email: String,
address: Address,
}
#[handler]
async fn create_user(data: CreateUserWithAddress) -> Result<String, StatusError> {
Ok(format!("User {} from {}", data.name, data.address.city))
}
Error Handling
use salvo::prelude::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[handler]
async fn create_user(req: &mut Request, res: &mut Response) {
match req.parse_json::<CreateUser>().await {
Ok(user) => {
res.render(Json(serde_json::json!({
"success": true,
"user": {"name": user.name, "email": user.email}
})));
}
Err(e) => {
res.status_code(StatusCode::BAD_REQUEST);
res.render(Json(serde_json::json!({
"error": format!("Invalid JSON: {}", e)
})));
}
}
}
Headers Extraction
#[handler]
async fn handler(req: &mut Request) -> Result<String, StatusError> {
// Get specific header
let auth = req.header::<String>("Authorization")
.ok_or_else(|| StatusError::unauthorized())?;
// Get content type
let content_type = req.header::<String>("Content-Type");
Ok(format!("Auth: {}", auth))
}
Complete Example
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 1, max = 100))]
name: String,
#[validate(email)]
email: String,
}
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
#[derive(Serialize)]
struct User {
id: i64,
name: String,
email: String,
}
#[handler]
async fn list_users(req: &mut Request) -> Json<Vec<User>> {
let pagination: Pagination = req.parse_queries().unwrap_or(Pagination {
page: Some(1),
per_page: Some(20),
});
Json(vec![User {
id: 1,
name: "Alice".to_string(),
email: "[email protected]".to_string(),
}])
}
#[handler]
async fn create_user(body: JsonBody<CreateUser>) -> Result<StatusCode, StatusError> {
let user = body.into_inner();
if let Err(e) = user.validate() {
return Err(StatusError::bad_request().brief(e.to_string()));
}
Ok(StatusCode::CREATED)
}
#[handler]
async fn get_user(req: &mut Request) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id")
.ok_or_else(|| StatusError::bad_request())?;
Ok(Json(User {
id,
name: "Alice".to_string(),
email: "[email protected]".to_string(),
}))
}
#[tokio::main]
async fn main() {
let router = Router::new()
.push(
Router::with_path("users")
.get(list_users)
.post(create_user)
.push(Router::with_path("{id}").get(get_user))
);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Best Practices
- Use
JsonBody<T>for simple JSON extraction - Use
Extractiblefor complex multi-source extraction - Specify data sources explicitly for clarity
- Validate input data at API boundaries
- Use typed path parameters (
req.param::<i64>) - Handle extraction errors with proper error responses
- Use
into_inner()to unwrap extracted data - Add
#[serde(default)]for optional fields
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.