use std::fmt;
use serde::{Deserialize, Serialize};
const MAX_LENGTH: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConnectionNameError {
#[error("Connection name cannot be empty")]
Empty,
#[error("Connection name too long: {len} chars (max {max})")]
TooLong { len: usize, max: usize },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConnectionName(String);
impl ConnectionName {
pub fn new(name: impl Into<String>) -> Result<Self, ConnectionNameError> {
let name = name.into();
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(ConnectionNameError::Empty);
}
let char_count = trimmed.chars().count();
if char_count > MAX_LENGTH {
return Err(ConnectionNameError::TooLong {
len: char_count,
max: MAX_LENGTH,
});
}
Ok(Self(trimmed.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn normalized(&self) -> String {
self.0.to_lowercase()
}
}
impl fmt::Display for ConnectionName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for ConnectionName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for ConnectionName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::new(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
mod new {
use super::*;
#[rstest]
#[case("Production")]
#[case("Local Dev")]
#[case(" Production ")] #[case("a")] fn accepts_nonempty_names(#[case] input: &str) {
assert!(ConnectionName::new(input).is_ok());
}
#[rstest]
#[case("")]
#[case(" ")]
fn rejects_empty_or_whitespace_names(#[case] input: &str) {
assert!(ConnectionName::new(input).is_err());
}
#[test]
fn exactly_50_chars_returns_ok() {
let name = "a".repeat(50);
assert!(ConnectionName::new(&name).is_ok());
}
#[test]
fn over_50_chars_returns_too_long_error() {
let name = "a".repeat(51);
let result = ConnectionName::new(&name);
assert!(matches!(
result,
Err(ConnectionNameError::TooLong { len: 51, max: 50 })
));
}
#[test]
fn trims_whitespace() {
let name = ConnectionName::new(" Production ").unwrap();
assert_eq!(name.as_str(), "Production");
}
}
mod normalized {
use super::*;
#[test]
fn lowercases_name() {
let name = ConnectionName::new("Production").unwrap();
assert_eq!(name.normalized(), "production");
}
#[test]
fn mixed_case_normalized_to_lowercase() {
let name = ConnectionName::new("My Local DB").unwrap();
assert_eq!(name.normalized(), "my local db");
}
}
mod display {
use super::*;
#[test]
fn formats_as_inner_string() {
let name = ConnectionName::new("Production").unwrap();
assert_eq!(format!("{name}"), "Production");
}
}
mod serde {
use super::*;
#[test]
fn serializes_to_string() {
let name = ConnectionName::new("Production").unwrap();
let json = serde_json::to_string(&name).unwrap();
assert_eq!(json, "\"Production\"");
}
#[test]
fn deserializes_valid_string() {
let json = "\"Production\"";
let name: ConnectionName = serde_json::from_str(json).unwrap();
assert_eq!(name.as_str(), "Production");
}
#[test]
fn deserialize_empty_returns_error() {
let json = "\"\"";
let result: Result<ConnectionName, _> = serde_json::from_str(json);
assert!(result.is_err());
}
}
}