diff --git a/Cargo.lock b/Cargo.lock index a23fae2..9b19eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "thiserror", "tokio", "utoipa", "utoipa-swagger-ui", @@ -2013,18 +2014,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index fd97f36..c528759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ rand = "0.8.5" serde = { version = "1.0.188", features = ["derive"] } serde_json = { version = "1.0.106", features = ["preserve_order"] } sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "postgres", "macros"] } +thiserror = "1.0.50" tokio = { version = "1.32.0", features = ["fs", "rt-multi-thread", "macros"] } utoipa = { version = "4.0.0", features = ["actix_extras", "preserve_order"] } utoipa-swagger-ui = { version = "4.0.0", features = ["actix-web"] } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f4f2558 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,55 @@ +use thiserror::Error; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::HttpResponse; +use sqlx::Error as SqlxError; +use serde_json::json; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("The requested resource was not found in the database")] + NotFound, + + #[error("Internal Server Error")] + InternalServerError, +} + +impl ResponseError for AppError { + fn error_response(&self) -> HttpResponse { + match self { + AppError::NotFound => { + HttpResponse::NotFound().json(json!({ + "message": self.to_string() + })) + } + AppError::InternalServerError => { + HttpResponse::InternalServerError().json(json!({ + "message": self.to_string() + })) + } + } + } + fn status_code(&self) -> StatusCode { + match *self { + AppError::NotFound => { + StatusCode::NOT_FOUND + } + AppError::InternalServerError => { + StatusCode::INTERNAL_SERVER_ERROR + } + } + } +} + +impl From for AppError { + fn from(err: SqlxError) -> Self { + match err { + SqlxError::RowNotFound | SqlxError::ColumnNotFound(_) => { + AppError::NotFound + } + _ => { + AppError::InternalServerError + } + } + } +} diff --git a/src/main.rs b/src/main.rs index bf78517..b92c4d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod models; mod routes; +mod error; #[cfg(test)] mod tests; @@ -31,8 +32,9 @@ pub struct AppData { get_translation_books, get_chaptercount, search, + get_next_page, ), - components(schemas(Hello, TranslationInfo, Verse, Book, Count, SearchParameters)) + components(schemas(Hello, TranslationInfo, Verse, Book, Count, SearchParameters, PageIn, PageOut)) )] struct ApiDoc; @@ -60,6 +62,7 @@ async fn main() -> std::io::Result<()> { .service(routes::get_chaptercount) .service(routes::get_random_verse) .service(routes::search) + .service(routes::get_next_page) .service( SwaggerUi::new("/docs/{_:.*}").url("/api-docs/openapi.json", ApiDoc::openapi()), ) diff --git a/src/models.rs b/src/models.rs index c9c0aba..a28c8a3 100644 --- a/src/models.rs +++ b/src/models.rs @@ -147,8 +147,22 @@ pub struct TranslationSelector { #[derive(Debug, Deserialize, ToSchema)] pub struct SearchParameters { pub search_text: String, - pub match_case: bool, + pub match_case: Option, pub translation: Option, pub books: Option>, pub abbreviations: Option>, } + +#[derive(Debug, Deserialize, ToSchema)] +pub struct PageIn { + pub book: Option, + pub abbreviation: Option, + pub chapter: i64, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct PageOut { + pub book: String, + pub abbreviation: String, + pub chapter: i64, +} diff --git a/src/routes.rs b/src/routes.rs index 896c036..8d83a36 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -5,6 +5,7 @@ use sqlx::{Postgres, QueryBuilder}; use crate::models::*; use crate::AppData; +use crate::error::AppError; #[allow(unused_assignments)] pub async fn query_verses(qp: web::Query, app_data: web::Data) -> Vec { @@ -303,16 +304,18 @@ pub async fn get_chaptercount( pub async fn get_translation_info( app_data: web::Data, path: web::Path, -) -> HttpResponse { +) -> Result { let translation = path.into_inner().to_uppercase(); - let q = sqlx::query_as!(TranslationInfo, r#"SELECT name, l.lname as language, full_name, year, license, description from "Translation" join (select id, name as lname from "Language") l on l.id=language_id WHERE name=$1"#, &translation).fetch_one(&app_data.pool).await; - if q.is_err() { - return HttpResponse::BadRequest().json(json!(format!( - "The requested translation {} is not found on the server", - &translation - ))); - } - return HttpResponse::Ok().json(q.unwrap()); + let q = sqlx::query_as!( + TranslationInfo, + r#" + SELECT name, l.lname AS language, + full_name, year, license, description + FROM "Translation" JOIN + (SELECT id, name AS lname FROM "Language") l + ON l.id=language_id WHERE name=$1"#, + &translation).fetch_one(&app_data.pool).await?; + return Ok(HttpResponse::Ok().json(q)); } /// Get a list of books with respect to the translation @@ -365,7 +368,8 @@ pub async fn get_translation_books( pub async fn search(search_parameters: web::Json, app_data: web::Data) -> HttpResponse { let mut qb: QueryBuilder = QueryBuilder::new(r#" SELECT translation, book, book_name, chapter, verse_number, verse from fulltable where verse "#); - if search_parameters.match_case { + let match_case = search_parameters.match_case.unwrap_or(false); + if match_case { qb.push("like("); } else { qb.push("ilike("); @@ -381,3 +385,97 @@ pub async fn search(search_parameters: web::Json, app_data: we let verses = query.fetch_all(&app_data.pool).await.unwrap(); return HttpResponse::Ok().json(verses); } + +/// Get the previous and next chapter / book to go to +/// +/// The frontend needs to know what page lies before and +/// after a specific chapter. So, instead of making multiple +/// API calls, the information is sent in a separate endpoint +#[utoipa::path( + post, + tag = "Frontend Helper", + path = "/nav", + request_body = PageIn, + responses( + (status = 200, description = "Returns info about the previous and next pages to navigate to", body = PageOut), + (status = 400, description = "Atleast one argument of book or abbreviation is required",), + ), +)] +#[post("/nav")] +pub async fn get_next_page(current_page: web::Json, app_data: web::Data) -> Result { + if current_page.book.is_none() && current_page.abbreviation.is_none() { + return Ok(HttpResponse::BadRequest().json(json!({ + "message": "Either one of book or abbreviation is required" + }))) + } + let previous: Option; + let next: Option; + let book_id; + if let Some(ref x) = current_page.book { + book_id = sqlx::query!( + r#" + SELECT id FROM "Book" WHERE name=$1 + "#, + x).fetch_one(&app_data.pool).await?.id; + } else { + let abbreviation = current_page.abbreviation.clone().unwrap().to_uppercase(); + book_id = sqlx::query!( + r#" + SELECT id FROM "Book" WHERE abbreviation=$1 + "#, + abbreviation).fetch_one(&app_data.pool).await?.id; + } + + if book_id == 1 && current_page.chapter == 1 { + previous = None; + } else { + if current_page.chapter == 1 { + let previous_chapter_count = sqlx::query!( + r#" + SELECT COUNT(*) AS count FROM "Chapter" WHERE book_id=$1 + "#, + book_id-1).fetch_one(&app_data.pool).await?.count.unwrap(); + let previous_book = sqlx::query!( + r#" + SELECT name, abbreviation FROM "Book" WHERE id=$1 + "#, + book_id-1).fetch_one(&app_data.pool).await?; + previous = Some(PageOut {book: previous_book.name, abbreviation: previous_book.abbreviation, chapter: previous_chapter_count}); + } else { + let prev = sqlx::query!( + r#" + SELECT name, abbreviation FROM "Book" WHERE id=$1 + "#, + book_id).fetch_one(&app_data.pool).await?; + previous = Some(PageOut{book: prev.name, abbreviation: prev.abbreviation, chapter: current_page.chapter - 1}); + } + } + + if book_id == 66 && current_page.chapter == 22 { + next = None; + } else { + let current_book_length = sqlx::query!( + r#" + SELECT COUNT(*) FROM "Chapter" WHERE book_id=$1 + "#, + book_id).fetch_one(&app_data.pool).await?.count.unwrap(); + if current_page.chapter == current_book_length { + let next_book = sqlx::query!( + r#" + SELECT name, abbreviation FROM "Book" WHERE id=$1 + "#, + book_id+1).fetch_one(&app_data.pool).await?; + next = Some(PageOut{book: next_book.name, abbreviation: next_book.abbreviation, chapter: 1}) + } else { + let bo = sqlx::query!( + r#" + SELECT name, abbreviation FROM "Book" WHERE id=$1 + "#, + book_id).fetch_one(&app_data.pool).await?; + next = Some(PageOut{book: bo.name, abbreviation: bo.abbreviation, chapter: current_page.chapter + 1}); + } + } + + return Ok(HttpResponse::Ok().json(json!({"previous": previous, "next":next}))); + +}