所有文章 > API设计 > 深入探索 Rust Salvo:从简单博客系统到完整 RESTful API 的实战项目

深入探索 Rust Salvo:从简单博客系统到完整 RESTful API 的实战项目

摘要

本文通过实战项目深入探讨 Rust Salvo 框架的应用,详细介绍如何实现一个简单的博客系统,包括用户注册、登录和发布文章功能,以及如何构建一个完整的 RESTful API 并生成 API 文档(如 OpenAPI)。通过详细的理论知识和完整的示例代码,帮助您全面理解并应用这些功能,从而提升您的 Rust Web 开发技能。

8. 实战项目

8.1. 博客系统

实现一个简单的博客系统

在这个项目中,我们将实现一个简单的博客系统,包括用户注册、登录和发布文章。

  1. Cargo.toml文件中添加必要的依赖:
[dependencies]
salvo = "0.68"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.5", features = ["sqlite", "async-std"] }
bcrypt = "0.10"
jsonwebtoken = "8.0"
  1. 实现用户注册、登录和发布文章:
use salvo::prelude::*;
use salvo::Server;
use sqlx::{sqlite::SqlitePool, Pool, Sqlite};
use bcrypt::{hash, verify};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: i32,
    username: String,
    password: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

#[derive(Debug, Serialize, Deserialize)]
struct BlogPost {
    id: i32,
    title: String,
    content: String,
    author: String,
}

#[handler]
async fn register(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let user: User = req.parse_json().await.unwrap();
    let hashed_password = hash(&user.password, 4).unwrap();
    sqlx::query("INSERT INTO users (username, password) VALUES (?, ?)")
        .bind(&user.username)
        .bind(&hashed_password)
        .execute(db)
        .await
        .unwrap();
    res.render_plain_text("User registered");
}

#[handler]
async fn login(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let user: User = req.parse_json().await.unwrap();
    let row = sqlx::query_as!(User, "SELECT * FROM users WHERE username = ?", user.username)
        .fetch_one(db)
        .await
        .unwrap();

    if verify(&user.password, &row.password).unwrap() {
        let expiration = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() as usize + 3600;
        let claims = Claims {
            sub: row.username,
            exp: expiration,
        };
        let token = encode(&Header::default(), &claims, &EncodingKey::from_secret("secret".as_ref())).unwrap();
        res.render_plain_text(&token);
    } else {
        res.set_status_code(StatusCode::UNAUTHORIZED);
        res.render_plain_text("Invalid credentials");
    }
}

#[handler]
async fn create_post(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let post: BlogPost = req.parse_json().await.unwrap();
    sqlx::query("INSERT INTO posts (title, content, author) VALUES (?, ?, ?)")
        .bind(&post.title)
        .bind(&post.content)
        .bind(&post.author)
        .execute(db)
        .await
        .unwrap();
    res.render_plain_text("Post created");
}

#[tokio::main]
async fn main() {
    let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
    sqlx::query("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
        .execute(&db)
        .await
        .unwrap();
    sqlx::query("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, content TEXT, author TEXT)")
        .execute(&db)
        .await
        .unwrap();

    let router = Router::new()
        .post("/register", register)
        .post("/login", login)
        .post("/posts", create_post);

    Server::new(router.with_shared_data(db))
        .bind(([0, 0, 0, 0], 3030))
        .await
        .unwrap();
}

这个示例展示了如何实现用户注册、登录和创建博客文章的基本功能。

8.2. RESTful API

构建一个完整的 RESTful API

在这个项目中,我们将构建一个完整的 RESTful API,包括 CRUD 操作和 API 文档生成。

  1. Cargo.toml文件中添加必要的依赖:
[dependencies]
salvo = "0.68"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.5", features = ["sqlite", "async-std"] }
utoipa = "0.3"
utoipa-swagger-ui = "0.3"
  1. 实现 RESTful API
use salvo::prelude::*;
use salvo::Server;
use serde::{Serialize, Deserialize};
use sqlx::{sqlite::SqlitePool, Pool, Sqlite};
use utoipa::openapi::{OpenApi, Info};
use utoipa::OpenApi;

#[derive(Debug, Serialize, Deserialize)]
struct Item {
    id: i32,
    name: String,
    description: String,
}

#[handler]
async fn create_item(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let item: Item = req.parse_json().await.unwrap();
    sqlx::query("INSERT INTO items (name, description) VALUES (?, ?)")
        .bind(&item.name)
        .bind(&item.description)
        .execute(db)
        .await
        .unwrap();
    res.render_plain_text("Item created");
}

#[handler]
async fn get_items(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let items: Vec<Item> = sqlx::query_as!(Item, "SELECT id, name, description FROM items")
        .fetch_all(db)
        .await
        .unwrap();
    res.render_json(&items);
}

#[handler]
async fn update_item(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let item: Item = req.parse_json().await.unwrap();
    sqlx::query("UPDATE items SET name = ?, description = ? WHERE id = ?")
        .bind(&item.name)
        .bind(&item.description)
        .bind(item.id)
        .execute(db)
        .await
        .unwrap();
    res.render_plain_text("Item updated");
}

#[handler]
async fn delete_item(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let id: i32 = req.parse_json().await.unwrap();
    sqlx::query("DELETE FROM items WHERE id = ?")
        .bind(id)
        .execute(db)
        .await
        .unwrap();
    res.render_plain_text("Item deleted");
}

#[tokio::main]
async fn main() {
    let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
    sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, description TEXT)")
        .execute(&db)
        .await
        .unwrap();

    let router = Router::new()
        .post("/items", create_item)
        .get("/items", get_items)
        .put("/items", update_item)
        .delete("/items", delete_item);

    Server::new(router.with_shared_data(db))
        .bind(([0, 0, 0, 0], 3030))
        .await
        .unwrap();
}

API 文档生成(如 OpenAPI)

  1. 使用utoipa生成 OpenAPI 文档:
#[utoipa::path(
    post,
    path = "/items",
    request_body = Item,
    responses(
        (status = 201, description = "Item created"),
        (status = 400, description = "Bad request")
    )
)]
#[handler]
async fn create_item(req: &mut Request, res: &mut Response, db: &SqlitePool) {
    let item: Item = req.parse_json().await.unwrap();
    sqlx::query("INSERT INTO items (name, description) VALUES (?, ?)")
        .bind(&item.name)
        .bind(&item.description)
        .execute(db)
        .await
        .unwrap();
    res.set_status_code(StatusCode::CREATED);
    res.render_plain_text("Item created");
}

#[tokio::main]
async fn main() {
    let db = SqlitePool::connect("sqlite::memory:").await.unwrap();
    sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, description TEXT)")
        .execute(&db)
        .await
        .unwrap();

    let api = utoipa::openapi::OpenApi::new(
        Info {
            title: "My API",
            description: Some("This is a sample API"),
            version: "1.0.0",
        },
        vec![utoipa::openapi::paths::PathItem::new("create_item", create_item)]
    );

    let router = Router::new()
        .post("/items", create_item)
        .get("/items", get_items)
        .put("/items", update_item)
        .delete("/items", delete_item)
        .get("/openapi.json", |_, res| {
            res.render_json(&api);
        })
        .get("/swagger-ui", |_, res| {
            res.render_html(utoipa_swagger_ui::SwaggerUi::new("/openapi.json"));
        });

    Server::new(router.with_shared_data(db))
        .bind(([0, 0, 0, 0], 3030))
        .await
        .unwrap();
}

通过这些示例,我们可以构建一个完整的 RESTful API,并生成 OpenAPI 文档。

通过这个详细的指南,您将能够全面掌握 Rust Salvo 框架的实战应用,并能够构建功能丰富的 Web 应用和 RESTful API

文章转自微信公众号@育儿之家 YEZJ

#你可能也喜欢这些API文章!