176 lines
4.9 KiB
Rust
176 lines
4.9 KiB
Rust
use axum::{
|
|
extract::{Form, Path, Query, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::{de, Deserialize, Deserializer};
|
|
use sqlx::{query_as, SqlitePool};
|
|
use uuid::Uuid;
|
|
|
|
use super::templates::{GetSearchWatches, GetWatch};
|
|
use crate::{AuthContext, GetWatches, ShowKind, Watch};
|
|
|
|
//-************************************************************************
|
|
// Constants
|
|
//-************************************************************************
|
|
|
|
const GET_SAVED_WATCHES_QUERY: &str =
|
|
"select * from watches left join witch_watch on $1 = witch_watch.witch and watches.id = witch_watch.watch";
|
|
|
|
const GET_WATCH_QUERY: &str = "select * from watches where id = $1";
|
|
|
|
const EMPTY_SEARCH_QUERY: SearchQuery = SearchQuery {
|
|
title: None,
|
|
kind: None,
|
|
year: None,
|
|
search: None,
|
|
};
|
|
|
|
//-************************************************************************
|
|
// Error types for Watch creation
|
|
//-************************************************************************
|
|
|
|
#[Error]
|
|
pub struct WatchAddError(#[from] WatchAddErrorKind);
|
|
|
|
#[Error]
|
|
#[non_exhaustive]
|
|
pub enum WatchAddErrorKind {
|
|
UnknownDBError,
|
|
}
|
|
|
|
impl IntoResponse for WatchAddError {
|
|
fn into_response(self) -> Response {
|
|
match self.0 {
|
|
WatchAddErrorKind::UnknownDBError => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-************************************************************************
|
|
// Types for receiving arguments from search queries
|
|
//-************************************************************************
|
|
|
|
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
|
|
pub struct SearchQuery {
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub search: Option<String>,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub title: Option<String>,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub kind: Option<String>,
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub year: Option<i64>,
|
|
}
|
|
|
|
// kinda the main form?
|
|
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
|
pub struct PostAddWatch {
|
|
pub id: Option<String>, // maybe this already exists
|
|
pub title: String,
|
|
pub kind: Option<ShowKind>,
|
|
pub release_date: Option<String>, // need a date-picker or something
|
|
pub metadata_url: Option<String>,
|
|
}
|
|
|
|
//-************************************************************************
|
|
// handlers
|
|
//-************************************************************************
|
|
|
|
/// Add a Watch to your watchlist (side effects system-add if missing)
|
|
pub async fn post_add_watch(
|
|
auth: AuthContext,
|
|
State(pool): State<SqlitePool>,
|
|
Form(form): Form<PostAddWatch>,
|
|
) -> impl IntoResponse {
|
|
}
|
|
|
|
/// A single Watch
|
|
pub async fn get_watch(
|
|
auth: AuthContext,
|
|
watch: Option<Path<String>>,
|
|
State(pool): State<SqlitePool>,
|
|
) -> impl IntoResponse {
|
|
let id = if let Some(Path(id)) = watch {
|
|
id
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
let id = id.trim();
|
|
let id = Uuid::try_parse(id).unwrap_or_default();
|
|
let watch: Option<Watch> = query_as(GET_WATCH_QUERY)
|
|
.bind(id)
|
|
.fetch_one(&pool)
|
|
.await
|
|
.ok();
|
|
|
|
GetWatch {
|
|
watch,
|
|
user: auth.current_user,
|
|
}
|
|
}
|
|
|
|
/// everything the user has saved
|
|
pub async fn get_watches(auth: AuthContext, State(pool): State<SqlitePool>) -> impl IntoResponse {
|
|
let user = auth.current_user;
|
|
let watches: Vec<Watch> = if (user).is_some() {
|
|
query_as(GET_SAVED_WATCHES_QUERY)
|
|
.bind(user.as_ref().unwrap().id)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap_or_default()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
GetWatches { watches, user }
|
|
}
|
|
|
|
pub async fn get_search_watch(
|
|
auth: AuthContext,
|
|
State(pool): State<SqlitePool>,
|
|
search: Query<SearchQuery>,
|
|
) -> impl IntoResponse {
|
|
let user = auth.current_user;
|
|
|
|
let search = if search.0 != EMPTY_SEARCH_QUERY {
|
|
let s = search.0;
|
|
format!("{s:?}")
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
// until tantivy search
|
|
let watches: Vec<Watch> = query_as("select * from watches")
|
|
.fetch_all(&pool)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
GetSearchWatches {
|
|
watches,
|
|
user,
|
|
search,
|
|
}
|
|
}
|
|
|
|
//-************************************************************************
|
|
// helper fns
|
|
//-************************************************************************
|
|
|
|
/// Serde deserialization decorator to map empty Strings to None,
|
|
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
T: std::str::FromStr,
|
|
T::Err: std::fmt::Display,
|
|
{
|
|
let opt = Option::<String>::deserialize(de)?;
|
|
match opt.as_deref() {
|
|
None | Some("") => Ok(None),
|
|
Some(s) => std::str::FromStr::from_str(s)
|
|
.map_err(de::Error::custom)
|
|
.map(Some),
|
|
}
|
|
}
|