Getting started with Rust Web (2): Actix

The notes of this tutorial come from the rust web full-stack tutorial of Mr. Yang Xu, the link is as follows:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

To learn Rust Web, you need to learn the pre-knowledge of rust. You can learn another course from Mr. Yang Xu

https://www.bilibili.com/video/BV1hp4y1k7SV/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

The source code of the project can be viewed in git: (note that the author uses the mysql database instead of the database of the original tutorial)

https://github.com/aiai0603/rust_web_mysql

Let's get started with the rust-based web framework Actix today:

Actix is ​​easy to use

Actix - Actor asynchronous concurrency framework for Rust

Based on Tokio and Future, Actix has asynchronous non-blocking event-driven concurrency capabilities out of the box. It implements a low-level Actor model to provide a lock-free concurrency model, and at the same time provides synchronous Actors, which are fast, reliable, and easy to expand.

On top of Actix is ​​the high-performance Actix-web framework, which is very easy to use. Applications developed with Actix-web will include an HTTP server in the native executable. You can put it on another HTTP server like nginx. But even in the complete absence of another HTTP server (like nginx), Actix-web is sufficient to provide HTTP 1 and HTTP 2 support as well as SSL/TLS. This is very useful for building microservice distributions.

We need to create a project first, then introduce the required dependencies, and then use bin to specify our bin directory

[package]
name = "stage_2"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "3"
actix-rt = "1.1.1"

[[bin]]
name = "server1"

Then we create a bin directory and a server1.rs under src to write our framework:

For server1.rs we need to initialize an app as our web project, then configure a routing function for it, and then run our app project on the specified port. Because it is asynchronous, we need to modify it with await and async and use the actix_rt::main package

use actix_web::{
    
    web, App, HttpResponse, HttpServer, Responder};
use std::io;
#[actix_rt::main]
async fn main() -> io::Result<()> {
    
    
    let app = move || App::new().configure(general_routes);
    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}

Then we write our routing function, which passes in a configuration item, in which you can configure the processing method of the corresponding route, for example, we process the get method of the /health path, we can write it in the following way, and provide it after to A function as our handler function.

The processing function needs to implement the Responder trait, so our return value needs to be returned using HttpResponse-related functions, where Ok() means the status code 200, and then use the json function to return a piece of json as our return value

pub fn general_routes(cfg: &mut web::ServiceConfig) {
    
    
    cfg.route("/health", web::get().to(health_check_handler));
}

pub async fn health_check_handler() -> impl Responder {
    
    
    HttpResponse::Ok().json("Actix Web Service is running!")
}

Now that our creation is complete, we start our project on the command line, and then visit 120.0.0.1:3000, we can see that Actix Web Service is running! This sentence, then our project can be used normally

Build a complete rust API

Now we can run our Actix framework, and then we will try to build a complete api with the function of adding, deleting, modifying and checking. We will create a new teacher-service.rs, set this project as the default project, and load the packages we need :

[package]
name = "stage_3"
version = "0.1.0"
edition = "2021"
default-run = "teacher-service"

[dependencies]
actix-web = "3"
actix-rt = "1.1.1"
serde = { version = "1.0.132", features = ["derive"] }
chrono = { version = "0.4.19", features = ["serde"] }

[[bin]]
name = "server1"

[[bin]]
name = "teacher-service"

The database part will be explained in the next part. Let's put our data in memory first. We first create a models.rs which is used to define our data structure. Through the serde package just introduced, we can convert json data into our data structure

use actix_web::web;
use chrono::NaiveDateTime;
use serde::{
    
    Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Course {
    
    
    pub teacher_id: usize,
    pub id: Option<usize>,
    pub name: String,
    pub time: Option<NaiveDateTime>,
}

impl From<web::Json<Course>> for Course {
    
    
    fn from(course: web::Json<Course>) -> Self {
    
    
        Course {
    
    
            teacher_id: course.teacher_id,
            id: course.id,
            name: course.name.clone(),
            time: course.time,
        }
    }
}

Then we write a state.rs to encapsulate our globally shared data structure, which includes a response, a visit count and a returned structure. This content will be shared in our program as global content, because multiple programs will be involved Call visit_count and courses data, so we put them in Mutex to ensure mutual exclusion:

use std::sync::Mutex;

use crate::modelds::Course;

pub struct AppState {
    
    
    pub health_check_response: String,
    pub visit_count: Mutex<u32>,
    pub courses: Mutex<Vec<Course>>,
}

Then configure the routing of the simple get method in the previous step here, and we create a new routers.rs to store the routing

use super::handlers::*;
use actix_web::web;
pub fn general_routes(cfg: &mut web::ServiceConfig) {
    
    
    cfg.route("/health", web::get().to(health_check_handler));
}

Then create a new handlers.rs method to set our routing processing function, here we can call the globally registered app_state, which will be discussed in the next part. We take out the number of visits and response content in the shared data, and then return a json data.

use super::state::AppState;
use actix_web::{
    
    web, HttpResponse};

pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
    
    
    println!("incoming for health check");
    let health_check_response = &app_state.health_check_response;
    let mut visit_count = app_state.visit_count.lock().unwrap();
    let response = format!("{} {} times", health_check_response, visit_count);
    *visit_count += 1;
    HttpResponse::Ok().json(&response)
}

Finally, we configure our main function teacher-service.rs, and start our project on port 3000. We configure an initialized shared_data into the project, and then it can be used in the entire process of the project

use actix_web::{
    
    web, App, HttpServer};
use std::io;
use std::sync::Mutex;

#[path = "../handlers.rs"]
mod handlers;
#[path = "../models.rs"]
mod modelds;
#[path = "../routers.rs"]
mod routers;
#[path = "../state.rs"]
mod state;

use routers::*;
use state::AppState;

#[actix_rt::main]
async fn main() -> io::Result<()> {
    
    
    let shared_data = web::Data::new(AppState {
    
    
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        courses: Mutex::new(vec![]),
    });
    let app = move || {
    
    
        App::new()
            .app_data(shared_data.clone())
            .configure(general_routes)
    };

    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}

So we can start our project at 127.0.0.1:3000, when you call 127.0.0.1:3000/health, you can see the output

I'm OK. 1 times, each call, times + 1

Handle POST requests

We are now able to process the get request and return a predetermined set of data, now let's try to call the POST request to add our data:

We first register a new route, which is in /coursesa space, which means that all its APIs must start with localhost:3000/courses, we first add a route of localhost:3000/courses, which is a post method for new Add a piece of data

pub fn course_routes(cfg: &mut web::ServiceConfig) {
    
    
    cfg.service(
        web::scope("/courses")
            .route("/", web::post().to(new_course))
    );
}

Then we write its processing function in handlers.rs: what we have to do is write the data we received into app_state, we first calculate how many data there are to calculate the id number of the newly added data as a unique identifier, Then store the incoming data in our global data

It should be noted that we need to obtain ownership first, and then clone a copy of the data to calculate the length, otherwise the data will be recycled after use:

use super::modelds::Course;
use chrono::Utc;
pub async fn new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> HttpResponse {
    
    
    println!("Received new course");
    let course_count = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .filter(|course| course.teacher_id == new_course.teacher_id)
        .collect::<Vec<Course>>()
        .len();
    let new_course = Course {
    
    
        teacher_id: new_course.teacher_id,
        id: Some(course_count + 1),
        name: new_course.name.clone(),
        time: Some(Utc::now().naive_utc()),
    };
    app_state.courses.lock().unwrap().push(new_course);
    HttpResponse::Ok().json("Course added")
}

We write a test to test our interface:

mod tests {
    
    
    use super::*;
    use actix_web::http::StatusCode;
    use std::sync::Mutex;

    #[actix_rt::test]
    async fn post_course_test() {
    
    
        let course = web::Json(Course {
    
    
            teacher_id: 1,
            name: "Test course".into(),
            id: None,
            time: None,
        });

        let app_state: web::Data<AppState> = web::Data::new(AppState {
    
    
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let resp = new_course(course, app_state).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }
}

dynamic routing

Sometimes we want our path to contain the query data we need. For example, we want /course/1to query the course corresponding to the teacher whose id is 1 through , /course/1/12and use to query the course corresponding to the teacher whose id is 1 and whose id is 12. Then we need Build a dynamic route:

First, we write a route like this, where user_id and course_id can be extracted as parameters, and our path can match these routes

pub fn course_routes(cfg: &mut web::ServiceConfig) {
    
    
    cfg.service(
        web::scope("/courses")
            .route("/", web::post().to(new_course))
            .route("/{user_id}", web::get().to(get_courses_for_teacher))
            .route("/{user_id}/{course_id}", web::get().to(get_course_detail)),
    );
}

Afterwards, we write the processing method in handlers, and we can get our path by passing in the parameter params. We need to build our query to return the corresponding value:

pub async fn get_courses_for_teacher(
    app_state: web::Data<AppState>,
    params: web::Path<usize>,
) -> HttpResponse {
    
    
    let teacher_id: usize = params.0;

    let filtered_courses = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .filter(|course| course.teacher_id == teacher_id)
        .collect::<Vec<Course>>();

    if filtered_courses.len() > 0 {
    
    
        HttpResponse::Ok().json(filtered_courses)
    } else {
    
    
        HttpResponse::Ok().json("No courses found for teacher".to_string())
    }
}

pub async fn get_course_detail(
    app_state: web::Data<AppState>,
    params: web::Path<(usize, usize)>,
) -> HttpResponse {
    
    
    let (teacher_id, course_id) = params.0;
    let selected_course = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .find(|x| x.teacher_id == teacher_id && x.id == Some(course_id))
        .ok_or("Course not found");
    if let Ok(course) = selected_course {
    
    
        HttpResponse::Ok().json(course)
    } else {
    
    
        HttpResponse::Ok().json("Course not found".to_string())
    }
}

We can also add tests for these two methods we wrote:

  #[actix_rt::test]
    async fn get_all_courses_success() {
    
    
        let app_state: web::Data<AppState> = web::Data::new(AppState {
    
    
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let teacher_id: web::Path<usize> = web::Path::from(1);
        let resp = get_courses_for_teacher(app_state, teacher_id).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_one_course_success() {
    
    
        let app_state: web::Data<AppState> = web::Data::new(AppState {
    
    
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let params: web::Path<(usize, usize)> = web::Path::from((1, 1));
        let resp = get_course_detail(app_state, params).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }

If the test passes, we will have a complete api with new addition and query functions, and we will register the route we just wrote to our main program:

async fn main() -> io::Result<()> {
    
    
    let shared_data = web::Data::new(AppState {
    
    
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        courses: Mutex::new(vec![]),
    });
    let app = move || {
    
    
        App::new()
            .app_data(shared_data.clone())
            .configure(general_routes)
            .configure(course_routes)
    };

    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}

Now you can use POSTMAN and other tools to test the API for adding and querying data. Later, we will explain how to persist our data through the database instead of using the globally injected data structure to store data.

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/129120470