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