use crate::error::{DkpError, DkpResult};
pub const MIN_SLUG_LEN: usize = 2;
pub const MAX_SLUG_LEN: usize = 40;
const RESERVED_DOMAIN_SLUGS: &[&str] = &[
"all",
"none",
"null",
"undefined",
"admin",
"root",
"system",
"test",
"example",
"unknown",
"n-a",
"na",
];
pub fn slugify_domain(display: &str) -> String {
let lowered = display.trim().to_lowercase();
let mut slug = String::with_capacity(lowered.len());
let mut prev_was_hyphen = true; for ch in lowered.chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch);
prev_was_hyphen = false;
} else if !prev_was_hyphen {
slug.push('-');
prev_was_hyphen = true;
}
}
while slug.ends_with('-') {
slug.pop();
}
slug
}
pub fn validate_domain_slug(slug: &str) -> DkpResult<()> {
if slug.len() < MIN_SLUG_LEN || slug.len() > MAX_SLUG_LEN {
return Err(DkpError::ManifestInvalid {
reason: format!(
"domain slug '{slug}' must be {MIN_SLUG_LEN}-{MAX_SLUG_LEN} characters (got {})",
slug.len()
),
});
}
if !slug
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(DkpError::ManifestInvalid {
reason: format!(
"domain slug '{slug}' must contain only lowercase letters, digits, and hyphens"
),
});
}
if slug.starts_with('-') || slug.ends_with('-') || slug.contains("--") {
return Err(DkpError::ManifestInvalid {
reason: format!("domain slug '{slug}' has malformed hyphenation"),
});
}
Ok(())
}
pub fn validate_domain_slug_for_registry(slug: &str) -> DkpResult<()> {
validate_domain_slug(slug)?;
if RESERVED_DOMAIN_SLUGS.contains(&slug) {
return Err(DkpError::ManifestInvalid {
reason: format!("domain '{slug}' is a reserved word and cannot be used"),
});
}
Ok(())
}
pub fn derive_and_validate_domain_slug(display: &str) -> DkpResult<String> {
let slug = slugify_domain(display);
validate_domain_slug(&slug)?;
Ok(slug)
}
pub fn derive_and_validate_domain_slug_for_registry(display: &str) -> DkpResult<String> {
let slug = slugify_domain(display);
validate_domain_slug_for_registry(&slug)?;
Ok(slug)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_cases() {
assert_eq!(slugify_domain("Startups"), "startups");
assert_eq!(slugify_domain(" Startups "), "startups");
assert_eq!(slugify_domain("Health & Law"), "health-law");
assert_eq!(slugify_domain("AI/ML Tools"), "ai-ml-tools");
assert_eq!(slugify_domain("---weird---"), "weird");
assert_eq!(slugify_domain("support"), "support");
}
#[test]
fn length_bounds() {
assert!(validate_domain_slug("a").is_err());
assert!(validate_domain_slug(&"a".repeat(41)).is_err());
assert!(validate_domain_slug(&"a".repeat(40)).is_ok());
assert!(validate_domain_slug("ab").is_ok());
}
#[test]
fn reserved_words_rejected_for_registry() {
for w in ["all", "none", "admin", "undefined", "null"] {
assert!(
validate_domain_slug_for_registry(w).is_err(),
"{w} should be reserved"
);
}
}
#[test]
fn reserved_words_allowed_locally() {
for w in ["all", "none", "admin", "undefined", "null", "test"] {
assert!(
validate_domain_slug(w).is_ok(),
"{w} should be allowed for local validation"
);
assert!(derive_and_validate_domain_slug(w).is_ok());
}
}
#[test]
fn support_is_not_reserved() {
assert!(validate_domain_slug("support").is_ok());
assert!(validate_domain_slug_for_registry("support").is_ok());
}
#[test]
fn charset_rejected_if_hand_constructed_badly() {
assert!(validate_domain_slug("Has_Underscore").is_err());
assert!(validate_domain_slug("-leading").is_err());
assert!(validate_domain_slug("trailing-").is_err());
assert!(validate_domain_slug("double--hyphen").is_err());
}
#[test]
fn derive_round_trip() {
assert_eq!(
derive_and_validate_domain_slug("Startups").unwrap(),
"startups"
);
assert_eq!(derive_and_validate_domain_slug("Law").unwrap(), "law");
assert_eq!(
derive_and_validate_domain_slug("support").unwrap(),
"support"
);
assert!(derive_and_validate_domain_slug_for_registry("Admin").is_err());
}
#[test]
fn slugify_empty_and_punctuation_only() {
assert_eq!(slugify_domain(""), "");
assert_eq!(slugify_domain(" "), "");
assert_eq!(slugify_domain("!!!"), "");
}
#[test]
fn slugify_unicode_dropped_as_separator() {
assert_eq!(slugify_domain("Café Law"), "caf-law");
}
#[test]
fn validate_rejects_all_reserved_words_for_registry() {
for w in [
"all",
"none",
"null",
"undefined",
"admin",
"root",
"system",
"test",
"example",
"unknown",
"n-a",
"na",
] {
assert!(
validate_domain_slug_for_registry(w).is_err(),
"{w} should be reserved"
);
}
}
#[test]
fn validate_rejects_uppercase() {
assert!(validate_domain_slug("Startups").is_err());
}
#[test]
fn validate_rejects_non_ascii() {
assert!(validate_domain_slug("café").is_err());
}
#[test]
fn derive_and_validate_reserved_word_collision_for_registry() {
assert!(derive_and_validate_domain_slug_for_registry("Root").is_err());
assert!(derive_and_validate_domain_slug_for_registry(" Test ").is_err());
assert!(derive_and_validate_domain_slug("Root").is_ok());
assert!(derive_and_validate_domain_slug(" Test ").is_ok());
}
#[test]
fn derive_and_validate_too_short_after_slugify() {
assert!(derive_and_validate_domain_slug("!!!").is_err());
assert!(derive_and_validate_domain_slug("a").is_err());
}
}