diff --git a/src/constants.rs b/src/constants.rs index 3a9f051..57da86e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -74,11 +74,23 @@ pub const BOOKS: [&'static str; 66] = [ "Revelation", ]; -pub const ABBREVIATIONS: [&'static str; 66] = ["GEN","EXO","LEV","NUM","DEU","JOS","JDG","RUT","1SA","2SA","1KI","2KI","1CH","2CH","EZR","NEH","EST","JOB","PSA","PRO","ECC","SNG","ISA","JER","LAM","EZK","DAN","HOS","JOL","AMO","OBA","JON","MIC","NAM","HAB","ZEP","HAG","ZEC","MAL","MAT","MRK","LUK","JHN","ACT","ROM","1CO","2CO","GAL","EPH","PHP","COL","1TH","2TH","1TI","2TI","TIT","PHM","HEB","JAM","1PE","2PE","1JN","2JN","3JN","JUD","REV"]; +pub const ABBREVIATIONS: [&'static str; 66] = [ + "GEN", "EXO", "LEV", "NUM", "DEU", "JOS", "JDG", "RUT", "1SA", "2SA", "1KI", "2KI", "1CH", + "2CH", "EZR", "NEH", "EST", "JOB", "PSA", "PRO", "ECC", "SNG", "ISA", "JER", "LAM", "EZK", + "DAN", "HOS", "JOL", "AMO", "OBA", "JON", "MIC", "NAM", "HAB", "ZEP", "HAG", "ZEC", "MAL", + "MAT", "MRK", "LUK", "JHN", "ACT", "ROM", "1CO", "2CO", "GAL", "EPH", "PHP", "COL", "1TH", + "2TH", "1TI", "2TI", "TIT", "PHM", "HEB", "JAM", "1PE", "2PE", "1JN", "2JN", "3JN", "JUD", + "REV", +]; pub const CHAPTERCOUNT: [u8; 66] = [ 50, 40, 27, 36, 34, 24, 21, 4, 31, 24, 22, 25, 29, 36, 10, 13, 10, 42, 150, 31, 12, 8, 66, 52, 5, 48, 12, 14, 3, 9, 1, 4, 7, 3, 3, 3, 2, 14, 4, 28, 16, 24, 21, 28, 16, 16, 13, 6, 6, 4, 4, 5, 3, 6, 4, 3, 1, 13, 5, 5, 3, 5, 1, 1, 1, 22, ]; -pub const VERSECOUNT: [u16; 66] = [1533, 1213, 859, 1288, 959, 658, 618, 85, 810, 695, 816, 719, 942, 822, 280, 406, 167, 1070, 2461, 915, 222, 117, 1292, 1364, 154, 1273, 357, 197, 73, 146, 21, 48, 105, 47, 56, 53, 38, 211, 55, 1071, 678, 1151, 879, 1007, 433, 437, 257, 149, 155, 104, 95, 89, 47, 113, 83, 46, 25, 303, 108, 105, 61, 105, 13, 14, 25, 404]; +pub const VERSECOUNT: [u16; 66] = [ + 1533, 1213, 859, 1288, 959, 658, 618, 85, 810, 695, 816, 719, 942, 822, 280, 406, 167, 1070, + 2461, 915, 222, 117, 1292, 1364, 154, 1273, 357, 197, 73, 146, 21, 48, 105, 47, 56, 53, 38, + 211, 55, 1071, 678, 1151, 879, 1007, 433, 437, 257, 149, 155, 104, 95, 89, 47, 113, 83, 46, 25, + 303, 108, 105, 61, 105, 13, 14, 25, 404, +]; diff --git a/src/error.rs b/src/error.rs index f4f2558..97af886 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,9 @@ -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; +use sqlx::Error as SqlxError; +use thiserror::Error; #[derive(Debug, Error)] pub enum AppError { @@ -17,26 +17,18 @@ pub enum AppError { 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() - })) - } + 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 - } + AppError::NotFound => StatusCode::NOT_FOUND, + AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, } } } @@ -44,12 +36,8 @@ impl ResponseError for AppError { impl From for AppError { fn from(err: SqlxError) -> Self { match err { - SqlxError::RowNotFound | SqlxError::ColumnNotFound(_) => { - AppError::NotFound - } - _ => { - AppError::InternalServerError - } + SqlxError::RowNotFound | SqlxError::ColumnNotFound(_) => AppError::NotFound, + _ => AppError::InternalServerError, } } } diff --git a/src/main.rs b/src/main.rs index d616e56..54ef3f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ +mod constants; +mod error; mod models; mod routes; -mod error; -mod constants; #[cfg(test)] mod tests; @@ -37,7 +37,20 @@ pub struct AppData { search, get_next_page, ), - components(schemas(Hello, TranslationInfo, Verse, VerseFilter, TranslationSelector, Book, Count, SearchParameters, PageIn, PageOut, PrevNext, BooksChapterCount)) + components(schemas( + Hello, + TranslationInfo, + Verse, + VerseFilter, + TranslationSelector, + Book, + Count, + SearchParameters, + PageIn, + PageOut, + PrevNext, + BooksChapterCount + )) )] struct ApiDoc; diff --git a/src/models.rs b/src/models.rs index c2df8e4..d393b5b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,9 +1,9 @@ -use serde::{Serialize, Deserialize}; +use crate::constants::{ABBREVIATIONS, BOOKS, CHAPTERCOUNT, TOTAL, VERSECOUNT}; +use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Type}; -use std::fmt; use std::env; -use utoipa::{ToSchema, IntoParams}; -use crate::constants::{TOTAL, BOOKS, ABBREVIATIONS, CHAPTERCOUNT, VERSECOUNT}; +use std::fmt; +use utoipa::{IntoParams, ToSchema}; #[derive(Debug, Deserialize, Serialize, Clone, Copy, Type)] #[sqlx(type_name = "testament")] @@ -18,7 +18,7 @@ impl fmt::Display for Testament { Self::OldTestament => { //fmt::Debug::fmt("Old", f) write!(f, "Old Testament") - }, + } Self::NewTestament => { //fmt::Debug::fmt("New", f) write!(f, "New Testament") @@ -171,8 +171,6 @@ pub struct PrevNext { pub next: Option, } - - #[derive(Debug, Serialize, ToSchema)] pub struct BooksChapterCount { pub book: String, @@ -185,7 +183,12 @@ impl BooksChapterCount { pub fn default() -> Vec { let mut ans: Vec = Vec::with_capacity(TOTAL as usize); for i in 0..TOTAL as usize { - let temp = BooksChapterCount {book: String::from(BOOKS[i]), abbreviation: String::from(ABBREVIATIONS[i]), chapters: CHAPTERCOUNT[i], verses: VERSECOUNT[i]}; + let temp = BooksChapterCount { + book: String::from(BOOKS[i]), + abbreviation: String::from(ABBREVIATIONS[i]), + chapters: CHAPTERCOUNT[i], + verses: VERSECOUNT[i], + }; ans.push(temp) } return ans; diff --git a/src/routes.rs b/src/routes.rs index 2fcbb38..23b51a4 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -3,9 +3,9 @@ use rand::{thread_rng, Rng}; use serde_json::json; use sqlx::{Postgres, QueryBuilder}; +use crate::error::AppError; 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 { @@ -138,7 +138,6 @@ pub async fn get_translations(app_data: web::Data) -> HttpResponse { return HttpResponse::Ok().json(q); } - /// Get a list of Bible books #[utoipa::path( get, @@ -337,12 +336,15 @@ pub async fn get_translation_info( 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?; + &translation + ) + .fetch_one(&app_data.pool) + .await?; return Ok(HttpResponse::Ok().json(q)); } /// Get a list of books with respect to the translation -/// +/// /// The name of the book in the translation language, etc #[utoipa::path( get, @@ -358,7 +360,9 @@ pub async fn get_translation_books( path: web::Path, ) -> HttpResponse { let translation = path.into_inner().to_uppercase(); - let q = sqlx::query_as!(Book, r#" + let q = sqlx::query_as!( + Book, + r#" SELECT b.id book_id, b.abbreviation abbreviation, tb.name book_name, b.name book, b.testament as "testament: Testament", tn.name testament_name from "Book" b @@ -367,7 +371,12 @@ pub async fn get_translation_books( join "TranslationBookName" tb on tb.translation_id=t.id and b.id=tb.book_id where t.name=$1 order by b.id - "#, &translation).fetch_all(&app_data.pool).await.unwrap(); + "#, + &translation + ) + .fetch_all(&app_data.pool) + .await + .unwrap(); if q.is_empty() { return HttpResponse::BadRequest().json(json!(format!( "The requested translation {} is not found on the server", @@ -378,6 +387,8 @@ pub async fn get_translation_books( } /// Get verses based on text search +/// +/// If the length of the search text is less than 3, an empty array is returned. (Not errored as the frontend does not have good error handling). #[utoipa::path( post, tag = "Verse", @@ -388,9 +399,17 @@ pub async fn get_translation_books( ), )] #[post("/search")] -pub async fn search(search_parameters: web::Json, app_data: web::Data) -> HttpResponse { - let mut qb: QueryBuilder = QueryBuilder::new(r#" - SELECT translation, book, abbreviation, book_name, chapter, verse_number, verse from fulltable where verse "#); +pub async fn search( + search_parameters: web::Json, + app_data: web::Data, +) -> HttpResponse { + if search_parameters.search_text.len() < 3 { + return HttpResponse::Ok().json(Vec::::new()); + } + let mut qb: QueryBuilder = QueryBuilder::new( + r#" + SELECT translation, book, abbreviation, book_name, chapter, verse_number, verse from fulltable where verse "#, + ); let match_case = search_parameters.match_case.unwrap_or(false); let whole_words = search_parameters.whole_words.unwrap_or(false); if whole_words { @@ -441,11 +460,14 @@ pub async fn search(search_parameters: web::Json, app_data: we ), )] #[post("/nav")] -pub async fn get_next_page(current_page: web::Json, app_data: web::Data) -> Result { +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; @@ -455,14 +477,22 @@ pub async fn get_next_page(current_page: web::Json, app_data: web::Data< r#" SELECT id FROM "Book" WHERE name=$1 "#, - x).fetch_one(&app_data.pool).await?.id; + 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; + abbreviation + ) + .fetch_one(&app_data.pool) + .await? + .id; } if current_page.chapter == 0 { @@ -473,8 +503,15 @@ pub async fn get_next_page(current_page: web::Json, app_data: web::Data< r#" SELECT name, abbreviation FROM "Book" where id=$1 "#, - book_id-1).fetch_one(&app_data.pool).await?; - previous = Some(PageOut {book: p.name, abbreviation: p.abbreviation, chapter: 0}); + book_id - 1 + ) + .fetch_one(&app_data.pool) + .await?; + previous = Some(PageOut { + book: p.name, + abbreviation: p.abbreviation, + chapter: 0, + }); } if book_id == 66 { next = None @@ -483,11 +520,18 @@ pub async fn get_next_page(current_page: web::Json, app_data: web::Data< r#" SELECT name, abbreviation FROM "Book" WHERE id=$1 "#, - book_id+1).fetch_one(&app_data.pool).await?; - next = Some(PageOut{book: n.name, abbreviation: n.abbreviation, chapter: 0}); + book_id + 1 + ) + .fetch_one(&app_data.pool) + .await?; + next = Some(PageOut { + book: n.name, + abbreviation: n.abbreviation, + chapter: 0, + }); } - let prev_next = PrevNext {previous, next}; - return Ok(HttpResponse::Ok().json(prev_next)) + let prev_next = PrevNext { previous, next }; + return Ok(HttpResponse::Ok().json(prev_next)); } if book_id == 1 && current_page.chapter == 1 { @@ -498,49 +542,86 @@ pub async fn get_next_page(current_page: web::Json, app_data: web::Data< r#" SELECT COUNT(*) AS count FROM "Chapter" WHERE book_id=$1 "#, - book_id-1).fetch_one(&app_data.pool).await?.count.unwrap(); + 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}); + 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}); + 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; + 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(); + 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}) + 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}); + book_id + ) + .fetch_one(&app_data.pool) + .await?; + next = Some(PageOut { + book: bo.name, + abbreviation: bo.abbreviation, + chapter: current_page.chapter + 1, + }); } } - let prev_next = PrevNext {previous, next}; + let prev_next = PrevNext { previous, next }; return Ok(HttpResponse::Ok().json(prev_next)); - } diff --git a/src/tests.rs b/src/tests.rs index 52083b7..a0b88ce 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,4 @@ -use actix_web::{test, App, http::header::ContentType}; +use actix_web::{http::header::ContentType, test, App}; use crate::routes::home; @@ -10,4 +10,4 @@ async fn test_index_get() { .to_request(); let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); -} \ No newline at end of file +}