From 944184ab852928b2fd679add8ab767a9b460ded5 Mon Sep 17 00:00:00 2001 From: Berin Aniesh Date: Fri, 3 Nov 2023 08:38:21 +0530 Subject: [PATCH 1/2] start functionality to find the next page --- src/main.rs | 4 +++- src/models.rs | 17 ++++++++++++- src/routes.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index bf78517..d9d3ec2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,8 +31,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, CurrentPage, NextPage)) )] struct ApiDoc; @@ -60,6 +61,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..b1899a9 100644 --- a/src/models.rs +++ b/src/models.rs @@ -147,8 +147,23 @@ 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 CurrentPage { + pub book: Option, + pub abbreviation: Option, + pub chapter: i64, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct NextPage { + pub book: String, + pub abbreviation: String, + pub chapter: i32, + pub bible_ended: bool, +} diff --git a/src/routes.rs b/src/routes.rs index 896c036..81aa477 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -365,7 +365,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 +382,66 @@ 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 next chapter / book to go to +/// +/// The frontend needs to know what page to go to once +/// a user finishes reading one chapter and since the frontend +/// doesn't have access to the database and it needs a few calls to +/// the API to figure it out, it'd be nice to have the API give +/// the information directly. +#[utoipa::path( + post, + tag = "Frontend Helper", + path = "/next", + request_body = CurrentPage, + responses( + (status = 200, description = "Returns info about the next page to navigate to", body = NextPage), + (status = 400, description = "Atleast one argument of book or abbreviation is required",), + ), +)] +#[post("/next")] +pub async fn get_next_page(current_page: web::Json, app_data: web::Data) -> HttpResponse { + if current_page.book.is_none() && current_page.abbreviation.is_none() { + return HttpResponse::BadRequest().json(json!({ + "message": "Either one of book or abbreviation is required" + })) + } + let mut is_revelation = false; + if current_page.book.is_some() { + let book = current_page.book.clone().unwrap(); + if book == "Revelation" { + is_revelation = true; + } + } + if current_page.abbreviation.is_some() { + let abbreviation = current_page.abbreviation.clone().unwrap().to_uppercase(); + if abbreviation == "REV" { + is_revelation = true; + } + } + if is_revelation && current_page.chapter == 22 { + let next_page = NextPage{book: "Genesis".to_string(), abbreviation: "GEN".to_string(), chapter: 1, bible_ended: true}; + return HttpResponse::Ok().json(next_page); + } + let mut query_builder: QueryBuilder = QueryBuilder::new( + r#"SELECT COUNT(*) AS count FROM "Chapter" WHERE book_id=(SELECT id FROM "Book" WHERE"#, + ); + if current_page.book.is_some() { + let book = current_page.book.clone().unwrap(); + query_builder.push(" name="); + query_builder.push_bind(book); + query_builder.push(")"); + } else { + let abbreviation = current_page.abbreviation.clone().unwrap(); + query_builder.push(" abbreviation="); + query_builder.push_bind(abbreviation); + query_builder.push(")"); + } + let query = query_builder.build_query_as::(); + let current_book_chapter_count = query.fetch_one(&app_data.pool).await.unwrap().count; + if current_page.chapter < current_book_chapter_count { +// let next_page = NextPage {} + } + return HttpResponse::Ok().json("hello"); +} From 578b7c71da7225059530a4b1b271d13f77b40ca0 Mon Sep 17 00:00:00 2001 From: Berin Aniesh Date: Fri, 3 Nov 2023 12:07:15 +0530 Subject: [PATCH 2/2] Navigation complete Also improved error handling --- Cargo.lock | 9 ++-- Cargo.toml | 1 + src/error.rs | 55 +++++++++++++++++++ src/main.rs | 3 +- src/models.rs | 9 ++-- src/routes.rs | 144 +++++++++++++++++++++++++++++++------------------- 6 files changed, 156 insertions(+), 65 deletions(-) create mode 100644 src/error.rs 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 d9d3ec2..b92c4d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod models; mod routes; +mod error; #[cfg(test)] mod tests; @@ -33,7 +34,7 @@ pub struct AppData { search, get_next_page, ), - components(schemas(Hello, TranslationInfo, Verse, Book, Count, SearchParameters, CurrentPage, NextPage)) + components(schemas(Hello, TranslationInfo, Verse, Book, Count, SearchParameters, PageIn, PageOut)) )] struct ApiDoc; diff --git a/src/models.rs b/src/models.rs index b1899a9..a28c8a3 100644 --- a/src/models.rs +++ b/src/models.rs @@ -154,16 +154,15 @@ pub struct SearchParameters { } #[derive(Debug, Deserialize, ToSchema)] -pub struct CurrentPage { +pub struct PageIn { pub book: Option, - pub abbreviation: Option, + pub abbreviation: Option, pub chapter: i64, } #[derive(Debug, Serialize, ToSchema)] -pub struct NextPage { +pub struct PageOut { pub book: String, pub abbreviation: String, - pub chapter: i32, - pub bible_ended: bool, + pub chapter: i64, } diff --git a/src/routes.rs b/src/routes.rs index 81aa477..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 @@ -383,65 +386,96 @@ pub async fn search(search_parameters: web::Json, app_data: we return HttpResponse::Ok().json(verses); } -/// Get the next chapter / book to go to +/// Get the previous and next chapter / book to go to /// -/// The frontend needs to know what page to go to once -/// a user finishes reading one chapter and since the frontend -/// doesn't have access to the database and it needs a few calls to -/// the API to figure it out, it'd be nice to have the API give -/// the information directly. +/// 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 = "/next", - request_body = CurrentPage, + path = "/nav", + request_body = PageIn, responses( - (status = 200, description = "Returns info about the next page to navigate to", body = NextPage), + (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("/next")] -pub async fn get_next_page(current_page: web::Json, app_data: web::Data) -> HttpResponse { +#[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 HttpResponse::BadRequest().json(json!({ + return Ok(HttpResponse::BadRequest().json(json!({ "message": "Either one of book or abbreviation is required" - })) - } - let mut is_revelation = false; - if current_page.book.is_some() { - let book = current_page.book.clone().unwrap(); - if book == "Revelation" { - is_revelation = true; - } - } - if current_page.abbreviation.is_some() { + }))) + } + 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(); - if abbreviation == "REV" { - is_revelation = true; - } - } - if is_revelation && current_page.chapter == 22 { - let next_page = NextPage{book: "Genesis".to_string(), abbreviation: "GEN".to_string(), chapter: 1, bible_ended: true}; - return HttpResponse::Ok().json(next_page); + book_id = sqlx::query!( + r#" + SELECT id FROM "Book" WHERE abbreviation=$1 + "#, + abbreviation).fetch_one(&app_data.pool).await?.id; } - let mut query_builder: QueryBuilder = QueryBuilder::new( - r#"SELECT COUNT(*) AS count FROM "Chapter" WHERE book_id=(SELECT id FROM "Book" WHERE"#, - ); - if current_page.book.is_some() { - let book = current_page.book.clone().unwrap(); - query_builder.push(" name="); - query_builder.push_bind(book); - query_builder.push(")"); + + if book_id == 1 && current_page.chapter == 1 { + previous = None; } else { - let abbreviation = current_page.abbreviation.clone().unwrap(); - query_builder.push(" abbreviation="); - query_builder.push_bind(abbreviation); - query_builder.push(")"); + 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}); + } } - let query = query_builder.build_query_as::(); - let current_book_chapter_count = query.fetch_one(&app_data.pool).await.unwrap().count; - if current_page.chapter < current_book_chapter_count { -// let next_page = NextPage {} + + 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 HttpResponse::Ok().json("hello"); + + return Ok(HttpResponse::Ok().json(json!({"previous": previous, "next":next}))); + }