use std::fmt::{Display, Formatter};
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AccountGroupId(u32);
pub const DEFAULT_ACCOUNT_GROUP: AccountGroupId = AccountGroupId(0);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AccountGroupIdError {
Empty,
Reserved,
}
impl Display for AccountGroupIdError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => formatter.write_str("account group id string must not be empty"),
Self::Reserved => {
formatter.write_str("account group id must not equal the reserved default group")
}
}
}
}
impl std::error::Error for AccountGroupIdError {}
impl AccountGroupId {
pub const DEFAULT: Self = DEFAULT_ACCOUNT_GROUP;
pub const fn from_u32(value: u32) -> Result<Self, AccountGroupIdError> {
if value == DEFAULT_ACCOUNT_GROUP.0 {
return Err(AccountGroupIdError::Reserved);
}
Ok(Self(value))
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(value: impl AsRef<str>) -> Result<Self, AccountGroupIdError> {
let value = value.as_ref();
if value.trim().is_empty() {
return Err(AccountGroupIdError::Empty);
}
Ok(Self(non_default_hash(fnv1a_32(value))))
}
pub const fn as_u32(self) -> u32 {
self.0
}
}
impl Display for AccountGroupId {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, formatter)
}
}
fn fnv1a_32(s: &str) -> u32 {
const OFFSET_BASIS: u32 = 2_166_136_261;
const PRIME: u32 = 16_777_619;
let mut hash = OFFSET_BASIS;
for byte in s.bytes() {
hash ^= u32::from(byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
const fn non_default_hash(hash: u32) -> u32 {
if hash == DEFAULT_ACCOUNT_GROUP.0 {
1
} else {
hash
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{non_default_hash, AccountGroupId, AccountGroupIdError, DEFAULT_ACCOUNT_GROUP};
fn group(value: u32) -> AccountGroupId {
AccountGroupId::from_u32(value).expect("account group id must be valid")
}
#[test]
fn from_u32_display_shows_integer() {
assert_eq!(group(7).to_string(), "7");
assert_eq!(group(42).to_string(), "42");
assert_eq!(group(u32::MAX).to_string(), u32::MAX.to_string());
}
#[test]
fn from_u32_equality() {
assert_eq!(group(7), group(7));
assert_ne!(group(7), group(8));
}
#[test]
fn from_u32_rejects_reserved_default() {
assert_eq!(
AccountGroupId::from_u32(0),
Err(AccountGroupIdError::Reserved)
);
}
#[test]
fn default_account_group_value_is_zero() {
assert_eq!(DEFAULT_ACCOUNT_GROUP.as_u32(), 0);
assert_eq!(AccountGroupId::DEFAULT, DEFAULT_ACCOUNT_GROUP);
}
#[test]
fn from_str_same_string_equal() {
assert_eq!(
AccountGroupId::from_str("group-a"),
AccountGroupId::from_str("group-a")
);
}
#[test]
fn from_str_different_strings_not_equal() {
assert_ne!(
AccountGroupId::from_str("group-a"),
AccountGroupId::from_str("group-b")
);
}
#[test]
fn from_u32_and_from_str_of_same_numeric_string_differ() {
assert_ne!(
group(42),
AccountGroupId::from_str("42").expect("account group id must be valid")
);
}
#[test]
fn hashmap_lookup_with_from_u32() {
let mut map: HashMap<AccountGroupId, &str> = HashMap::new();
map.insert(group(100), "alpha");
assert_eq!(map[&group(100)], "alpha");
}
#[test]
fn hashmap_lookup_with_from_str() {
let mut map: HashMap<AccountGroupId, &str> = HashMap::new();
map.insert(
AccountGroupId::from_str("beta").expect("account group id must be valid"),
"beta-value",
);
assert_eq!(
map[&AccountGroupId::from_str("beta").expect("account group id must be valid")],
"beta-value"
);
}
#[test]
fn from_str_rejects_empty_or_whitespace() {
assert_eq!(
AccountGroupId::from_str(""),
Err(AccountGroupIdError::Empty)
);
assert_eq!(
AccountGroupId::from_str(" "),
Err(AccountGroupIdError::Empty)
);
}
#[test]
fn from_str_never_yields_reserved_default() {
for raw in ["", "a", "desk-emea", "0", "42", " spaced "] {
if let Ok(id) = AccountGroupId::from_str(raw) {
assert_ne!(id, DEFAULT_ACCOUNT_GROUP);
}
}
}
#[test]
fn non_default_hash_remaps_reserved_value_only() {
assert_eq!(non_default_hash(0), 1);
assert_eq!(non_default_hash(1), 1);
assert_eq!(non_default_hash(42), 42);
assert_eq!(non_default_hash(u32::MAX), u32::MAX);
}
#[test]
fn account_group_id_error_display_is_stable() {
assert_eq!(
AccountGroupIdError::Empty.to_string(),
"account group id string must not be empty"
);
assert_eq!(
AccountGroupIdError::Reserved.to_string(),
"account group id must not equal the reserved default group"
);
}
#[test]
fn as_u32_returns_inner_value() {
assert_eq!(group(99).as_u32(), 99);
assert_eq!(group(7).as_u32(), 7);
assert_eq!(group(u32::MAX).as_u32(), u32::MAX);
}
#[test]
fn from_str_is_deterministic() {
let first = AccountGroupId::from_str("deterministic")
.expect("account group id must be valid")
.as_u32();
let second = AccountGroupId::from_str("deterministic")
.expect("account group id must be valid")
.as_u32();
assert_eq!(first, second);
}
}