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}