Skip to main content

solti_model/domain/identity/
task.rs

1//! # Task identifier.
2//!
3//! [`TaskId`] is a unique identifier for a task resource (newtype over `Arc<str>`).
4
5use super::validate_identity;
6use crate::error::ModelError;
7
8/// Maximum length of a `TaskId`.
9pub const TASK_ID_MAX_LEN: usize = 256;
10
11arc_str_newtype! {
12    /// Unique identifier for a task instance.
13    ///
14    /// Generated by the runner during task creation via `solti_runner::make_run_id`.
15    /// Format: `{runner}-{slot}-{seq}` (e.g., `subprocess-demo-slot-42`).
16    ///
17    /// ```text
18    ///  TaskSpec { slot: "build" }
19    ///           │
20    ///           ▼  submit #1
21    ///  TaskId: "subprocess-build-1"   ← attempt 1..N, then done
22    ///           │
23    ///           ▼  submit #2
24    ///  TaskId: "subprocess-build-2"   ← new run, new id
25    ///           │
26    ///           ▼  submit #3
27    ///  TaskId: "subprocess-build-3"
28    /// ```
29    ///
30    /// The [`Slot`](crate::Slot) stays the same across submissions.
31    /// The `TaskId` is unique per run: same slot, different execution, different id.
32    pub struct TaskId;
33}
34
35impl TaskId {
36    /// Validate that the task id is safe to use across the SDK.
37    ///
38    /// See [`validate_identity`] for the exact rules.
39    pub fn validate_format(&self) -> Result<(), ModelError> {
40        validate_identity("task_id", self.as_str(), TASK_ID_MAX_LEN)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use std::sync::Arc;
48
49    #[test]
50    fn task_id_from_string() {
51        let id = TaskId::from("subprocess-slot-2a");
52        assert_eq!(id.as_str(), "subprocess-slot-2a");
53    }
54
55    #[test]
56    fn task_id_display() {
57        let id = TaskId::new("test-id");
58        assert_eq!(format!("{}", id), "test-id");
59    }
60
61    #[test]
62    fn task_id_serde_transparent() {
63        let id = TaskId::from("runner-slot-ff");
64        let json = serde_json::to_string(&id).unwrap();
65        assert_eq!(json, r#""runner-slot-ff""#);
66
67        let back: TaskId = serde_json::from_str(&json).unwrap();
68        assert_eq!(back, id);
69    }
70
71    #[test]
72    fn task_id_hash_equality() {
73        use std::collections::HashSet;
74
75        let mut set = HashSet::new();
76        set.insert(TaskId::from("id-1"));
77        set.insert(TaskId::from("id-2"));
78        set.insert(TaskId::from("id-1"));
79
80        assert_eq!(set.len(), 2);
81        assert!(set.contains(&TaskId::from("id-1")));
82    }
83
84    #[test]
85    fn clone_is_cheap() {
86        let id = TaskId::new("shared-task");
87        let cloned = id.clone();
88        let a: Arc<str> = id.into_inner();
89        let b: Arc<str> = cloned.into_inner();
90        assert!(Arc::ptr_eq(&a, &b));
91    }
92
93    #[test]
94    fn validate_format_accepts_runner_generated() {
95        TaskId::new("subprocess-build-1").validate_format().unwrap();
96        TaskId::new("subprocess-build.frontend-ff")
97            .validate_format()
98            .unwrap();
99    }
100
101    #[test]
102    fn validate_format_rejects_invalid() {
103        assert!(TaskId::new("").validate_format().is_err());
104        assert!(TaskId::new("with/slash").validate_format().is_err());
105        assert!(TaskId::new("with space").validate_format().is_err());
106        assert!(TaskId::new(&"x".repeat(257)).validate_format().is_err());
107    }
108}