agentstategraph_core/
namespace.rs1use serde::{Deserialize, Serialize};
13use std::fmt;
14use std::str::FromStr;
15
16pub const MAX_NAMESPACE_LEN: usize = 64;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct Namespace(String);
28
29impl Namespace {
30 pub const DEFAULT: &'static str = "default";
33
34 pub fn new(name: impl Into<String>) -> Result<Self, NamespaceError> {
36 let name = name.into();
37 if name.is_empty() {
38 return Err(NamespaceError::Empty);
39 }
40 if name.len() > MAX_NAMESPACE_LEN {
41 return Err(NamespaceError::TooLong(name.len()));
42 }
43 if !name
44 .chars()
45 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
46 {
47 return Err(NamespaceError::InvalidChars(name));
48 }
49 Ok(Namespace(name))
50 }
51
52 pub fn default_ns() -> Self {
55 Namespace(Self::DEFAULT.to_string())
56 }
57
58 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62}
63
64impl fmt::Display for Namespace {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.write_str(&self.0)
67 }
68}
69
70impl AsRef<str> for Namespace {
71 fn as_ref(&self) -> &str {
72 &self.0
73 }
74}
75
76impl FromStr for Namespace {
77 type Err = NamespaceError;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 Namespace::new(s)
81 }
82}
83
84impl TryFrom<String> for Namespace {
85 type Error = NamespaceError;
86
87 fn try_from(s: String) -> Result<Self, Self::Error> {
88 Namespace::new(s)
89 }
90}
91
92impl TryFrom<&str> for Namespace {
93 type Error = NamespaceError;
94
95 fn try_from(s: &str) -> Result<Self, Self::Error> {
96 Namespace::new(s)
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
102pub enum NamespaceError {
103 #[error("namespace name must not be empty")]
104 Empty,
105
106 #[error("namespace name is too long ({0} bytes, max {MAX_NAMESPACE_LEN})")]
107 TooLong(usize),
108
109 #[error(
110 "namespace name contains invalid characters: {0:?} \
111 (only ASCII alphanumerics, hyphens, and underscores are allowed)"
112 )]
113 InvalidChars(String),
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn valid_names() {
122 for name in ["default", "my-project", "proj_42", "a", "A1-b_C"] {
123 assert!(Namespace::new(name).is_ok(), "expected valid: {name}");
124 }
125 }
126
127 #[test]
128 fn invalid_names() {
129 assert_eq!(Namespace::new("").unwrap_err(), NamespaceError::Empty);
130 assert!(matches!(
131 Namespace::new("a".repeat(65)).unwrap_err(),
132 NamespaceError::TooLong(_)
133 ));
134 for bad in ["has space", "dot.name", "slash/name", "colon:name", "bang!"] {
135 assert!(
136 matches!(
137 Namespace::new(bad).unwrap_err(),
138 NamespaceError::InvalidChars(_)
139 ),
140 "expected invalid: {bad}"
141 );
142 }
143 }
144
145 #[test]
146 fn default_namespace_is_valid() {
147 let ns = Namespace::default_ns();
148 assert_eq!(ns.as_str(), "default");
149 assert_eq!(Namespace::new(Namespace::DEFAULT).unwrap(), ns);
150 }
151
152 #[test]
153 fn roundtrip_serde() {
154 let ns = Namespace::new("my-project").unwrap();
155 let json = serde_json::to_string(&ns).unwrap();
156 assert_eq!(json, r#""my-project""#);
157 let back: Namespace = serde_json::from_str(&json).unwrap();
158 assert_eq!(back, ns);
159 }
160}