web: hook up POST, PUT and DELETE for books

This commit is contained in:
Yiao Shen 2023-06-30 03:20:10 -04:00
parent edb8ddeae3
commit 2776e6e796
Signed by: y266shen
GPG Key ID: D126FA196DA0F362
5 changed files with 106 additions and 66 deletions

View File

@ -1,5 +1,5 @@
use super::CscLibraryError;
use crate::types::{Book, BookDetailed, UpdateBook};
use crate::types::{Book, BookDetailed, ModifyBook};
use anyhow::{bail, Result};
use log::warn;
@ -87,29 +87,31 @@ WHERE book_id=?1,watid=?2,time_borrow=?3",
}
pub fn category_exist(&self, name: &str) -> Result<bool, CscLibraryError> {
let res = self.category_cache.values().filter(|v| v.as_str() == name).count() > 0;
let res = self.category_cache.values().any(|v| v.as_str() == name);
Ok(res)
}
pub fn add_category(&mut self, cat_name: &str) -> Result<(), CscLibraryError> {
pub fn new_category(&mut self, cat_name: &str) -> Result<(), CscLibraryError> {
// Check if we have this category already
if self.category_exist(cat_name)? {
return Err(CscLibraryError::CategoryAlreadyExist);
}
self.conn.execute("INSERT INTO categories (category) VALUES (?)", [cat_name])?;
self.conn
.execute("INSERT INTO categories (category) VALUES (?)", [cat_name])?;
// Update cache
self.update_cagegory_cache()?;
Ok(())
}
pub fn remove_category(&mut self, cat_name: &str) -> Result<(), CscLibraryError> {
pub fn del_category(&mut self, cat_name: &str) -> Result<(), CscLibraryError> {
// Check if we have this category already
if self.category_exist(cat_name)? {
return Err(CscLibraryError::CategoryAlreadyExist);
}
self.conn.execute("DELETE FROM categories WHERE category=?", [cat_name])?;
self.conn
.execute("DELETE FROM categories WHERE category=?", [cat_name])?;
// Update cache
self.update_cagegory_cache()?;
Ok(())
@ -121,7 +123,10 @@ WHERE book_id=?1,watid=?2,time_borrow=?3",
.prepare("SELECT cat_id FROM book_categories WHERE id = ?")?;
let cat_id: Option<i64> = stmt.query_row([id], |row| Ok(row.get(0)?)).optional()?;
if let Some(cat_id) = cat_id {
Ok(self.category_cache.get(&cat_id).map(|cat_name| cat_name.to_owned()))
Ok(self
.category_cache
.get(&cat_id)
.map(|cat_name| cat_name.to_owned()))
} else {
Ok(None)
}
@ -190,7 +195,7 @@ WHERE id=?1",
}
}
pub fn new_book(&self, book: &UpdateBook) -> Result<(), CscLibraryError> {
pub fn add_book(&self, book: &ModifyBook) -> Result<(), CscLibraryError> {
if let Some(category_name) = &book.category {
if !self.category_cache.values().any(|val| val == category_name) {
return Err(CscLibraryError::BadCategoryName(category_name.clone()));
@ -205,7 +210,7 @@ WHERE id=?1",
Ok(())
}
pub fn update_book(&self, id: i64, book: &UpdateBook) -> Result<(), CscLibraryError> {
pub fn update_book(&self, id: i64, book: &ModifyBook) -> Result<(), CscLibraryError> {
if let Some(category_name) = &book.category {
if !self.category_cache.values().any(|val| val == category_name) {
return Err(CscLibraryError::BadCategoryName(category_name.clone()));
@ -215,7 +220,7 @@ WHERE id=?1",
let (_, sql, values) = book.to_sql();
self.conn.execute(
&format!(
"UPDATE books SET {},last_update=CURRENT_TIMESTAMP WHERE id=?",
"UPDATE books SET {},last_updated=CURRENT_TIMESTAMP WHERE id=?",
sql
),
rusqlite::params_from_iter(values.iter().chain([Value::from(id)].iter())),
@ -223,7 +228,7 @@ WHERE id=?1",
Ok(())
}
pub fn remove_book(&self, id: i64) -> Result<(), CscLibraryError> {
pub fn del_book(&self, id: i64) -> Result<(), CscLibraryError> {
self.conn.execute("DELETE FROM books WHERE id=?", [id])?;
Ok(())
}

View File

@ -8,7 +8,9 @@ mod web;
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("Hello, world!");
// initialize tracing
tracing_subscriber::fmt::init();
if let Err(e) = try_main().await {
eprintln!("{e}");
exit(1);
@ -18,19 +20,6 @@ async fn main() {
async fn try_main() -> Result<()> {
let path: PathBuf = PathBuf::from("./catalogue.db");
let db = db::CscLibraryDb::new(&path)?;
for book in db.get_all_books()? {
if let Some(category) = book.category {
println!(
"Found book {}: {} with category {}",
book.id, book.title, category
);
} else {
println!("Book {}: {}", book.id, book.title);
}
}
let book41 = db.get_book_detailed(41)?;
println!("{:?}", book41);
// Start web API server
let db_mutex = Arc::new(Mutex::new(db));

View File

@ -32,7 +32,7 @@ pub struct BookDetailed {
}
#[derive(Debug, Deserialize, Default)]
pub struct UpdateBook {
pub struct ModifyBook {
pub title: Option<String>,
pub subtitle: Option<String>,
pub authors: Option<String>,
@ -57,7 +57,7 @@ macro_rules! option_to_sql {
$(
if let Some(value) = $self.$x.as_ref() {
keys.push_str(&format!("{},", stringify!($x)));
let sql_subcommand = format!("{}=$,", stringify!($x));
let sql_subcommand = format!("{}=?,", stringify!($x));
sql.push_str(&sql_subcommand);
values.push(Value::from(value.to_owned()));
}
@ -69,7 +69,7 @@ macro_rules! option_to_sql {
};
}
impl UpdateBook {
impl ModifyBook {
pub fn to_sql(&self) -> (String, String, Vec<Value>) {
let (keys, sql, values) = option_to_sql![
self,
@ -97,8 +97,11 @@ mod tests {
#[test]
fn test_update_book_struct_to_sql() {
let mut update_book = UpdateBook::default();
assert_eq!(update_book.to_sql(), ("".to_string(), "".to_string(), Vec::new()));
let mut update_book = ModifyBook::default();
assert_eq!(
update_book.to_sql(),
("".to_string(), "".to_string(), Vec::new())
);
update_book.title = Some("new title".to_owned());
update_book.authors = Some("new authors".to_owned());
update_book.pages = Some(42);
@ -106,7 +109,7 @@ mod tests {
update_book.to_sql(),
(
"title,authors,pages".to_owned(),
"title=$,authors=$,pages=$".to_owned(),
"title=?,authors=?,pages=?".to_owned(),
vec![
Value::from("new title".to_owned()),
Value::from("new authors".to_owned()),

72
src/web/book.rs Normal file
View File

@ -0,0 +1,72 @@
use super::AppState;
use crate::{
db::CscLibraryError,
types::{Book, BookDetailed, ModifyBook},
};
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use serde::Deserialize;
pub async fn all_books(
State(state): State<AppState>,
) -> Result<Json<Vec<Book>>, (StatusCode, String)> {
let db = state.db.lock().await;
match db.get_all_books() {
Ok(r) => Ok(Json(r)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
pub async fn book_detail(
State(state): State<AppState>,
Path(book_id): Path<String>,
) -> Result<Json<BookDetailed>, (StatusCode, String)> {
let db = state.db.lock().await;
let book_id: i64 = match book_id.parse() {
Ok(id) => id,
Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())),
};
match db.get_book_detailed(book_id) {
Ok(r) => Ok(Json(r)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
pub async fn add_book(
State(state): State<AppState>,
Json(payload): Json<ModifyBook>,
) -> Result<(), (StatusCode, String)> {
let db = state.db.lock().await;
match db.add_book(&payload) {
Ok(_) => Ok(()),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
pub async fn update_book(
State(state): State<AppState>,
Path(book_id): Path<i64>,
Json(payload): Json<ModifyBook>,
) -> Result<(), (StatusCode, String)> {
let db = state.db.lock().await;
match db.update_book(book_id, &payload) {
Ok(_) => Ok(()),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}
pub async fn del_book(
State(state): State<AppState>,
Path(book_id): Path<i64>,
) -> Result<(), (StatusCode, String)> {
let db = state.db.lock().await;
match db.del_book(book_id) {
Ok(_) => Ok(()),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}
}

View File

@ -3,6 +3,8 @@ use crate::{
types::{Book, BookDetailed},
};
mod book;
use anyhow::Result;
use serde::Serialize;
use std::sync::Arc;
@ -12,20 +14,20 @@ use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
routing::{get, post, put, delete},
Json, Router,
};
#[derive(Clone)]
struct AppState {
pub struct AppState {
db: Arc<Mutex<CscLibraryDb>>,
}
pub async fn start(db: Arc<Mutex<CscLibraryDb>>, listen_addr: &str) -> Result<()> {
let app = Router::new()
.route("/", get(root))
.route("/book", get(all_books))
.route("/book/:id", get(book_detail))
.route("/books", get(book::all_books).post(book::add_book))
.route("/books/:id", get(book::book_detail).put(book::update_book).delete(book::del_book))
.with_state(AppState { db });
axum::Server::bind(&listen_addr.parse()?)
@ -37,34 +39,3 @@ pub async fn start(db: Arc<Mutex<CscLibraryDb>>, listen_addr: &str) -> Result<()
async fn root() -> &'static str {
"Librarian-rs Ready"
}
async fn all_books(
State(state): State<AppState>,
) -> Result<Json<Vec<Book>>, (StatusCode, String)> {
let db = state.db.lock().await;
match db.get_all_books() {
Ok(r) => Ok(Json(r)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
#[axum::debug_handler]
async fn book_detail(
State(state): State<AppState>,
Path(book_id): Path<String>,
) -> Result<Json<BookDetailed>, (StatusCode, String)> {
let db = state.db.lock().await;
let book_id: i64 = match book_id.parse() {
Ok(id) => id,
Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())),
};
match db.get_book_detailed(book_id) {
Ok(r) => Ok(Json(r)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())),
}
}
/*
async fn new_book(Json(book_detailed): BookDetailed) -> Result<(), (StatusCode, String)> {
}
*/