Skip to main content

track_core/
task_id.rs

1use crate::time_utils::format_task_id_timestamp;
2
3pub const TASK_SLUG_MAX_LENGTH: usize = 60;
4
5pub fn build_task_slug(description: &str) -> String {
6    let slug = slug::slugify(description);
7    let trimmed = slug.chars().take(TASK_SLUG_MAX_LENGTH).collect::<String>();
8
9    if trimmed.is_empty() {
10        "task".to_owned()
11    } else {
12        trimmed
13    }
14}
15
16pub fn build_unique_task_id<F>(
17    timestamp: time::OffsetDateTime,
18    description: &str,
19    mut exists: F,
20) -> String
21where
22    F: FnMut(&str) -> bool,
23{
24    let base_id = format!(
25        "{}-{}",
26        format_task_id_timestamp(timestamp),
27        build_task_slug(description)
28    );
29
30    if !exists(&base_id) {
31        return base_id;
32    }
33
34    let mut suffix = 2;
35    loop {
36        let candidate = format!("{base_id}-{suffix}");
37        if !exists(&candidate) {
38            return candidate;
39        }
40
41        suffix += 1;
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use time::macros::datetime;
48
49    use super::{build_task_slug, build_unique_task_id};
50
51    #[test]
52    fn slug_falls_back_when_description_has_no_letters() {
53        assert_eq!(build_task_slug("!!!"), "task");
54    }
55
56    #[test]
57    fn unique_id_appends_suffix_when_needed() {
58        let id = build_unique_task_id(
59            datetime!(2026-03-16 09:08:07 UTC),
60            "Fix issue",
61            |candidate| candidate == "20260316-090807-fix-issue",
62        );
63
64        assert_eq!(id, "20260316-090807-fix-issue-2");
65    }
66}