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