grafos_store/
bucket.rs

1//! Bucket management for organizing objects into logical containers.
2
3extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use grafos_std::error::{FabricError, Result};
8use grafos_std::host;
9
10use serde::{Deserialize, Serialize};
11
12use crate::bucket_config::{BucketConfig, BucketTier};
13use crate::bucket_handle::BucketHandle;
14use crate::bucket_locator::BucketLocator;
15
16/// Metadata for a bucket.
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
18pub struct BucketInfo {
19    /// Bucket name.
20    pub name: String,
21    /// Pool the bucket belongs to.
22    pub pool: String,
23    /// Creation timestamp (unix seconds).
24    pub created_at: u64,
25}
26
27/// Manages creation and lifecycle of object store buckets.
28///
29/// Buckets are logical containers within a pool. The bucket manager
30/// tracks which buckets exist and their metadata.
31///
32/// # Example
33///
34/// ```rust
35/// use grafos_store::BucketManager;
36///
37/// let mut mgr = BucketManager::new("default");
38/// mgr.create("images").unwrap();
39/// mgr.create("logs").unwrap();
40///
41/// assert!(mgr.exists("images"));
42/// assert_eq!(mgr.list().len(), 2);
43/// ```
44pub struct BucketManager {
45    pool: String,
46    buckets: Vec<BucketInfo>,
47    generation_counter: u64,
48    published: Vec<BucketLocator>,
49}
50
51impl BucketManager {
52    /// Create a new bucket manager for the given pool.
53    pub fn new(pool: &str) -> Self {
54        BucketManager {
55            pool: String::from(pool),
56            buckets: Vec::new(),
57            generation_counter: 0,
58            published: Vec::new(),
59        }
60    }
61
62    /// Create a new bucket. Returns an error if the bucket already exists.
63    pub fn create(&mut self, name: &str) -> Result<&BucketInfo> {
64        if self.exists(name) {
65            return Err(FabricError::IoError(-11));
66        }
67        let now = host::unix_time_secs();
68        let info = BucketInfo {
69            name: String::from(name),
70            pool: self.pool.clone(),
71            created_at: now,
72        };
73        self.buckets.push(info);
74
75        #[cfg(feature = "observe")]
76        crate::observe_hooks::on_bucket_created(name);
77
78        Ok(self.buckets.last().unwrap())
79    }
80
81    /// Check if a bucket exists.
82    pub fn exists(&self, name: &str) -> bool {
83        self.buckets.iter().any(|b| b.name == name)
84    }
85
86    /// Get bucket info by name.
87    pub fn get(&self, name: &str) -> Option<&BucketInfo> {
88        self.buckets.iter().find(|b| b.name == name)
89    }
90
91    /// Drop (delete) a bucket by name. Returns `true` if it existed.
92    pub fn drop_bucket(&mut self, name: &str) -> bool {
93        let len_before = self.buckets.len();
94        self.buckets.retain(|b| b.name != name);
95        self.buckets.len() < len_before
96    }
97
98    /// List all bucket names.
99    pub fn list(&self) -> Vec<&str> {
100        self.buckets.iter().map(|b| b.name.as_str()).collect()
101    }
102
103    /// The pool this manager belongs to.
104    pub fn pool(&self) -> &str {
105        &self.pool
106    }
107
108    /// Create a bucket from a configuration, returning a typed handle.
109    pub fn create_bucket(&mut self, config: BucketConfig) -> Result<BucketHandle> {
110        if self.exists(&config.name) {
111            return Err(FabricError::IoError(-11));
112        }
113        let now = host::unix_time_secs();
114        self.generation_counter += 1;
115        let info = BucketInfo {
116            name: config.name.clone(),
117            pool: self.pool.clone(),
118            created_at: now,
119        };
120        self.buckets.push(info);
121        Ok(BucketHandle {
122            name: config.name,
123            pool: self.pool.clone(),
124            tier: config.tier,
125            created_at: now,
126            generation: self.generation_counter,
127        })
128    }
129
130    /// Open an existing bucket by name, returning a typed handle.
131    pub fn open_bucket(&self, name: &str) -> Result<BucketHandle> {
132        let info = self.get(name).ok_or(FabricError::IoError(-4))?;
133        Ok(BucketHandle {
134            name: info.name.clone(),
135            pool: info.pool.clone(),
136            tier: BucketTier::Memory, // default; tier info not stored in BucketInfo
137            created_at: info.created_at,
138            generation: self.generation_counter,
139        })
140    }
141
142    /// Publish a bucket handle as a locator for cross-app discovery.
143    pub fn publish(&mut self, handle: &BucketHandle) -> BucketLocator {
144        let locator = BucketLocator {
145            version: 1,
146            name: handle.name.clone(),
147            pool: handle.pool.clone(),
148            tier: handle.tier.clone(),
149            generation: handle.generation,
150            created_at: handle.created_at,
151        };
152        // Replace existing locator for same name, or push new
153        if let Some(pos) = self.published.iter().position(|l| l.name == locator.name) {
154            self.published[pos] = locator.clone();
155        } else {
156            self.published.push(locator.clone());
157        }
158        locator
159    }
160
161    /// Discover a previously published bucket locator by name.
162    pub fn discover(&self, name: &str) -> Option<&BucketLocator> {
163        self.published.iter().find(|l| l.name == name)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use grafos_std::host;
171
172    fn setup() {
173        host::reset_mock();
174    }
175
176    #[test]
177    fn create_and_exists() {
178        setup();
179        let mut mgr = BucketManager::new("pool");
180        assert!(!mgr.exists("b1"));
181
182        mgr.create("b1").unwrap();
183        assert!(mgr.exists("b1"));
184    }
185
186    #[test]
187    fn create_duplicate_fails() {
188        setup();
189        let mut mgr = BucketManager::new("pool");
190        mgr.create("b1").unwrap();
191        assert!(mgr.create("b1").is_err());
192    }
193
194    #[test]
195    fn drop_bucket() {
196        setup();
197        let mut mgr = BucketManager::new("pool");
198        mgr.create("b1").unwrap();
199        mgr.create("b2").unwrap();
200
201        assert!(mgr.drop_bucket("b1"));
202        assert!(!mgr.exists("b1"));
203        assert!(mgr.exists("b2"));
204        assert!(!mgr.drop_bucket("b1")); // already gone
205    }
206
207    #[test]
208    fn list_buckets() {
209        setup();
210        let mut mgr = BucketManager::new("pool");
211        mgr.create("alpha").unwrap();
212        mgr.create("beta").unwrap();
213        mgr.create("gamma").unwrap();
214
215        let mut names = mgr.list();
216        names.sort();
217        assert_eq!(names, vec!["alpha", "beta", "gamma"]);
218    }
219
220    #[test]
221    fn get_bucket_info() {
222        setup();
223        let mut mgr = BucketManager::new("mypool");
224        mgr.create("data").unwrap();
225
226        let info = mgr.get("data").unwrap();
227        assert_eq!(info.name, "data");
228        assert_eq!(info.pool, "mypool");
229    }
230
231    #[test]
232    fn pool_name() {
233        let mgr = BucketManager::new("test");
234        assert_eq!(mgr.pool(), "test");
235    }
236
237    #[test]
238    fn create_bucket_from_config() {
239        setup();
240        use crate::bucket_config::{BucketConfig, BucketTier};
241        let mut mgr = BucketManager::new("pool");
242        let config = BucketConfig::new("data").tier(BucketTier::Block);
243        let handle = mgr.create_bucket(config).unwrap();
244        assert_eq!(handle.name(), "data");
245        assert_eq!(handle.pool(), "pool");
246        assert_eq!(*handle.tier(), BucketTier::Block);
247        assert_eq!(handle.generation(), 1);
248        assert!(mgr.exists("data"));
249    }
250
251    #[test]
252    fn create_bucket_duplicate_fails() {
253        setup();
254        use crate::bucket_config::BucketConfig;
255        let mut mgr = BucketManager::new("pool");
256        mgr.create_bucket(BucketConfig::new("dup")).unwrap();
257        assert!(mgr.create_bucket(BucketConfig::new("dup")).is_err());
258    }
259
260    #[test]
261    fn open_bucket() {
262        setup();
263        use crate::bucket_config::BucketConfig;
264        let mut mgr = BucketManager::new("pool");
265        mgr.create_bucket(BucketConfig::new("open-me")).unwrap();
266
267        let handle = mgr.open_bucket("open-me").unwrap();
268        assert_eq!(handle.name(), "open-me");
269        assert_eq!(handle.pool(), "pool");
270    }
271
272    #[test]
273    fn open_bucket_not_found() {
274        let mgr = BucketManager::new("pool");
275        assert!(mgr.open_bucket("nope").is_err());
276    }
277
278    #[test]
279    fn publish_and_discover() {
280        setup();
281        use crate::bucket_config::{BucketConfig, BucketTier};
282        let mut mgr = BucketManager::new("pool");
283        let handle = mgr
284            .create_bucket(BucketConfig::new("shared").tier(BucketTier::Memory))
285            .unwrap();
286
287        let locator = mgr.publish(&handle);
288        assert_eq!(locator.version, 1);
289        assert_eq!(locator.name, "shared");
290        assert_eq!(locator.pool, "pool");
291        assert_eq!(locator.tier, BucketTier::Memory);
292        assert_eq!(locator.generation, 1);
293
294        let found = mgr.discover("shared").unwrap();
295        assert_eq!(found, &locator);
296        assert!(mgr.discover("missing").is_none());
297    }
298
299    #[test]
300    fn publish_replaces_existing() {
301        setup();
302        use crate::bucket_config::BucketConfig;
303        let mut mgr = BucketManager::new("pool");
304        let h1 = mgr.create_bucket(BucketConfig::new("b")).unwrap();
305        mgr.publish(&h1);
306
307        // Drop and recreate the bucket for a new generation
308        mgr.drop_bucket("b");
309        let h2 = mgr.create_bucket(BucketConfig::new("b")).unwrap();
310        mgr.publish(&h2);
311
312        let found = mgr.discover("b").unwrap();
313        assert_eq!(found.generation, 2);
314    }
315
316    #[test]
317    fn bucket_handle_uri() {
318        setup();
319        use crate::bucket_config::BucketConfig;
320        let mut mgr = BucketManager::new("pool");
321        let handle = mgr.create_bucket(BucketConfig::new("bkt")).unwrap();
322        let uri = handle.uri("my-key").unwrap();
323        assert_eq!(uri.pool(), "pool");
324        assert_eq!(uri.bucket(), "bkt");
325        assert_eq!(uri.key(), "my-key");
326    }
327
328    #[test]
329    fn generation_increments() {
330        setup();
331        use crate::bucket_config::BucketConfig;
332        let mut mgr = BucketManager::new("pool");
333        let h1 = mgr.create_bucket(BucketConfig::new("a")).unwrap();
334        let h2 = mgr.create_bucket(BucketConfig::new("b")).unwrap();
335        assert_eq!(h1.generation(), 1);
336        assert_eq!(h2.generation(), 2);
337    }
338}