agentmux_srv/backend/
oref.rs1use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
10use std::fmt;
11use uuid::Uuid;
12
13use super::obj::VALID_OTYPES;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
28pub struct ORef {
29 pub otype: String,
30 pub oid: String,
31}
32
33impl ORef {
34 #[allow(dead_code)]
35 pub fn new(otype: impl Into<String>, oid: impl Into<String>) -> Self {
36 Self {
37 otype: otype.into(),
38 oid: oid.into(),
39 }
40 }
41
42 pub fn is_empty(&self) -> bool {
43 self.otype.is_empty() || self.oid.is_empty()
44 }
45
46 pub fn parse(s: &str) -> Result<Self, ORefParseError> {
48 if s.is_empty() {
49 return Ok(Self {
50 otype: String::new(),
51 oid: String::new(),
52 });
53 }
54
55 let parts: Vec<&str> = s.splitn(2, ':').collect();
56 if parts.len() != 2 {
57 return Err(ORefParseError::InvalidFormat(s.to_string()));
58 }
59
60 let otype = parts[0];
61 let oid = parts[1];
62
63 if otype.is_empty() || !otype.chars().all(|c| c.is_ascii_lowercase()) {
65 return Err(ORefParseError::InvalidOType(otype.to_string()));
66 }
67
68 if !VALID_OTYPES.contains(&otype) {
69 return Err(ORefParseError::UnknownOType(otype.to_string()));
70 }
71
72 Uuid::parse_str(oid).map_err(|_| ORefParseError::InvalidOID(oid.to_string()))?;
74
75 Ok(Self {
76 otype: otype.to_string(),
77 oid: oid.to_string(),
78 })
79 }
80}
81
82impl fmt::Display for ORef {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 if self.is_empty() {
85 return write!(f, "");
86 }
87 write!(f, "{}:{}", self.otype, self.oid)
88 }
89}
90
91#[derive(Debug, Clone, thiserror::Error)]
93pub enum ORefParseError {
94 #[error("invalid object reference: {0:?}")]
95 InvalidFormat(String),
96 #[error("invalid object type: {0:?}")]
97 InvalidOType(String),
98 #[error("unknown object type: {0:?}")]
99 UnknownOType(String),
100 #[error("invalid object id: {0:?}")]
101 InvalidOID(String),
102}
103
104impl ts_rs::TS for ORef {
121 type WithoutGenerics = Self;
122 type OptionInnerType = Self;
123 fn name(_: &ts_rs::Config) -> String {
124 "string".to_owned()
125 }
126 fn inline(cfg: &ts_rs::Config) -> String {
127 <Self as ts_rs::TS>::name(cfg)
128 }
129 fn inline_flattened(_: &ts_rs::Config) -> String {
130 panic!("ORef cannot be flattened")
131 }
132 fn decl(_: &ts_rs::Config) -> String {
133 panic!("ORef cannot be declared")
134 }
135 fn decl_concrete(_: &ts_rs::Config) -> String {
136 panic!("ORef cannot be declared")
137 }
138}
139
140impl Serialize for ORef {
143 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
144 serializer.serialize_str(&self.to_string())
145 }
146}
147
148impl<'de> Deserialize<'de> for ORef {
149 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
150 let s = String::deserialize(deserializer)?;
151 ORef::parse(&s).map_err(de::Error::custom)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
172 fn ts_binding_is_a_string_because_the_wire_is_a_string() {
173 #[derive(ts_rs::TS)]
178 #[allow(dead_code)]
179 struct Consumer {
180 oref: ORef,
181 }
182 let decl = <Consumer as ts_rs::TS>::decl(&ts_rs::Config::from_env());
183 assert!(
184 decl.contains("oref: string"),
185 "a field holding an ORef must generate `string`; got: {decl}"
186 );
187 assert!(
188 !decl.contains("otype"),
189 "the struct shape must not leak into the binding; got: {decl}"
190 );
191
192 let oref = ORef::new("block", "550e8400-e29b-41d4-a716-446655440000");
193 assert_eq!(
194 serde_json::to_string(&oref).unwrap(),
195 "\"block:550e8400-e29b-41d4-a716-446655440000\"",
196 "the wire form the binding above claims to describe"
197 );
198 }
199
200 #[test]
201 fn test_oref_roundtrip() {
202 let oref = ORef::new("block", "550e8400-e29b-41d4-a716-446655440000");
203 let json = serde_json::to_string(&oref).unwrap();
204 assert_eq!(json, r#""block:550e8400-e29b-41d4-a716-446655440000""#);
205
206 let parsed: ORef = serde_json::from_str(&json).unwrap();
207 assert_eq!(parsed, oref);
208 }
209
210 #[test]
211 fn test_oref_empty() {
212 let oref = ORef::parse("").unwrap();
213 assert!(oref.is_empty());
214 let json = serde_json::to_string(&oref).unwrap();
215 assert_eq!(json, r#""""#);
216 }
217
218 #[test]
219 fn test_oref_all_valid_types() {
220 let uuid = "550e8400-e29b-41d4-a716-446655440000";
221 for otype in VALID_OTYPES {
222 let s = format!("{otype}:{uuid}");
223 let oref = ORef::parse(&s).unwrap();
224 assert_eq!(oref.otype, *otype);
225 assert_eq!(oref.oid, uuid);
226 }
227 }
228
229 #[test]
230 fn test_oref_invalid_otype() {
231 let result = ORef::parse("BLOCK:550e8400-e29b-41d4-a716-446655440000");
232 assert!(result.is_err());
233 assert!(matches!(result, Err(ORefParseError::InvalidOType(_))));
234 }
235
236 #[test]
237 fn test_oref_unknown_otype() {
238 let result = ORef::parse("foobar:550e8400-e29b-41d4-a716-446655440000");
239 assert!(result.is_err());
240 assert!(matches!(result, Err(ORefParseError::UnknownOType(_))));
241 }
242
243 #[test]
244 fn test_oref_invalid_uuid() {
245 let result = ORef::parse("block:not-a-uuid");
246 assert!(result.is_err());
247 assert!(matches!(result, Err(ORefParseError::InvalidOID(_))));
248 }
249
250 #[test]
251 fn test_oref_no_colon() {
252 let result = ORef::parse("blockuuid");
253 assert!(result.is_err());
254 assert!(matches!(result, Err(ORefParseError::InvalidFormat(_))));
255 }
256
257 #[test]
258 fn test_oref_display() {
259 let oref = ORef::new("tab", "550e8400-e29b-41d4-a716-446655440000");
260 assert_eq!(oref.to_string(), "tab:550e8400-e29b-41d4-a716-446655440000");
261 }
262}