1use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::{Error, data_dir::valid_task_handle};
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct TaskHandle(String);
22
23impl TaskHandle {
24 pub fn parse(handle: impl Into<String>) -> Result<Self, Error> {
34 let handle = handle.into();
35 if valid_task_handle(&handle).is_some() {
36 Ok(Self(handle))
37 } else {
38 Err(Error::invalid_reference(format!(
39 "`{handle}` is not a task handle"
40 )))
41 }
42 }
43
44 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47
48 pub(crate) fn key(&self) -> &str {
51 valid_task_handle(&self.0)
52 .expect("a TaskHandle is only ever constructed from a valid grammar")
53 .0
54 }
55}
56
57impl fmt::Display for TaskHandle {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(&self.0)
60 }
61}
62
63impl std::str::FromStr for TaskHandle {
64 type Err = Error;
65
66 fn from_str(handle: &str) -> Result<Self, Self::Err> {
67 Self::parse(handle)
68 }
69}
70
71impl AsRef<str> for TaskHandle {
72 fn as_ref(&self) -> &str {
73 &self.0
74 }
75}
76
77impl Serialize for TaskHandle {
78 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
79 serializer.serialize_str(&self.0)
80 }
81}
82
83impl<'de> Deserialize<'de> for TaskHandle {
84 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85 let handle = String::deserialize(deserializer)?;
86 Self::parse(handle).map_err(serde::de::Error::custom)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn a_well_formed_handle_round_trips() {
96 let text = format!("0123456789abcdef/{:032x}", 1);
97 let handle = TaskHandle::parse(text.clone()).expect("well-formed");
98 assert_eq!(handle.as_str(), text);
99 assert_eq!(handle.to_string(), text);
100 assert_eq!(handle.key(), "0123456789abcdef");
101 }
102
103 #[test]
104 fn a_malformed_handle_is_refused_generically() {
105 for bad in ["not-a-handle", "0123456789abcdef", "../../etc/passwd"] {
106 let error = TaskHandle::parse(bad).expect_err("refused");
107 assert!(error.to_string().contains("not a task handle"), "{error}");
108 assert!(
109 error.is_invalid_reference(),
110 "a malformed handle was never going to resolve: {error}"
111 );
112 }
113 }
114
115 #[test]
116 fn json_round_trips_as_a_bare_string() {
117 let text = format!("0123456789abcdef/{:032x}", 2);
118 let handle = TaskHandle::parse(text.clone()).unwrap();
119 let json = serde_json::to_string(&handle).unwrap();
120 assert_eq!(json, format!("\"{text}\""));
121 let back: TaskHandle = serde_json::from_str(&json).unwrap();
122 assert_eq!(back, handle);
123 }
124}