1extern 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#[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 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 pub fn pool(&self) -> &str {
55 &self.pool
56 }
57
58 pub fn bucket(&self) -> &str {
60 &self.bucket
61 }
62
63 pub fn key(&self) -> &str {
65 &self.key
66 }
67
68 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 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}