web: hook up POST, PUT and DELETE for books
This commit is contained in:
parent
edb8ddeae3
commit
2776e6e796
27
src/db/db.rs
27
src/db/db.rs
|
@ -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(())
|
||||
}
|
||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -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));
|
||||
|
|
15
src/types.rs
15
src/types.rs
|
@ -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()),
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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)> {
|
||||
}
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue