use std::path::Path;
pub fn slugify(s: &str) -> String {
let slug: String = s
.trim()
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.lines()
.collect::<Vec<&str>>()
.join("-");
let collapsed = collapse_dashes(&slug);
let trimmed = collapsed.trim_matches('-');
let capped = if trimmed.len() > 50 {
let mut end = 50;
while !trimmed.is_char_boundary(end) {
end -= 1;
}
&trimmed[..end]
} else {
trimmed
};
if capped.is_empty() {
"run".to_string()
} else {
capped.to_string()
}
}
fn collapse_dashes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut prev_dash = false;
for c in s.chars() {
if c == '-' {
if !prev_dash {
result.push(c);
}
prev_dash = true;
} else {
result.push(c);
prev_dash = false;
}
}
result
}
pub fn derive_slug(workflow_path: Option<&Path>, nl: Option<&str>) -> String {
if let Some(path) = workflow_path {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
return slugify(stem);
}
}
if let Some(text) = nl {
let words: Vec<&str> = text.split_whitespace().take(6).collect();
return slugify(&words.join(" "));
}
"luft-workflow".to_string()
}
pub fn compose(slug: &str, unix_ts: u64) -> String {
format!("{slug}_{unix_ts}")
}
pub fn ensure_unique(base_dir: &Path, dir_name: &str) -> String {
if !base_dir.join(dir_name).exists() {
return dir_name.to_string();
}
for n in 2..u64::MAX {
let candidate = format!("{dir_name}_{n}");
if !base_dir.join(&candidate).exists() {
return candidate;
}
}
unreachable!("exhausted suffix space");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slugify_basic() {
assert_eq!(slugify("Hello World"), "hello-world");
assert_eq!(slugify("deep research v2!"), "deep-research-v2");
assert_eq!(slugify(" trim "), "trim");
}
#[test]
fn slugify_collapse() {
assert_eq!(slugify("a---b"), "a-b");
assert_eq!(slugify("a b"), "a-b");
}
#[test]
fn slugify_empty_fallback() {
assert_eq!(slugify("!!!"), "run");
assert_eq!(slugify(""), "run");
}
#[test]
fn slugify_long() {
let long = "a".repeat(100);
let result = slugify(&long);
assert_eq!(result.len(), 50);
}
#[test]
fn derive_from_workflow_path() {
assert_eq!(
derive_slug(Some(Path::new("scripts/clean.lua")), None),
"clean"
);
assert_eq!(
derive_slug(Some(Path::new("examples/deep-research.lua")), None),
"deep-research"
);
}
#[test]
fn derive_from_nl() {
let slug = derive_slug(None, Some("research AI trends in 2025"));
assert_eq!(slug, "research-ai-trends-in-2025");
}
#[test]
fn derive_fallback() {
assert_eq!(derive_slug(None, None), "luft-workflow");
}
#[test]
fn compose_format() {
assert_eq!(compose("clean", 1781980050), "clean_1781980050");
}
#[test]
fn ensure_unique_no_collision() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(ensure_unique(dir.path(), "clean_123"), "clean_123");
}
#[test]
fn ensure_unique_with_collision() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("clean_123")).unwrap();
assert_eq!(ensure_unique(dir.path(), "clean_123"), "clean_123_2");
}
}