用 Rust Actix-web 写一个 Todo 应用(二)── 请求获取和日志记录
如何获取路径参数
添加根据 id 获取数据操作:
#![allow(unused)] fn main() { use std::io::{Error, ErrorKind}; // ...省略 pub async fn get_todo(client: &Client, list_id: i32) -> Result<TodoList, Error> { let statement = client .prepare("select * from todo_list where id = $1") .await .unwrap(); let may_todo = client .query_opt(&statement, &[&list_id]) .await .expect("Error getting todo lists") .map(|row| TodoList::from_row_ref(&row).unwrap()); match may_todo { Some(todo) => Ok(todo), None => Err(Error::new(ErrorKind::NotFound, "Not found")), } } }
设置请求参数,设置使用 Info
作为请求路径参数序列化结构体:
#![allow(unused)] fn main() { use crate::db::{get_todo, get_todos}; use serde::Deserialize; // ...省略 #[derive(Deserialize)] pub struct Info { pub list_id: i32, } pub async fn todo(info: web::Path<Info>, db_pool: web::Data<Pool>) -> impl Responder { let client: Client = db_pool .get() .await .expect("Error connecting to the database"); let result = get_todo(&client, info.list_id).await; match result { Ok(todo) => HttpResponse::Ok().json(todo), Err(_) => HttpResponse::InternalServerError().into(), } } }
添加路由:
#![allow(unused)] fn main() { use handlers::{todo, todos}; // ...省略 HttpServer::new(move || { App::new() .data(pool.clone()) .service(hello) .route("/todos{_:/?}", web::get().to(todos)) .route("/todos/{list_id}{_:/?}", web::get().to(todo)) }) }
运行并获取结果:
$ curl 127.0.0.1:8000/todos/1
{"id":1,"title":"List 1"}
增加日志记录
为了方便查看操作过程,可以增加日志记录,使用 env_logger
方便从环境变量中设置日志记录级别,log
用于记录不同级别日志,比如 info
,debug
。
Cargo.toml
:
[dependencies]
# ...
env_logger = "0.8"
log="0.4"
在 .env
中可以手动设置日志记录级别,比如
RUST_LOG=info
main.rs
:
use actix_web::{get, middleware, web, App, HttpServer, Responder}; use env_logger; use log::info; // ... async fn main() -> io::Result<()> { dotenv().ok(); + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "actix_web=info"); + } + env_logger::init(); let cfg = crate::config::Config::from_env().unwrap(); let pool = cfg.pg.create_pool(NoTls).unwrap(); + info!( "Starting server at http://{}:{}", cfg.server.host, cfg.server.port ); HttpServer::new(move || { App::new() .data(pool.clone()) + .wrap(middleware::Logger::default()) // ...
运行以查看日志
$ cargo run
Compiling todo-list v0.1.0 (/Users/qiwihui/rust/todo-list)
Finished dev [unoptimized + debuginfo] target(s) in 1m 19s
Running `target/debug/todo-list`
[2020-10-23T06:43:18Z INFO todo_list] Starting server at http://127.0.0.1:8000
[2020-10-23T06:43:18Z INFO actix_server::builder] Starting 4 workers
[2020-10-23T06:43:18Z INFO actix_server::builder] Starting "actix-web-service-127.0.0.1:8000" service on 127.0.0.1:8000
[2020-10-23T06:45:04Z INFO actix_web::middleware::logger] 127.0.0.1:63751 "GET /todos HTTP/1.1" 200 79 "" "curl/7.64.1" 0.016465
如何获取请求体
首先,我们增加插入数据操作,sql 语句中的 returning id, title
用于返回插入成功的数据
db.rs
:
#![allow(unused)] fn main() { pub async fn create_todo(client: &Client, title: String) -> Result<TodoList, Error> { let statement = client .prepare("insert into todo_list (title) values ($1) returning id, title") .await .unwrap(); client .query(&statement, &[&title]) .await .expect("Error creating todo list") .iter() .map(|row| TodoList::from_row_ref(row).unwrap()) .collect::<Vec<TodoList>>() .pop() .ok_or(Error::new(ErrorKind::Other, "Error creating todo list")) } }
增加请求处理,CreateTodoList
用于序列化请求的数据:
handles.rs
#![allow(unused)] fn main() { #[derive(Deserialize)] pub struct CreateTodoList { pub title: String, } pub async fn create_todo( info: web::Json<CreateTodoList>, state: web::Data<AppState>, ) -> impl Responder { let client: Client = state .pool .get() .await .expect("Error connecting to the database"); let result = db::create_todo(&client, info.0.title.clone()).await; match result { Ok(todo) => HttpResponse::Ok().json(todo), Err(_) => HttpResponse::InternalServerError().into(), } } }
其中,state: web::Data<AppState>
将 原来的 pool 做了简单的封装,好处在于可以传入多个数据作为 web::Data
。
// .. 省略 pub struct AppState { pub pool: Pool, } #[actix_web::main] async fn main() -> io::Result<()> { // .. 省略 HttpServer::new(move || { App::new() .data(AppState { pool: pool.clone() })
同时添加路由:
#![allow(unused)] fn main() { App::new() // 省略 .route("/todos{_:/?}", web::post().to(handlers::create_todo)) }
完成后运行 cargo run
,在使用 curl 进行请求时,注意添加 -H "Content-Type: application/json"
头部信息,否则无法处理。
$ curl -X POST 127.0.0.1:8000/todos -d '{"title": "list 3"}' -H "Content-Type: application/json"
{"id":3,"title":"list 3"}
其他操作
继续添加其他操作,如获取创建单个项,以及完成项目(check_todo
)
#![allow(unused)] fn main() { pub async fn get_items(client: &Client, list_id: i32) -> Result<Vec<TodoItem>, Error> { // ... } pub async fn get_item(client: &Client, list_id: i32, item_id: i32) -> Result<TodoItem, Error> { // ... } pub async fn create_item(client: &Client, list_id: i32, title: String) -> Result<TodoItem, Error> { // ... } pub async fn check_todo(client: &Client, list_id: i32, item_id: i32) -> Result<bool, Error> { // ... } }
以及对应的路由和请求处理:
#![allow(unused)] fn main() { .route("/todos/{list_id}/items{_:/?}", web::get().to(handlers::items)) .route("/todos/{list_id}/items{_:/?}", web::post().to(handlers::create_item)) .route("/todos/{list_id}/items/{item_id}{_:/?}", web::get().to(handlers::get_item)) .route("/todos/{list_id}/items/{item_id}{_:/?}", web::put().to(handlers::check_todo)) }
#![allow(unused)] fn main() { pub async fn items(info: web::Path<GetTodoList>, state: web::Data<AppState>) -> impl Responder { // ... } pub async fn create_item( todo: web::Path<GetTodoList>, info: web::Json<CreateTodoItem>, state: web::Data<AppState>, ) -> impl Responder { // ... } pub async fn get_item(info: web::Path<GetTodoItem>, state: web::Data<AppState>) -> impl Responder { // ... } pub async fn check_todo( info: web::Path<GetTodoItem>, state: web::Data<AppState>, ) -> impl Responder { // ... } }
小结
- 获取路径参数
- 获取请求体
- 设置日志记录
参考文档和项目
GitHub repo: qiwihui/blog
Follow me: @qiwihui
Site: QIWIHUI