» Rust:使用Rocket构建REST API » 2. 开发 » 2.4 路由设置

路由设置

修改 src/main.rs,为 Book 添加 CRUD 路由:

添加 CRUD 操作

#[macro_use]
extern crate rocket;

mod model;

use chrono::Utc;
use rocket::http::Status;
use rocket::response::content;
use rocket::response::status::{self, NoContent};
use rocket::serde::json::Json;
use rusqlite::{params, Connection, Result as SqliteResult};
use std::sync::Mutex;

// 初始化数据库实例
lazy_static::lazy_static! {
    static ref DB: Mutex<Database> = Mutex::new(Database::new().unwrap());
}

// Define the database schema
pub struct Database {
    conn: Connection,
}

impl Database {
    pub fn new() -> SqliteResult<Self> {
        let conn = Connection::open("test.db")?;
        conn.execute(
            "CREATE TABLE IF NOT EXISTS books (
                id INTEGER PRIMARY KEY,
                title TEXT NOT NULL,
                author TEXT NOT NULL,
                published_at TEXT NOT NULL,
                description TEXT NOT NULL,
                isbn TEXT NOT NULL,
                total_pages INTEGER NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )",
            [],
        )?;
        Ok(Database { conn })
    }

    pub fn get_books(&self) -> SqliteResult<Vec<model::Book>> {
        let mut stmt = self.conn.prepare("SELECT * FROM books")?;
        let rows = stmt.query_map([], |row| {
            Ok(model::Book {
                id: row.get(0)?,
                title: row.get(1)?,
                author: row.get(2)?,
                published_at: row.get(3)?,
                description: row.get(4)?,
                isbn: row.get(5)?,
                total_pages: row.get(6)?,
                created_at: row.get(7)?,
                updated_at: row.get(8)?,
            })
        })?;

        let mut books = Vec::new();
        for book in rows {
            books.push(book?);
        }
        Ok(books)
    }

    pub fn get_book(&self, id: u32) -> SqliteResult<Option<model::Book>> {
        let mut stmt = self.conn.prepare("SELECT * FROM books WHERE id = ?")?;
        let mut rows = stmt.query([id])?;

        if let Some(row) = rows.next()? {
            Ok(Some(model::Book {
                id: row.get(0)?,
                title: row.get(1)?,
                author: row.get(2)?,
                published_at: row.get(3)?,
                description: row.get(4)?,
                isbn: row.get(5)?,
                total_pages: row.get(6)?,
                created_at: row.get(7)?,
                updated_at: row.get(8)?,
            }))
        } else {
            Ok(None)
        }
    }

    pub fn create_book(&self, book: &model::Book) -> SqliteResult<()> {
        self.conn.execute(
            "INSERT INTO books (title, author, published_at, description, isbn, total_pages)
             VALUES (?, ?, ?, ?, ?, ?)",
            params![
                book.title,
                book.author,
                book.published_at,
                book.description,
                book.isbn,
                book.total_pages,
            ],
        )?;
        Ok(())
    }

    pub fn update_book(&self, id: u32, book: &model::Book) -> SqliteResult<()> {
        self.conn.execute(
            "UPDATE books SET title = ?, author = ?, published_at = ?, description = ?, isbn = ?, total_pages = ?, updated_at = ?
             WHERE id = ?",
            params![
                book.title,
                book.author,
                book.published_at,
                book.description,
                book.isbn,
                book.total_pages,
                Utc::now().to_rfc3339(),
                id,
            ],
        )?;
        Ok(())
    }

    pub fn delete_book(&self, id: u32) -> SqliteResult<()> {
        self.conn.execute("DELETE FROM books WHERE id = ?", [id])?;
        Ok(())
    }
}

#[derive(serde::Serialize)]
struct ErrorResponse {
    error: String,
}

// Define a health endpoint handler, use `/health` or `/`
#[get("/")]
fn health() -> content::RawJson<&'static str> {
    // Return a simple response indicating the server is healthy
    content::RawJson("{\"status\":\"ok\"}")
}

#[get("/books")]
fn get_books() -> Result<Json<Vec<model::Book>>, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.get_books() {
        Ok(books) => Ok(Json(books)),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[get("/books/<id>")]
fn get_book(id: u32) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.get_book(id) {
        Ok(book) => match book {
            Some(b) => Ok(Json(b)),
            None => Err(status::Custom(
                Status::NotFound,
                Json(ErrorResponse {
                    error: format!("book {id} not found"),
                }),
            )),
        },
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[post("/books", format = "json", data = "<book>")]
fn create_book(
    book: Json<model::Book>,
) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.create_book(&book) {
        Ok(_) => Ok(book),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[put("/books/<id>", format = "json", data = "<book>")]
fn update_book(
    id: u32,
    book: Json<model::Book>,
) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.update_book(id, &book) {
        Ok(_) => Ok(book),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[delete("/books/<id>")]
fn delete_book(id: u32) -> Result<NoContent, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.delete_book(id) {
        Ok(_) => Ok(NoContent),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount(
        "/",
        routes![
            health,
            get_books,
            get_book,
            create_book,
            update_book,
            delete_book
        ],
    )
}

此刻,暂使用 SQLite1 数据库作演示使用。

目前的依赖列表, Cargo.toml:

[dependencies]
chrono = { version = "0.4.35", features = ["serde"] }
lazy_static = "1.4.0"
rocket = { version = "0.5.0", features = ["json"] }
rusqlite = "0.31.0"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"

rusqlite 是一个在 Rust 语言中使用 SQLite 的高效封装库。

curl 测试

创建一本新图书:

curl -X POST \
  http://localhost:8000/books \
  -H 'Content-Type: application/json' \
  -d '{
    "id": 0,
    "title": "Sample Book",
    "author": "John Doe",
    "published_at": "2023-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 200,
    "created_at": "",
    "updated_at": ""
}'

应如下响应:

{"id":0,"title":"Sample Book","author":"John Doe","published_at":"2023-01-01","description":"A sample book description","isbn":"1234567890","total_pages":200,"created_at":"","updated_at":""}

根据 ID 获取一本图书:

curl -X GET http://localhost:8000/books/1

结果:

{
  "id": 1,
  "title": "Sample Book",
  "author": "John Doe",
  "published_at": "2023-01-01",
  "description": "A sample book description",
  "isbn": "1234567890",
  "total_pages": 200,
  "created_at": "2024-03-14 06:37:27",
  "updated_at": "2024-03-14 06:37:27"
}

列出所有图书:

curl -X GET http://localhost:8000/books

结果列表:

[
  {
    "id": 1,
    "title": "Sample Book",
    "author": "John Doe",
    "published_at": "2023-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 200,
    "created_at": "2024-03-14 06:37:27",
    "updated_at": "2024-03-14 06:37:27"
  }
]

更新一本已有的图书:

curl -X PUT \
  http://localhost:8000/books/1 \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Updated Book Title",
    "author": "Jane Smith",
    "id": 0,
    "published_at": "2023-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 200,
    "created_at": "",
    "updated_at": ""
}'

结果:

{"id":0,"title":"Updated Book Title","author":"Jane Smith","published_at":"2023-01-01","description":"A sample book description","isbn":"1234567890","total_pages":200,"created_at":"","updated_at":""}

删除一本存在的图书:

curl -X DELETE http://localhost:8000/books/1

服务端返回 code 204 以表示成功删除。

此刻 REST API 服务器已经初具雏形。不错!

Footnotes

  1. SQLite: https://www.sqlite.org/index.html

上页下页