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}