diff --git a/.gitignore b/.gitignore index ea8c4bf..71c4f75 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +*.db +*.db3 +*.env diff --git a/Cargo.toml b/Cargo.toml index 80a0b3a..786f1a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ dotenvy = "0.15.7" maud = { version = "0.27.0", features = ["axum"] } rand = "0.10.0" rusqlite = { version = "0.38.0", features = ["bundled", "chrono", "uuid"] } -serde = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" thiserror = "2.0.18" tokio = { version = "1.49.0", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index 189b68b..30908b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,13 @@ -mod database; +use std::error::Error; -fn main() { +mod database; +mod persons; +mod quotes; +mod tags; +mod users; + +fn main() -> Result<(), Box> { println!("Hello, world!"); + + Ok(()) } diff --git a/src/persons/mod.rs b/src/persons/mod.rs new file mode 100644 index 0000000..b6fd5d9 --- /dev/null +++ b/src/persons/mod.rs @@ -0,0 +1,3 @@ +pub mod names; + +pub struct Person; diff --git a/src/persons/names.rs b/src/persons/names.rs new file mode 100644 index 0000000..a050f41 --- /dev/null +++ b/src/persons/names.rs @@ -0,0 +1 @@ +pub struct Name; diff --git a/src/quotes/lines.rs b/src/quotes/lines.rs new file mode 100644 index 0000000..64cd091 --- /dev/null +++ b/src/quotes/lines.rs @@ -0,0 +1 @@ +pub struct QuoteLine; diff --git a/src/quotes/mod.rs b/src/quotes/mod.rs new file mode 100644 index 0000000..b22580d --- /dev/null +++ b/src/quotes/mod.rs @@ -0,0 +1,3 @@ +pub mod lines; + +pub struct Quote; diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..7fe1c9e --- /dev/null +++ b/src/tags.rs @@ -0,0 +1 @@ +pub struct Tag; diff --git a/src/users/auth.rs b/src/users/auth.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/users/handle.rs b/src/users/handle.rs new file mode 100644 index 0000000..af11473 --- /dev/null +++ b/src/users/handle.rs @@ -0,0 +1,91 @@ +use std::{fmt::Display, ops::Deref, str::FromStr}; + +use rusqlite::{ + Result as RusqliteResult, + types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[serde(into = "String")] +#[serde(try_from = "String")] +pub struct UserHandle(String); + +#[derive(Debug, thiserror::Error, Clone, PartialEq)] +pub enum UserHandleError { + #[error("Handle is too short - must be 3 or more characters.")] + HandleTooShort, + #[error("Handle is too long - must be 16 or less characters.")] + HandleTooLong, + #[error("Handle must consist of ASCII alphanumeric characters only.")] + HandleNonAsciiAlphanumeric, +} + +impl UserHandle { + pub fn new(input: impl AsRef) -> Result { + let s = input.as_ref(); + UserHandle::validate_str(s)?; + Ok(UserHandle(s.to_string())) + } + pub fn validate_str(str: &str) -> Result<(), UserHandleError> { + match str.len() { + ..=2 => return Err(UserHandleError::HandleTooShort), + 17.. => return Err(UserHandleError::HandleTooLong), + _ => (), + }; + if str.bytes().any(|c| !c.is_ascii_alphanumeric()) { + return Err(UserHandleError::HandleNonAsciiAlphanumeric); + } + Ok(()) + } + pub fn as_str(&self) -> &str { + &self.0 + } +} +impl AsRef for UserHandle { + fn as_ref(&self) -> &str { + &self.0 + } +} +impl Deref for UserHandle { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl Display for UserHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl FromStr for UserHandle { + type Err = UserHandleError; + fn from_str(s: &str) -> Result { + Self::validate_str(s)?; + Ok(UserHandle(s.to_string())) + } +} +impl TryFrom for UserHandle { + type Error = UserHandleError; + fn try_from(value: String) -> Result { + Self::validate_str(&value)?; + Ok(UserHandle(value)) + } +} +impl From for String { + fn from(value: UserHandle) -> Self { + value.0 + } +} + +impl ToSql for UserHandle { + fn to_sql(&self) -> RusqliteResult> { + Ok(self.0.to_sql()?) + } +} + +impl FromSql for UserHandle { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + UserHandle::from_str(value.as_str()?).map_err(|e| FromSqlError::Other(Box::new(e))) + } +} diff --git a/src/users/mod.rs b/src/users/mod.rs new file mode 100644 index 0000000..41df237 --- /dev/null +++ b/src/users/mod.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::users::handle::UserHandle; + +pub mod auth; +pub mod handle; +pub mod sessions; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct User { + pub id: Uuid, + pub handle: UserHandle, +} diff --git a/src/users/sessions.rs b/src/users/sessions.rs new file mode 100644 index 0000000..e19ab6e --- /dev/null +++ b/src/users/sessions.rs @@ -0,0 +1 @@ +pub struct Session;