Rust:axum learning notes extract

Posted by ensanity on Sun, 02 Jan 2022 17:39:59 +0100

meet Last Continue today to learn how to Extract the desired content from the Request, which is called Extract with the concept in axum.

Preliminary knowledge: json serialization / deserialization

Since json format is widely used in web development, first get familiar with how to serialize / deserialize json in t rust.

[dependencies]
serde_json = "1"

Add serde first_ JSON dependency, and then it can be used. First define a struct:

#[derive(Debug, Serialize, Deserialize)]
struct Order {
    //order number
    order_no: String,
    //Total amount
    amount: f32,
    //Receiving address
    address: String,
}

Note: don't forget to add #[derive(Debug, Serialize, Deserialize)], which represents the modified struct, realizes serialization / deserialization, and "{:?}" The ability to debug output, of course, should be use d at the beginning:

use serde::{Deserialize, Serialize};
use serde_json as sj;

Next you can use:

//serialize
let order = Order{
    order_no:"1234567".to_string(),
    amount:100.0,
    address:"test".to_string()
};
let order_json =sj::to_string(&order).unwrap();
println!("{}",order_json);

//Deserialization
let order_json = r#"
        {
            "order_no": "1234567",
            "amount": 100.0,
            "address": "test"
        }
"#;
let order:Order = sj::from_str(order_json).unwrap();
println!("{:?}",order);

//The following two fields are less assigned, and an error will be reported during deserialization
let order_json = r#"
    {
        "order_no": "1234567"
    }
"#;
let order:Order = sj::from_str(order_json).unwrap();
println!("{:?}",order);

Output:

****************************

{"order_no":"1234567","amount":100.0,"address":"test"}
Order { order_no: "1234567", amount: 100.0, address: "test" }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error("missing field `amount`", line: 4, column: 9)', request/src/main.rs:198:48

****************************

It can be seen that, compared with json class libraries such as Jackson and gson in java and other languages, serde in t rust is very strict, and an error will be reported when deserializing one less field. Therefore, it is recommended to add Option for fields that may be empty when defining struct

#[derive(Debug, Serialize, Deserialize)]
struct Order {
    //order number
    order_no: String,
    //Total amount
    amount: Option<f32>,
    //Receiving address
    address: Option<String>,
}

When deserializing this time, no error will be reported:

    //The following two fields are less assigned, and an error will be reported during deserialization
    let order_json = r#"
    {
        "order_no": "1234567"
    }
"#;
    let order: Order = sj::from_str(order_json).unwrap();
    println!("{:?}", order);

Output:

Order { order_no: "1234567", amount: None, address: None }

 

1, Extract content from path
1.1 single parameter extraction

route:

.route("/user/:id", get(user_info))

Processing function:

// eg: /user/30, id=30 will be parsed
async fn user_info(Path(id): Path<i32>) -> String {
    format!("user id:{}", id)
}

You can also do this:

// eg: /user2/30, id=30 will be parsed
async fn user_info_2(id: Path<i32>) -> String {
    format!("user id:{}", id.0)
}

1.2 multi parameter extraction

route:

.route("/person/:id/:age", get(person))

Processing function:

// eg: /person/123/30, id=123, age=30
async fn person(Path((id, age)): Path<(i32, i32)>) -> String {
    format!("id:{},age:{}", id, age)
}

Tuples such as (X,Y) are used to extract parameters, but if there are many parameters, the parameters are usually objectified and encapsulated into a struct

1.3 struct extraction

route:

.route("/path_req/:a/:b/:c/:d", get(path_req))

Processing function:

#[derive(Deserialize)]
struct SomeRequest {
    a: String,
    b: i32,
    c: String,
    d: u32,
}

// eg: path_req/a1/b1/c1/d1
async fn path_req(Path(req): Path<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", req.a, req.b, req.c, req.d)
}

However, this method requires all parameters, such as: http://localhost:3000/path_req/abc/2/yjmyzz/4 , if 1 parameter is missing, for example: http://localhost:3000/path_req/abc/2/yjmyzz Route matching fails

 

2, Extract content from queryString

route:

.route("/query_req", get(query_req))

Processing function:

//eg: query_req/?a=test&b=2&c=abc&d=80
async fn query_req(Query(args): Query<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", args.a, args.b, args.c, args.d)
}

Note: according to the above processing method, QueryString must have parameters a, B, C and D at the same time, otherwise an error will be reported. If you want some parameters to be empty, you need to change SomeRequest's corresponding field to Option as mentioned earlier

#[derive(Deserialize)]
struct SomeRequest2 {
    a: Option<String>,
    b: Option<i32>,
    c: Option<String>,
    d: Option<u32>,
}

//eg: query_ req2? A = ABC & C = People's Republic of China & D = 123
async fn query_req2(Query(args): Query<SomeRequest2>) -> String {
    format!(
        "a:{},b:{},c:{},d:{}",
        args.a.unwrap_or_default(),
        args.b.unwrap_or(-1), //b the default value is specified as - 1
        args.c.unwrap_or_default(),
        args.d.unwrap_or_default()
    )
}

Sometimes, if you want to obtain all QueryString parameters, you can use HashMap and refer to the following code:

route:

.route("/query", get(query))

Processing function:

//eg: query?a=1&b=1.0&c=xxx
async fn query(Query(params): Query<HashMap<String, String>>) -> String {
    for (key, value) in &params {
        println!("key:{},value:{}", key, value);
    }
    format!("{:?}", params)
}

  

3, Submit extracted content from Form

route:

.route("/form", post(form_request))

Processing function:

// Form submission
async fn form_request(Form(model): Form<SomeRequest2>) -> String {
    format!(
        "a:{},b:{},c:{},d:{}",
        model.a.unwrap_or_default(),
        model.b.unwrap_or(-1), //b the default value is specified as - 1
        model.c.unwrap_or_default(),
        model.d.unwrap_or_default()
    )
}

  

4, Extract content from applicatiaion / JSON

route:

.route("/json", post(json_request))

Processing function:

// json submission
async fn json_request(Json(model): Json<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", model.a, model.b, model.c, model.d)
}

  

5, Extract HttpHeader

5.1 extract all header headers

route:

.route("/header", get(get_all_header))

Processing function:

/**
 * Get all request headers
 */
async fn get_all_header(headers: HeaderMap) -> String {
    for (key, value) in &headers {
        println!("key:{:?} , value:{:?}", key, value);
    }
    format!("{:?}", headers)
}

5.2 extract the specified header, such as user agent

route:

.route("/user_agent", get(get_user_agent_header))

Processing function:

/**
 * Get user in http headers_ Agent header
 */
async fn get_user_agent_header(TypedHeader(user_agent): TypedHeader<headers::UserAgent>) -> String {
    user_agent.to_string()
}

 

5, cookie read and write

route:

        .route("/set_cookie", get(set_cookie_and_redirect))
        .route("/get_cookie", get(get_cookie));

Processing function:

/**
 * Set the cookie and jump to a new page
 */
async fn set_cookie_and_redirect(mut headers: HeaderMap) -> (StatusCode, HeaderMap, ()) {
    //Set cookies, blog_ The URL is the key of the cookie
    headers.insert(
        axum::http::header::SET_COOKIE,
        HeaderValue::from_str("blog_url=http://yjmyzz.cnblogs.com/").unwrap(),
    );

    //Reset the LOCATION and jump to a new page
    headers.insert(
        axum::http::header::LOCATION,
        HeaderValue::from_str("/get_cookie").unwrap(),
    );
    //302 redirection
    (StatusCode::FOUND, headers, ())
}

/**
 * Read cookie s
 */
async fn get_cookie(headers: HeaderMap) -> (StatusCode, String) {
    //Read the cookie and convert it into a string
    let cookies = headers
        .get(axum::http::header::COOKIE)
        .and_then(|v| v.to_str().ok())
        .map(|v| v.to_string())
        .unwrap_or("".to_string());

    //cookie null judgment
    if cookies.is_empty() {
        println!("cookie is empty!");
        return (StatusCode::OK, "cookie is empty".to_string());
    }

    //Split cookie s into lists
    let cookies: Vec<&str> = cookies.split(';').collect();
    println!("{:?}", cookies);
    for cookie in &cookies {
        //Split content into k=v format
        let cookie_pair: Vec<&str> = cookie.split('=').collect();
        if cookie_pair.len() == 2 {
            let cookie_name = cookie_pair[0].trim();
            let cookie_value = cookie_pair[1].trim();
            println!("{:?}", cookie_pair);
            //Judge whether there is the blog just set_ url
            if cookie_name == "blog_url" && !cookie_value.is_empty() {
                println!("found:{}", cookie_value);
                return (StatusCode::OK, cookie_value.to_string());
            }
        }
    }
    return (StatusCode::OK, "empty".to_string());
}

 

Finally, the complete code of the above example is attached:

cargo.toml dependencies:

[dependencies]
axum = {  version="0.4.3", features = ["headers"] }
tokio = { version="1", features = ["full"] }
serde = { version="1", features = ["derive"] }
serde_json = "1"
http = "0.2.1"
headers = "0.3"

main.rs

use std::collections::HashMap;

use axum::{
    extract::{Form, Path, Query, TypedHeader},
    http::header::{HeaderMap, HeaderValue},
    response::Json,
    routing::{get, post},
    Router,
};
use http::StatusCode;
use serde::Deserialize;

// eg: /user/30, id=30 will be parsed
async fn user_info(Path(id): Path<i32>) -> String {
    format!("user id:{}", id)
}

// eg: /user2/30, id=30 will be parsed
async fn user_info_2(id: Path<i32>) -> String {
    format!("user id:{}", id.0)
}

// eg: /person/123/30, id=123, age=30
async fn person(Path((id, age)): Path<(i32, i32)>) -> String {
    format!("id:{},age:{}", id, age)
}



#[derive(Deserialize)]
struct SomeRequest2 {
    a: Option<String>,
    b: Option<i32>,
    c: Option<String>,
    d: Option<u32>,
}

#[derive(Deserialize)]
struct SomeRequest {
    a: String,
    b: i32,
    c: String,
    d: u32,
}

// eg: path_req/a1/b1/c1/d1
async fn path_req(Path(req): Path<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", req.a, req.b, req.c, req.d)
}

//eg: query_req/?a=test&b=2&c=abc&d=80
async fn query_req(Query(args): Query<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", args.a, args.b, args.c, args.d)
}

//eg: query_ req2? A = ABC & C = People's Republic of China & D = 123
async fn query_req2(Query(args): Query<SomeRequest2>) -> String {
    format!(
        "a:{},b:{},c:{},d:{}",
        args.a.unwrap_or_default(),
        args.b.unwrap_or(-1), //b the default value is specified as - 1
        args.c.unwrap_or_default(),
        args.d.unwrap_or_default()
    )
}

//eg: query?a=1&b=1.0&c=xxx
async fn query(Query(params): Query<HashMap<String, String>>) -> String {
    for (key, value) in &params {
        println!("key:{},value:{}", key, value);
    }
    format!("{:?}", params)
}

// Form submission
async fn form_request(Form(model): Form<SomeRequest2>) -> String {
    format!(
        "a:{},b:{},c:{},d:{}",
        model.a.unwrap_or_default(),
        model.b.unwrap_or(-1), //b the default value is specified as - 1
        model.c.unwrap_or_default(),
        model.d.unwrap_or_default()
    )
}

// json submission
async fn json_request(Json(model): Json<SomeRequest>) -> String {
    format!("a:{},b:{},c:{},d:{}", model.a, model.b, model.c, model.d)
}

/**
 * Get all request headers
 */
async fn get_all_header(headers: HeaderMap) -> String {
    for (key, value) in &headers {
        println!("key:{:?} , value:{:?}", key, value);
    }
    format!("{:?}", headers)
}

/**
 * Get user in http headers_ Agent header
 */
async fn get_user_agent_header(TypedHeader(user_agent): TypedHeader<headers::UserAgent>) -> String {
    user_agent.to_string()
}

/**
 * Set the cookie and jump to a new page
 */
async fn set_cookie_and_redirect(mut headers: HeaderMap) -> (StatusCode, HeaderMap, ()) {
    //Set cookies, blog_ The URL is the key of the cookie
    headers.insert(
        axum::http::header::SET_COOKIE,
        HeaderValue::from_str("blog_url=http://yjmyzz.cnblogs.com/").unwrap(),
    );

    //Reset the LOCATION and jump to a new page
    headers.insert(
        axum::http::header::LOCATION,
        HeaderValue::from_str("/get_cookie").unwrap(),
    );
    //302 redirection
    (StatusCode::FOUND, headers, ())
}

/**
 * Read cookie s
 */
async fn get_cookie(headers: HeaderMap) -> (StatusCode, String) {
    //Read the cookie and convert it into a string
    let cookies = headers
        .get(axum::http::header::COOKIE)
        .and_then(|v| v.to_str().ok())
        .map(|v| v.to_string())
        .unwrap_or("".to_string());

    //cookie null judgment
    if cookies.is_empty() {
        println!("cookie is empty!");
        return (StatusCode::OK, "cookie is empty".to_string());
    }

    //Split cookie s into lists
    let cookies: Vec<&str> = cookies.split(';').collect();
    println!("{:?}", cookies);
    for cookie in &cookies {
        //Split content into k=v format
        let cookie_pair: Vec<&str> = cookie.split('=').collect();
        if cookie_pair.len() == 2 {
            let cookie_name = cookie_pair[0].trim();
            let cookie_value = cookie_pair[1].trim();
            println!("{:?}", cookie_pair);
            //Judge whether there is the blog just set_ url
            if cookie_name == "blog_url" && !cookie_value.is_empty() {
                println!("found:{}", cookie_value);
                return (StatusCode::OK, cookie_value.to_string());
            }
        }
    }
    return (StatusCode::OK, "empty".to_string());
}

#[tokio::main]
async fn main() {
    // our router
    let app = Router::new()
        .route("/user/:id", get(user_info))
        .route("/user2/:id", get(user_info_2))
        .route("/person/:id/:age", get(person))
        .route("/path_req/:a/:b/:c/:d", get(path_req))
        .route("/query_req", get(query_req))
        .route("/query_req2", get(query_req2))
        .route("/query", get(query))
        .route("/form", post(form_request))
        .route("/json", post(json_request))
        .route("/header", get(get_all_header))
        .route("/user_agent", get(get_user_agent_header))
        .route("/set_cookie", get(set_cookie_and_redirect))
        .route("/get_cookie", get(get_cookie));

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

  

Reference documents:

https://docs.rs/axum/latest/axum/#extractors

https://github.com/tokio-rs/axum/tree/main/examples

Topics: Rust