use derive_more::Display;
use snafu::Snafu;
use crate::utils::ExampleData;
#[derive(Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
feature = "diesel",
derive(
opentalk_diesel_newtype::DieselNewtype,
diesel::expression::AsExpression,
diesel::deserialize::FromSqlRow
)
)]
#[cfg_attr(feature="diesel",
diesel(sql_type = diesel::sql_types::VarChar),
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(example = json!(RoomSuffix::example_data())))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RoomSuffix(String);
impl RoomSuffix {
#[cfg(feature = "rand")]
pub fn generate(length: u8) -> Self {
use rand::seq::IndexedRandom;
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::rng();
let code = (0..length)
.map(|_| *CHARSET.choose(&mut rng).expect("charset is never empty") as char)
.collect();
Self(code)
}
pub fn nil(length: u8) -> Self {
Self("0".repeat(length as usize))
}
pub fn char_count(&self) -> u8 {
self.0.chars().count() as u8
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
#[derive(Debug, Snafu, PartialEq, Eq, Clone, Copy)]
#[snafu(display("Room suffix contains invalid characters"))]
pub struct InvalidCharacters;
impl TryFrom<String> for RoomSuffix {
type Error = InvalidCharacters;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9')) {
Ok(Self(value))
} else {
Err(InvalidCharacters)
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for RoomSuffix {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
String::deserialize(deserializer)?
.try_into()
.map_err(D::Error::custom)
}
}
impl ExampleData for RoomSuffix {
fn example_data() -> Self {
const EXAMPLE_ROOM_SUFFIX_LENGTH: u8 = 16;
Self::nil(EXAMPLE_ROOM_SUFFIX_LENGTH)
}
}
#[cfg(test)]
mod tests {
use super::{InvalidCharacters, RoomSuffix};
#[test]
fn invalid_character_is_rejected() {
let result = RoomSuffix::try_from("abc!".to_string());
assert_eq!(result, Err(InvalidCharacters));
}
#[test]
fn upper_case_is_rejected() {
let result = RoomSuffix::try_from("ABC".to_string());
assert_eq!(result, Err(InvalidCharacters));
}
#[test]
fn valid_suffix_is_accepted() {
let value = "abc123".to_string();
let result = RoomSuffix::try_from(value.clone());
assert_eq!(result.map(RoomSuffix::into_inner), Ok(value));
}
}
#[cfg(all(test, feature = "rand"))]
mod rand_tests {
use super::RoomSuffix;
#[test]
fn generate_creates_suffix_with_correct_length() {
let suffix = RoomSuffix::generate(10);
assert_eq!(suffix.char_count(), 10);
}
#[test]
fn generate_uses_only_valid_characters() {
let suffix = RoomSuffix::generate(100);
assert!(
suffix
.as_str()
.chars()
.all(|c| matches!(c, 'a'..='z' | '0'..='9'))
);
}
#[test]
fn generate_produces_different_results() {
let a = RoomSuffix::generate(32);
let b = RoomSuffix::generate(32);
assert_ne!(a, b);
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use super::RoomSuffix;
use crate::utils::ExampleData as _;
#[test]
fn serialize() {
assert_eq!(
serde_json::to_value(RoomSuffix::example_data()).unwrap(),
json!("0000000000000000")
);
}
#[test]
fn deserialize() {
assert_eq!(
serde_json::from_value::<RoomSuffix>(json!("0000000000000000")).unwrap(),
RoomSuffix::example_data()
);
}
#[test]
fn deserialize_rejects_invalid_characters() {
assert!(serde_json::from_value::<RoomSuffix>(json!("ABC!")).is_err());
}
}