use serde::{Deserialize, Serialize};
use crate::error::ManifestError;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Profile(String);
impl Profile {
pub const DEFAULT_NAME: &'static str = "default";
#[must_use]
pub fn default_profile() -> Self {
Self(Self::DEFAULT_NAME.to_owned())
}
pub fn named(name: impl Into<String>) -> Self {
let name = name.into();
if name == Self::DEFAULT_NAME {
return Self::default_profile();
}
Self(name)
}
pub fn try_named(name: impl Into<String>) -> Result<Self, ManifestError> {
let name = name.into();
if name.is_empty() {
return Err(ManifestError::Invalid("profile name is empty".into()));
}
if name.len() > 32 {
return Err(ManifestError::Invalid(
"profile name longer than 32 characters".into(),
));
}
for (offset, &b) in name.as_bytes().iter().enumerate() {
if !(b.is_ascii_alphanumeric() || b == b'_' || b == b'-') {
return Err(ManifestError::Invalid(format!(
"profile name contains an invalid character at byte offset {offset}"
)));
}
}
if name.bytes().all(|b| b.is_ascii_digit()) {
return Err(ManifestError::Invalid(
"profile name must contain at least one non-digit character".into(),
));
}
Ok(Self::named(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_default(&self) -> bool {
self.0 == Self::DEFAULT_NAME
}
}
impl Default for Profile {
fn default() -> Self {
Self::default_profile()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_profile_is_named_default() {
let p = Profile::default_profile();
assert!(p.is_default());
assert_eq!(p.as_str(), "default");
}
#[test]
fn named_profile_is_not_default() {
let p = Profile::named("dev");
assert!(!p.is_default());
assert_eq!(p.as_str(), "dev");
}
#[test]
fn named_default_normalizes_to_canonical() {
let p = Profile::named("default");
assert!(p.is_default());
assert_eq!(p, Profile::default_profile());
}
#[test]
fn try_named_accepts_valid_names() {
for name in ["dev", "staging", "prod-2", "feat_branch_123"] {
assert!(Profile::try_named(name).is_ok(), "expected {name} ok");
}
}
#[test]
fn try_named_rejects_invalid_names() {
let bad = [
"",
" ",
"with space",
"with\nnewline",
"[evil]",
"123",
&"a".repeat(33),
];
for name in bad {
assert!(
Profile::try_named(name).is_err(),
"expected {name:?} rejected"
);
}
}
#[test]
fn try_named_default_normalizes() {
let p = Profile::try_named("default").expect("valid");
assert!(p.is_default());
}
#[test]
fn profile_serializes_transparently() {
let p = Profile::named("staging");
let s = serde_json::to_string(&p).expect("serde");
assert_eq!(s, "\"staging\"");
}
}