grafos_store/
uri.rs

1//! Fabric URI type: `fabric://pool/bucket/key`.
2
3extern crate alloc;
4use alloc::format;
5use alloc::string::String;
6
7use core::fmt;
8use core::str::FromStr;
9
10use grafos_std::error::FabricError;
11use serde::{Deserialize, Serialize};
12
13/// A URI addressing an object in the fabric store.
14///
15/// Format: `fabric://pool/bucket/key`
16///
17/// - `pool`: logical cluster or namespace (e.g. `"default"`)
18/// - `bucket`: container within the pool
19/// - `key`: object name (may contain `/` for hierarchical keys)
20///
21/// # Parsing
22///
23/// ```rust
24/// use grafos_store::FabricUri;
25///
26/// let uri: FabricUri = "fabric://default/images/photo.png".parse().unwrap();
27/// assert_eq!(uri.pool(), "default");
28/// assert_eq!(uri.bucket(), "images");
29/// assert_eq!(uri.key(), "photo.png");
30/// ```
31#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub struct FabricUri {
33    pool: String,
34    bucket: String,
35    key: String,
36}
37
38impl FabricUri {
39    /// Create a new FabricUri from components.
40    ///
41    /// Returns an error if any component is empty.
42    pub fn new(pool: &str, bucket: &str, key: &str) -> Result<Self, FabricError> {
43        if pool.is_empty() || bucket.is_empty() || key.is_empty() {
44            return Err(FabricError::IoError(-10));
45        }
46        Ok(FabricUri {
47            pool: String::from(pool),
48            bucket: String::from(bucket),
49            key: String::from(key),
50        })
51    }
52
53    /// The pool component.
54    pub fn pool(&self) -> &str {
55        &self.pool
56    }
57
58    /// The bucket component.
59    pub fn bucket(&self) -> &str {
60        &self.bucket
61    }
62
63    /// The key component.
64    pub fn key(&self) -> &str {
65        &self.key
66    }
67
68    /// Returns the bucket-scoped key string: `"bucket/key"`.
69    pub fn bucket_key(&self) -> String {
70        format!("{}/{}", self.bucket, self.key)
71    }
72}
73
74impl FromStr for FabricUri {
75    type Err = FabricError;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        let rest = s
79            .strip_prefix("fabric://")
80            .ok_or(FabricError::IoError(-10))?;
81
82        // Split into pool / bucket / key (key may contain '/')
83        let slash1 = rest.find('/').ok_or(FabricError::IoError(-10))?;
84        let pool = &rest[..slash1];
85        let after_pool = &rest[slash1 + 1..];
86
87        let slash2 = after_pool.find('/').ok_or(FabricError::IoError(-10))?;
88        let bucket = &after_pool[..slash2];
89        let key = &after_pool[slash2 + 1..];
90
91        FabricUri::new(pool, bucket, key)
92    }
93}
94
95impl fmt::Display for FabricUri {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "fabric://{}/{}/{}", self.pool, self.bucket, self.key)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parse_roundtrip() {
107        let s = "fabric://default/mybucket/path/to/obj";
108        let uri: FabricUri = s.parse().unwrap();
109        assert_eq!(uri.pool(), "default");
110        assert_eq!(uri.bucket(), "mybucket");
111        assert_eq!(uri.key(), "path/to/obj");
112        assert_eq!(uri.to_string(), s);
113    }
114
115    #[test]
116    fn parse_simple() {
117        let uri: FabricUri = "fabric://prod/images/logo.png".parse().unwrap();
118        assert_eq!(uri.pool(), "prod");
119        assert_eq!(uri.bucket(), "images");
120        assert_eq!(uri.key(), "logo.png");
121    }
122
123    #[test]
124    fn parse_missing_scheme() {
125        let result = "http://pool/bucket/key".parse::<FabricUri>();
126        assert!(result.is_err());
127    }
128
129    #[test]
130    fn parse_missing_key() {
131        let result = "fabric://pool/bucket/".parse::<FabricUri>();
132        assert!(result.is_err());
133    }
134
135    #[test]
136    fn parse_missing_bucket() {
137        let result = "fabric://pool/".parse::<FabricUri>();
138        assert!(result.is_err());
139    }
140
141    #[test]
142    fn new_empty_component_fails() {
143        assert!(FabricUri::new("", "b", "k").is_err());
144        assert!(FabricUri::new("p", "", "k").is_err());
145        assert!(FabricUri::new("p", "b", "").is_err());
146    }
147
148    #[test]
149    fn display_format() {
150        let uri = FabricUri::new("cluster1", "data", "file.bin").unwrap();
151        assert_eq!(uri.to_string(), "fabric://cluster1/data/file.bin");
152    }
153
154    #[test]
155    fn bucket_key() {
156        let uri = FabricUri::new("p", "b", "k").unwrap();
157        assert_eq!(uri.bucket_key(), "b/k");
158    }
159
160    #[test]
161    fn serde_roundtrip() {
162        let uri = FabricUri::new("pool", "bucket", "key").unwrap();
163        let bytes = postcard::to_allocvec(&uri).unwrap();
164        let decoded: FabricUri = postcard::from_bytes(&bytes).unwrap();
165        assert_eq!(uri, decoded);
166    }
167}