Skip to main content

machi_protocol/
tool_id.rs

1//! Stable tool identity used for routing and metrics.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Canonical tool identifier (name-based for v1).
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ToolId {
10    name: String,
11}
12
13impl ToolId {
14    /// Create from a non-empty name.
15    ///
16    /// # Errors
17    ///
18    /// Returns `None` when `name` is empty or whitespace-only.
19    #[must_use]
20    pub fn new(name: impl Into<String>) -> Option<Self> {
21        let name = name.into();
22        if name.trim().is_empty() {
23            return None;
24        }
25        Some(Self { name })
26    }
27
28    /// Create without validation (for compile-time constants).
29    ///
30    /// # Panics
31    ///
32    /// Panics if `name` is empty. Prefer [`Self::new`] for runtime input.
33    #[must_use]
34    pub fn const_new(name: &'static str) -> Self {
35        assert!(!name.is_empty(), "tool id must be non-empty");
36        Self {
37            name: name.to_owned(),
38        }
39    }
40
41    /// Underlying name string.
42    #[must_use]
43    pub fn as_str(&self) -> &str {
44        &self.name
45    }
46}
47
48impl fmt::Display for ToolId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(&self.name)
51    }
52}
53
54impl AsRef<str> for ToolId {
55    fn as_ref(&self) -> &str {
56        &self.name
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn rejects_empty() {
66        assert!(ToolId::new("").is_none(), "empty rejected");
67        assert!(ToolId::new("  ").is_none(), "whitespace rejected");
68    }
69
70    #[test]
71    fn accepts_name() {
72        let id = ToolId::new("read_file").expect("id");
73        assert_eq!(id.as_str(), "read_file");
74    }
75}