Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to find next and previous pages. Also improved error handling #2

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
55 changes: 55 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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<SqlxError> for AppError {
fn from(err: SqlxError) -> Self {
match err {
SqlxError::RowNotFound | SqlxError::ColumnNotFound(_) => {
AppError::NotFound
}
_ => {
AppError::InternalServerError
}
}
}
}
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod models;
mod routes;
mod error;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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()),
)
Expand Down
16 changes: 15 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>,
pub translation: Option<String>,
pub books: Option<Vec<String>>,
pub abbreviations: Option<Vec<String>>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct PageIn {
pub book: Option<String>,
pub abbreviation: Option<String>,
pub chapter: i64,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct PageOut {
pub book: String,
pub abbreviation: String,
pub chapter: i64,
}
118 changes: 108 additions & 10 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerseFilter>, app_data: web::Data<AppData>) -> Vec<Verse> {
Expand Down Expand Up @@ -303,16 +304,18 @@ pub async fn get_chaptercount(
pub async fn get_translation_info(
app_data: web::Data<AppData>,
path: web::Path<String>,
) -> HttpResponse {
) -> Result<HttpResponse, AppError> {
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
Expand Down Expand Up @@ -365,7 +368,8 @@ pub async fn get_translation_books(
pub async fn search(search_parameters: web::Json<SearchParameters>, app_data: web::Data<AppData>) -> HttpResponse {
let mut qb: QueryBuilder<Postgres> = 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(");
Expand All @@ -381,3 +385,97 @@ pub async fn search(search_parameters: web::Json<SearchParameters>, 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<PageIn>, app_data: web::Data<AppData>) -> Result<HttpResponse, AppError> {
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<PageOut>;
let next: Option<PageOut>;
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})));

}
Loading