use std::io::{self, Write};
use anyhow::bail;
use crate::entity;
pub(crate) fn resolve_title(title: Option<String>) -> anyhow::Result<String> {
if let Some(t) = title {
let t = t.trim().to_string();
if t.is_empty() {
bail!("Title must not be empty");
}
return Ok(t);
}
let mut stdout = io::stdout();
write!(stdout, "Title: ")?;
stdout.flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let entered = line.trim().to_string();
if entered.is_empty() {
bail!("Title must not be empty");
}
Ok(entered)
}
const SLUG_MAX: usize = 100;
pub(crate) fn resolve_slug(title: &str, slug: Option<String>) -> anyhow::Result<String> {
if let Some(s) = slug {
let normalised = entity::derive_slug(&s);
if normalised.is_empty() {
bail!("--slug must not be empty after normalisation");
}
return Ok(truncate_slug(&normalised, SLUG_MAX));
}
let derived = entity::derive_slug(title);
if derived.is_empty() {
bail!("Could not derive a slug from the title; pass --slug");
}
Ok(truncate_slug(&derived, SLUG_MAX))
}
fn truncate_slug(slug: &str, max: usize) -> String {
if slug.len() <= max {
return slug.to_string();
}
let prefix = slug.get(..max).unwrap_or(slug);
match prefix.rfind('-') {
Some(cut) if cut > 0 => prefix.get(..cut).unwrap_or(prefix).to_string(),
_ => prefix.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_title_uses_and_trims_the_argument() {
assert_eq!(
resolve_title(Some(" My Title ".into())).unwrap(),
"My Title"
);
}
#[test]
fn resolve_title_rejects_an_empty_argument() {
let err = resolve_title(Some(" ".into())).unwrap_err();
assert!(err.to_string().contains("Title must not be empty"));
}
#[test]
fn resolve_slug_prefers_the_explicit_flag() {
assert_eq!(
resolve_slug("My Title", Some("custom".into())).unwrap(),
"custom"
);
}
#[test]
fn resolve_slug_derives_from_the_title_when_unset() {
assert_eq!(resolve_slug("My Title", None).unwrap(), "my-title");
}
#[test]
fn resolve_slug_bails_when_a_symbol_only_title_derives_to_nothing() {
let err = resolve_slug("!!!", None).unwrap_err();
assert!(err.to_string().contains("pass --slug"));
}
#[test]
fn resolve_slug_rejects_an_empty_explicit_flag() {
let err = resolve_slug("My Title", Some(String::new())).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
}
#[test]
fn resolve_slug_accepts_a_well_formed_explicit_flag_verbatim() {
assert_eq!(
resolve_slug("My Title", Some("a-clean-slug".into())).unwrap(),
"a-clean-slug"
);
assert_eq!(resolve_slug("My Title", Some("a".into())).unwrap(), "a");
}
#[test]
fn resolve_slug_normalises_an_explicit_flag() {
assert_eq!(
resolve_slug("My Title", Some("My Custom Slug".into())).unwrap(),
"my-custom-slug"
);
assert_eq!(
resolve_slug("My Title", Some("UPPER".into())).unwrap(),
"upper"
);
assert_eq!(
resolve_slug("My Title", Some("under_score".into())).unwrap(),
"under-score"
);
assert_eq!(
resolve_slug("My Title", Some("-leading".into())).unwrap(),
"leading"
);
assert_eq!(
resolve_slug("My Title", Some("trailing-".into())).unwrap(),
"trailing"
);
assert_eq!(
resolve_slug("My Title", Some("has space".into())).unwrap(),
"has-space"
);
assert_eq!(
resolve_slug("My Title", Some("tab\tslug".into())).unwrap(),
"tab-slug"
);
assert_eq!(
resolve_slug("My Title", Some("../../etc".into())).unwrap(),
"etc"
);
assert_eq!(resolve_slug("My Title", Some("a/b".into())).unwrap(), "ab");
assert_eq!(
resolve_slug("My Title", Some(".hidden".into())).unwrap(),
"hidden"
);
let err = resolve_slug("My Title", Some("..".into())).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
let err = resolve_slug("My Title", Some("!!!".into())).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
}
#[test]
fn resolve_slug_truncates_an_overlong_explicit_flag() {
let long = "a".repeat(SLUG_MAX + 1);
let slug = resolve_slug("My Title", Some(long)).unwrap();
assert!(slug.len() <= SLUG_MAX, "len {}", slug.len());
assert!(!slug.is_empty());
}
#[test]
fn resolve_slug_truncates_a_derived_slug_over_the_cap() {
let title = "word ".repeat(40); let slug = resolve_slug(&title, None).unwrap();
assert!(slug.len() <= SLUG_MAX, "len {}", slug.len());
assert!(!slug.is_empty());
}
#[test]
fn truncate_slug_returns_a_within_cap_slug_unchanged() {
assert_eq!(truncate_slug("short-slug", SLUG_MAX), "short-slug");
assert_eq!(truncate_slug("abc", 3), "abc");
}
#[test]
fn truncate_slug_cuts_at_the_last_dash_within_the_prefix() {
assert_eq!(truncate_slug("alpha-beta-gamma", 10), "alpha");
}
#[test]
fn truncate_slug_hard_cuts_on_a_boundary_when_no_usable_dash() {
assert_eq!(truncate_slug("supercalifragilistic", 5), "super");
assert_eq!(truncate_slug("-leadingdash", 4), "-lea");
}
#[test]
fn truncate_slug_never_empties_a_non_empty_slug() {
assert!(!truncate_slug("aaaaaaaaaa", 3).is_empty());
assert!(!truncate_slug("a-b-c-d-e-f", 4).is_empty());
}
}