Skip to main content

atm_storage/
types.rs

1use std::fmt;
2use std::ops::Deref;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Deserializer, Serialize};
7
8use crate::error::AtmError;
9use crate::validation::validate_path_segment;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct IsoTimestamp(DateTime<Utc>);
14
15impl IsoTimestamp {
16    pub fn now() -> Self {
17        Self(Utc::now())
18    }
19
20    pub fn from_datetime(datetime: DateTime<Utc>) -> Self {
21        Self(datetime)
22    }
23
24    pub fn into_inner(self) -> DateTime<Utc> {
25        self.0
26    }
27}
28
29impl FromStr for IsoTimestamp {
30    type Err = chrono::ParseError;
31
32    fn from_str(value: &str) -> Result<Self, Self::Err> {
33        value.parse::<DateTime<Utc>>().map(Self::from_datetime)
34    }
35}
36
37impl From<DateTime<Utc>> for IsoTimestamp {
38    fn from(datetime: DateTime<Utc>) -> Self {
39        Self(datetime)
40    }
41}
42
43impl fmt::Display for IsoTimestamp {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}", self.0.to_rfc3339())
46    }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
50#[serde(transparent)]
51pub struct AgentName(String);
52
53impl AgentName {
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57
58    pub fn into_inner(self) -> String {
59        self.0
60    }
61
62    #[doc(hidden)]
63    pub fn from_validated(value: impl Into<String>) -> Self {
64        Self(value.into())
65    }
66}
67
68impl FromStr for AgentName {
69    type Err = AtmError;
70
71    fn from_str(value: &str) -> Result<Self, Self::Err> {
72        let trimmed = value.trim();
73        validate_path_segment(trimmed, "agent")?;
74        Ok(Self(trimmed.to_string()))
75    }
76}
77
78impl<'de> Deserialize<'de> for AgentName {
79    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80    where
81        D: Deserializer<'de>,
82    {
83        let value = String::deserialize(deserializer)?;
84        value.parse().map_err(serde::de::Error::custom)
85    }
86}
87
88impl From<AgentName> for String {
89    fn from(value: AgentName) -> Self {
90        value.0
91    }
92}
93
94impl AsRef<str> for AgentName {
95    fn as_ref(&self) -> &str {
96        self.as_str()
97    }
98}
99
100impl Deref for AgentName {
101    type Target = str;
102
103    fn deref(&self) -> &Self::Target {
104        self.as_str()
105    }
106}
107
108impl fmt::Display for AgentName {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.as_str())
111    }
112}
113
114impl PartialEq<&str> for AgentName {
115    fn eq(&self, other: &&str) -> bool {
116        self.as_str() == *other
117    }
118}
119
120#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
121#[serde(transparent)]
122pub struct AgentId(String);
123
124impl AgentId {
125    pub fn new(value: impl Into<String>) -> Result<Self, AtmError> {
126        let value = value.into();
127        let trimmed = value.trim();
128        match trimmed.split_once('@') {
129            Some((agent, team)) => {
130                validate_path_segment(agent, "agent id")?;
131                validate_path_segment(team, "agent id")?;
132            }
133            None => validate_path_segment(trimmed, "agent id")?,
134        }
135        Ok(Self(trimmed.to_string()))
136    }
137
138    pub fn as_str(&self) -> &str {
139        &self.0
140    }
141
142    pub fn is_empty(&self) -> bool {
143        self.0.is_empty()
144    }
145
146    pub fn into_inner(self) -> String {
147        self.0
148    }
149}
150
151impl FromStr for AgentId {
152    type Err = AtmError;
153
154    fn from_str(value: &str) -> Result<Self, Self::Err> {
155        Self::new(value)
156    }
157}
158
159impl<'de> Deserialize<'de> for AgentId {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: Deserializer<'de>,
163    {
164        let value = String::deserialize(deserializer)?;
165        value.parse().map_err(serde::de::Error::custom)
166    }
167}
168
169impl From<AgentId> for String {
170    fn from(value: AgentId) -> Self {
171        value.0
172    }
173}
174
175impl AsRef<str> for AgentId {
176    fn as_ref(&self) -> &str {
177        self.as_str()
178    }
179}
180
181impl Deref for AgentId {
182    type Target = str;
183
184    fn deref(&self) -> &Self::Target {
185        self.as_str()
186    }
187}
188
189impl fmt::Display for AgentId {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.write_str(self.as_str())
192    }
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
196#[serde(transparent)]
197pub struct TeamName(String);
198
199impl TeamName {
200    pub fn as_str(&self) -> &str {
201        &self.0
202    }
203
204    pub fn into_inner(self) -> String {
205        self.0
206    }
207
208    #[doc(hidden)]
209    pub fn from_validated(value: impl Into<String>) -> Self {
210        Self(value.into())
211    }
212}
213
214impl FromStr for TeamName {
215    type Err = AtmError;
216
217    fn from_str(value: &str) -> Result<Self, Self::Err> {
218        let trimmed = value.trim();
219        validate_path_segment(trimmed, "team")?;
220        Ok(Self(trimmed.to_string()))
221    }
222}
223
224impl<'de> Deserialize<'de> for TeamName {
225    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
226    where
227        D: Deserializer<'de>,
228    {
229        let value = String::deserialize(deserializer)?;
230        value.parse().map_err(serde::de::Error::custom)
231    }
232}
233
234impl From<TeamName> for String {
235    fn from(value: TeamName) -> Self {
236        value.0
237    }
238}
239
240impl AsRef<str> for TeamName {
241    fn as_ref(&self) -> &str {
242        self.as_str()
243    }
244}
245
246impl Deref for TeamName {
247    type Target = str;
248
249    fn deref(&self) -> &Self::Target {
250        self.as_str()
251    }
252}
253
254impl fmt::Display for TeamName {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.write_str(self.as_str())
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{AgentId, AgentName, IsoTimestamp};
263
264    #[test]
265    fn agent_id_new_matches_agent_name_validation_for_valid_value() {
266        let agent_id = AgentId::new("worker-1").expect("agent id");
267        let agent_name: AgentName = "worker-1".parse().expect("agent name");
268
269        assert_eq!(agent_id.as_str(), agent_name.as_str());
270    }
271
272    #[test]
273    fn agent_id_new_rejects_invalid_path_segments() {
274        for invalid in ["", ".hidden", "two..dots", "bad/name", "bad name"] {
275            assert!(
276                AgentId::new(invalid).is_err(),
277                "expected `{invalid}` to fail"
278            );
279        }
280    }
281
282    #[test]
283    fn agent_id_deserialize_applies_validation() {
284        let parsed: AgentId = serde_json::from_str("\"worker-2\"").expect("deserialize");
285        assert_eq!(parsed.as_str(), "worker-2");
286        let compound: AgentId =
287            serde_json::from_str("\"worker-2@test-team\"").expect("compound deserialize");
288        assert_eq!(compound.as_str(), "worker-2@test-team");
289
290        let error = serde_json::from_str::<AgentId>("\"bad/name\"").expect_err("invalid id");
291        assert!(error.to_string().contains("path separators"));
292    }
293
294    #[test]
295    fn iso_timestamp_from_str_round_trips_rfc3339_input() {
296        let timestamp: IsoTimestamp = "2026-07-11T01:20:17Z".parse().expect("timestamp");
297
298        assert_eq!(timestamp.to_string(), "2026-07-11T01:20:17+00:00");
299    }
300}
301
302impl PartialEq<&str> for TeamName {
303    fn eq(&self, other: &&str) -> bool {
304        self.as_str() == *other
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
309#[serde(transparent)]
310pub struct TaskId(String);
311
312impl TaskId {
313    pub fn as_str(&self) -> &str {
314        &self.0
315    }
316
317    pub fn into_inner(self) -> String {
318        self.0
319    }
320}
321
322impl FromStr for TaskId {
323    type Err = AtmError;
324
325    fn from_str(value: &str) -> Result<Self, Self::Err> {
326        let trimmed = value.trim();
327        if trimmed.is_empty() {
328            return Err(
329                AtmError::validation("task id must not be blank").with_recovery(
330                    "Provide a non-empty --task-id value or omit --task-id for non-task messages.",
331                ),
332            );
333        }
334        Ok(Self(trimmed.to_string()))
335    }
336}
337
338impl<'de> Deserialize<'de> for TaskId {
339    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
340    where
341        D: Deserializer<'de>,
342    {
343        let value = String::deserialize(deserializer)?;
344        value.parse().map_err(serde::de::Error::custom)
345    }
346}
347
348impl From<TaskId> for String {
349    fn from(value: TaskId) -> Self {
350        value.0
351    }
352}
353
354impl AsRef<str> for TaskId {
355    fn as_ref(&self) -> &str {
356        self.as_str()
357    }
358}
359
360impl Deref for TaskId {
361    type Target = str;
362
363    fn deref(&self) -> &Self::Target {
364        self.as_str()
365    }
366}
367
368impl fmt::Display for TaskId {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        f.write_str(self.as_str())
371    }
372}
373
374const MAX_MODEL_NAME_BYTES: usize = 256;
375const MAX_PANE_ID_BYTES: usize = 256;
376
377#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
378#[serde(transparent)]
379pub struct ModelName(String);
380
381impl ModelName {
382    pub fn new(value: impl Into<String>) -> Result<Self, AtmError> {
383        let value = value.into();
384        if value.len() > MAX_MODEL_NAME_BYTES {
385            return Err(AtmError::validation(format!(
386                "model must be at most {MAX_MODEL_NAME_BYTES} bytes"
387            )));
388        }
389        Ok(Self(value))
390    }
391
392    pub fn as_str(&self) -> &str {
393        &self.0
394    }
395
396    pub fn is_empty(&self) -> bool {
397        self.0.is_empty()
398    }
399
400    pub fn into_inner(self) -> String {
401        self.0
402    }
403}
404
405impl<'de> Deserialize<'de> for ModelName {
406    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
407    where
408        D: Deserializer<'de>,
409    {
410        let value = String::deserialize(deserializer)?;
411        Self::new(value).map_err(serde::de::Error::custom)
412    }
413}
414
415impl From<ModelName> for String {
416    fn from(value: ModelName) -> Self {
417        value.0
418    }
419}
420
421impl AsRef<str> for ModelName {
422    fn as_ref(&self) -> &str {
423        self.as_str()
424    }
425}
426
427impl Deref for ModelName {
428    type Target = str;
429
430    fn deref(&self) -> &Self::Target {
431        self.as_str()
432    }
433}
434
435impl fmt::Display for ModelName {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        f.write_str(self.as_str())
438    }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
442#[serde(transparent)]
443pub struct PaneId(String);
444
445impl PaneId {
446    pub fn new(value: impl Into<String>) -> Result<Self, AtmError> {
447        let value = value.into();
448        let trimmed = value.trim();
449        if trimmed.is_empty() {
450            return Err(AtmError::validation("pane id must not be blank"));
451        }
452        if trimmed.len() > MAX_PANE_ID_BYTES {
453            return Err(AtmError::validation(format!(
454                "pane id must be at most {MAX_PANE_ID_BYTES} bytes"
455            )));
456        }
457        Ok(Self(trimmed.to_string()))
458    }
459
460    pub fn from_cli(value: &str) -> Result<Self, AtmError> {
461        let trimmed = value.trim();
462        if trimmed.is_empty() {
463            return Err(AtmError::validation("pane id must not be blank"));
464        }
465        let normalized = if trimmed.starts_with('%') || trimmed.contains(':') {
466            trimmed.to_string()
467        } else {
468            format!("%{trimmed}")
469        };
470        Self::new(normalized)
471    }
472
473    pub fn as_str(&self) -> &str {
474        &self.0
475    }
476
477    pub fn into_inner(self) -> String {
478        self.0
479    }
480}
481
482impl<'de> Deserialize<'de> for PaneId {
483    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
484    where
485        D: Deserializer<'de>,
486    {
487        let value = String::deserialize(deserializer)?;
488        Self::new(value).map_err(serde::de::Error::custom)
489    }
490}
491
492impl From<PaneId> for String {
493    fn from(value: PaneId) -> Self {
494        value.0
495    }
496}
497
498impl AsRef<str> for PaneId {
499    fn as_ref(&self) -> &str {
500        self.as_str()
501    }
502}
503
504impl Deref for PaneId {
505    type Target = str;
506
507    fn deref(&self) -> &Self::Target {
508        self.as_str()
509    }
510}
511
512impl fmt::Display for PaneId {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        f.write_str(self.as_str())
515    }
516}