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, #[serde(default, deserialize_with = "empty_string_as_none")] pub title: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub kind: Option, #[serde(default, deserialize_with = "empty_string_as_none")] pub year: Option, } // kinda the main form? #[derive(Debug, Default, Deserialize, PartialEq, Eq)] pub struct PostAddWatch { pub id: Option, // maybe this already exists pub title: String, pub kind: Option, pub release_date: Option, // need a date-picker or something pub metadata_url: Option, } //-************************************************************************ // handlers //-************************************************************************ /// Add a Watch to your watchlist (side effects system-add if missing) pub async fn post_add_watch( auth: AuthContext, State(pool): State, Form(form): Form, ) -> impl IntoResponse { } /// A single Watch pub async fn get_watch( auth: AuthContext, watch: Option>, State(pool): State, ) -> 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 = 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) -> impl IntoResponse { let user = auth.current_user; let watches: Vec = 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, search: Query, ) -> 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 = 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, D::Error> where D: Deserializer<'de>, T: std::str::FromStr, T::Err: std::fmt::Display, { let opt = Option::::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), } }