1use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ObjectVersion {
10 pub version: u32,
12 pub cid: String,
14 pub size_bytes: u64,
16 pub created_at_secs: u64,
18 pub author: String,
20 pub message: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct StoredObject {
27 pub object_id: u64,
29 pub name: String,
31 pub namespace: String,
33 pub versions: Vec<ObjectVersion>,
35 pub tags: Vec<String>,
37 pub pinned: bool,
39}
40
41impl StoredObject {
42 pub fn current_version(&self) -> Option<&ObjectVersion> {
44 self.versions.last()
45 }
46
47 pub fn version_count(&self) -> usize {
49 self.versions.len()
50 }
51
52 pub fn total_size_bytes(&self) -> u64 {
54 self.versions.iter().map(|v| v.size_bytes).sum()
55 }
56
57 pub fn find_version(&self, v: u32) -> Option<&ObjectVersion> {
59 self.versions.iter().find(|ov| ov.version == v)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ObjectStoreStats {
66 pub total_objects: usize,
68 pub total_versions: usize,
70 pub total_size_bytes: u64,
72 pub pinned_objects: usize,
74 pub namespaces: usize,
76}
77
78#[derive(Debug, Default)]
81pub struct StorageObjectStore {
82 pub objects: HashMap<u64, StoredObject>,
84 pub name_index: HashMap<(String, String), u64>,
86 pub next_object_id: u64,
88}
89
90impl StorageObjectStore {
91 pub fn new() -> Self {
93 Self {
94 objects: HashMap::new(),
95 name_index: HashMap::new(),
96 next_object_id: 1,
97 }
98 }
99
100 #[allow(clippy::too_many_arguments)]
105 pub fn create(
106 &mut self,
107 name: &str,
108 namespace: &str,
109 cid: &str,
110 size_bytes: u64,
111 author: &str,
112 message: &str,
113 created_at_secs: u64,
114 ) -> u64 {
115 let key = (namespace.to_owned(), name.to_owned());
116 if let Some(&existing_id) = self.name_index.get(&key) {
117 return existing_id;
118 }
119
120 let object_id = self.next_object_id;
121 self.next_object_id += 1;
122
123 let version = ObjectVersion {
124 version: 1,
125 cid: cid.to_owned(),
126 size_bytes,
127 created_at_secs,
128 author: author.to_owned(),
129 message: message.to_owned(),
130 };
131
132 let stored = StoredObject {
133 object_id,
134 name: name.to_owned(),
135 namespace: namespace.to_owned(),
136 versions: vec![version],
137 tags: Vec::new(),
138 pinned: false,
139 };
140
141 self.objects.insert(object_id, stored);
142 self.name_index.insert(key, object_id);
143 object_id
144 }
145
146 pub fn add_version(
151 &mut self,
152 object_id: u64,
153 cid: &str,
154 size_bytes: u64,
155 author: &str,
156 message: &str,
157 created_at_secs: u64,
158 ) -> bool {
159 let Some(obj) = self.objects.get_mut(&object_id) else {
160 return false;
161 };
162
163 let next_version = obj.versions.iter().map(|v| v.version).max().unwrap_or(0) + 1;
164
165 obj.versions.push(ObjectVersion {
166 version: next_version,
167 cid: cid.to_owned(),
168 size_bytes,
169 created_at_secs,
170 author: author.to_owned(),
171 message: message.to_owned(),
172 });
173
174 true
175 }
176
177 pub fn get_by_name(&self, namespace: &str, name: &str) -> Option<&StoredObject> {
179 let key = (namespace.to_owned(), name.to_owned());
180 let id = self.name_index.get(&key)?;
181 self.objects.get(id)
182 }
183
184 pub fn get(&self, object_id: u64) -> Option<&StoredObject> {
186 self.objects.get(&object_id)
187 }
188
189 pub fn delete(&mut self, object_id: u64) -> bool {
193 let Some(obj) = self.objects.get(&object_id) else {
194 return false;
195 };
196
197 if obj.pinned {
198 return false;
199 }
200
201 let key = (obj.namespace.clone(), obj.name.clone());
202 self.objects.remove(&object_id);
203 self.name_index.remove(&key);
204 true
205 }
206
207 pub fn pin(&mut self, object_id: u64) -> bool {
211 let Some(obj) = self.objects.get_mut(&object_id) else {
212 return false;
213 };
214 obj.pinned = true;
215 true
216 }
217
218 pub fn unpin(&mut self, object_id: u64) -> bool {
222 let Some(obj) = self.objects.get_mut(&object_id) else {
223 return false;
224 };
225 obj.pinned = false;
226 true
227 }
228
229 pub fn add_tag(&mut self, object_id: u64, tag: &str) -> bool {
233 let Some(obj) = self.objects.get_mut(&object_id) else {
234 return false;
235 };
236 if !obj.tags.iter().any(|t| t == tag) {
237 obj.tags.push(tag.to_owned());
238 }
239 true
240 }
241
242 pub fn objects_in_namespace(&self, namespace: &str) -> Vec<&StoredObject> {
244 let mut result: Vec<&StoredObject> = self
245 .objects
246 .values()
247 .filter(|o| o.namespace == namespace)
248 .collect();
249 result.sort_by(|a, b| a.name.cmp(&b.name));
250 result
251 }
252
253 pub fn objects_with_tag(&self, tag: &str) -> Vec<&StoredObject> {
255 let mut result: Vec<&StoredObject> = self
256 .objects
257 .values()
258 .filter(|o| o.tags.iter().any(|t| t == tag))
259 .collect();
260 result.sort_by_key(|o| o.object_id);
261 result
262 }
263
264 pub fn stats(&self) -> ObjectStoreStats {
266 let total_objects = self.objects.len();
267 let total_versions = self.objects.values().map(|o| o.versions.len()).sum();
268 let total_size_bytes = self.objects.values().map(|o| o.total_size_bytes()).sum();
269 let pinned_objects = self.objects.values().filter(|o| o.pinned).count();
270 let namespaces = self
271 .objects
272 .values()
273 .map(|o| o.namespace.as_str())
274 .collect::<std::collections::HashSet<_>>()
275 .len();
276
277 ObjectStoreStats {
278 total_objects,
279 total_versions,
280 total_size_bytes,
281 pinned_objects,
282 namespaces,
283 }
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 fn make_store() -> StorageObjectStore {
292 StorageObjectStore::new()
293 }
294
295 fn create_simple(store: &mut StorageObjectStore, name: &str, namespace: &str) -> u64 {
298 store.create(name, namespace, "cid-abc", 100, "alice", "initial", 1000)
299 }
300
301 #[test]
304 fn test_new_starts_empty() {
305 let store = make_store();
306 assert!(store.objects.is_empty());
307 assert!(store.name_index.is_empty());
308 assert_eq!(store.next_object_id, 1);
309 }
310
311 #[test]
312 fn test_create_stores_object() {
313 let mut store = make_store();
314 let id = create_simple(&mut store, "obj1", "ns1");
315 let obj = store.get(id).expect("object should exist");
316 assert_eq!(obj.name, "obj1");
317 assert_eq!(obj.namespace, "ns1");
318 assert_eq!(obj.versions.len(), 1);
319 assert_eq!(obj.versions[0].version, 1);
320 assert_eq!(obj.versions[0].cid, "cid-abc");
321 }
322
323 #[test]
324 fn test_create_returns_incrementing_ids() {
325 let mut store = make_store();
326 let id1 = create_simple(&mut store, "a", "ns");
327 let id2 = create_simple(&mut store, "b", "ns");
328 let id3 = create_simple(&mut store, "c", "ns");
329 assert!(id1 < id2);
330 assert!(id2 < id3);
331 }
332
333 #[test]
334 fn test_create_registers_in_name_index() {
335 let mut store = make_store();
336 let id = create_simple(&mut store, "myobj", "mynamespace");
337 let indexed_id = store
338 .name_index
339 .get(&("mynamespace".to_owned(), "myobj".to_owned()))
340 .copied();
341 assert_eq!(indexed_id, Some(id));
342 }
343
344 #[test]
345 fn test_create_idempotent_same_namespace_name() {
346 let mut store = make_store();
347 let id1 = create_simple(&mut store, "dup", "ns");
348 let id2 = create_simple(&mut store, "dup", "ns");
349 assert_eq!(id1, id2);
350 assert_eq!(store.objects.len(), 1);
351 }
352
353 #[test]
354 fn test_add_version_appends_with_correct_version_number() {
355 let mut store = make_store();
356 let id = create_simple(&mut store, "v", "ns");
357 let ok = store.add_version(id, "cid-v2", 200, "bob", "second", 2000);
358 assert!(ok);
359 let obj = store.get(id).expect("object should exist");
360 assert_eq!(obj.versions.len(), 2);
361 assert_eq!(obj.versions[1].version, 2);
362 assert_eq!(obj.versions[1].cid, "cid-v2");
363
364 let ok2 = store.add_version(id, "cid-v3", 300, "carol", "third", 3000);
365 assert!(ok2);
366 assert_eq!(store.get(id).unwrap().versions[2].version, 3);
367 }
368
369 #[test]
370 fn test_add_version_false_for_unknown_object_id() {
371 let mut store = make_store();
372 let ok = store.add_version(9999, "cid", 100, "x", "msg", 0);
373 assert!(!ok);
374 }
375
376 #[test]
377 fn test_current_version_returns_last_version() {
378 let mut store = make_store();
379 let id = create_simple(&mut store, "cv", "ns");
380 store.add_version(id, "cid-v2", 200, "bob", "second", 2000);
381 let obj = store.get(id).unwrap();
382 let cv = obj.current_version().expect("should have current version");
383 assert_eq!(cv.version, 2);
384 assert_eq!(cv.cid, "cid-v2");
385 }
386
387 #[test]
388 fn test_current_version_none_when_no_versions() {
389 let obj = StoredObject {
391 object_id: 1,
392 name: "empty".to_owned(),
393 namespace: "ns".to_owned(),
394 versions: vec![],
395 tags: vec![],
396 pinned: false,
397 };
398 assert!(obj.current_version().is_none());
399 }
400
401 #[test]
402 fn test_find_version_by_number() {
403 let mut store = make_store();
404 let id = create_simple(&mut store, "fv", "ns");
405 store.add_version(id, "cid-v2", 200, "bob", "second", 2000);
406 store.add_version(id, "cid-v3", 300, "carol", "third", 3000);
407 let obj = store.get(id).unwrap();
408 let v2 = obj.find_version(2).expect("version 2 should exist");
409 assert_eq!(v2.cid, "cid-v2");
410 assert!(obj.find_version(99).is_none());
411 }
412
413 #[test]
414 fn test_version_count_correct() {
415 let mut store = make_store();
416 let id = create_simple(&mut store, "vc", "ns");
417 assert_eq!(store.get(id).unwrap().version_count(), 1);
418 store.add_version(id, "cid-v2", 200, "x", "m", 0);
419 assert_eq!(store.get(id).unwrap().version_count(), 2);
420 }
421
422 #[test]
423 fn test_get_by_name_some() {
424 let mut store = make_store();
425 let id = create_simple(&mut store, "named", "space");
426 let obj = store.get_by_name("space", "named");
427 assert!(obj.is_some());
428 assert_eq!(obj.unwrap().object_id, id);
429 }
430
431 #[test]
432 fn test_get_by_name_none() {
433 let store = make_store();
434 assert!(store.get_by_name("nonexistent", "nothing").is_none());
435 }
436
437 #[test]
438 fn test_get_some() {
439 let mut store = make_store();
440 let id = create_simple(&mut store, "g", "ns");
441 assert!(store.get(id).is_some());
442 }
443
444 #[test]
445 fn test_get_none() {
446 let store = make_store();
447 assert!(store.get(12345).is_none());
448 }
449
450 #[test]
451 fn test_delete_removes_object() {
452 let mut store = make_store();
453 let id = create_simple(&mut store, "del", "ns");
454 let ok = store.delete(id);
455 assert!(ok);
456 assert!(store.get(id).is_none());
457 assert!(!store
458 .name_index
459 .contains_key(&("ns".to_owned(), "del".to_owned())));
460 }
461
462 #[test]
463 fn test_delete_false_when_pinned() {
464 let mut store = make_store();
465 let id = create_simple(&mut store, "pinned-obj", "ns");
466 store.pin(id);
467 let ok = store.delete(id);
468 assert!(!ok);
469 assert!(store.get(id).is_some());
470 }
471
472 #[test]
473 fn test_delete_false_when_not_found() {
474 let mut store = make_store();
475 assert!(!store.delete(999));
476 }
477
478 #[test]
479 fn test_pin_sets_pinned_true() {
480 let mut store = make_store();
481 let id = create_simple(&mut store, "p", "ns");
482 assert!(!store.get(id).unwrap().pinned);
483 let ok = store.pin(id);
484 assert!(ok);
485 assert!(store.get(id).unwrap().pinned);
486 }
487
488 #[test]
489 fn test_pin_false_when_not_found() {
490 let mut store = make_store();
491 assert!(!store.pin(404));
492 }
493
494 #[test]
495 fn test_unpin_sets_pinned_false() {
496 let mut store = make_store();
497 let id = create_simple(&mut store, "u", "ns");
498 store.pin(id);
499 let ok = store.unpin(id);
500 assert!(ok);
501 assert!(!store.get(id).unwrap().pinned);
502 }
503
504 #[test]
505 fn test_unpin_false_when_not_found() {
506 let mut store = make_store();
507 assert!(!store.unpin(404));
508 }
509
510 #[test]
511 fn test_add_tag_idempotent() {
512 let mut store = make_store();
513 let id = create_simple(&mut store, "t", "ns");
514 assert!(store.add_tag(id, "important"));
515 assert!(store.add_tag(id, "important")); assert!(store.add_tag(id, "critical"));
517 let obj = store.get(id).unwrap();
518 assert_eq!(obj.tags.len(), 2);
519 assert!(obj.tags.contains(&"important".to_owned()));
520 assert!(obj.tags.contains(&"critical".to_owned()));
521 }
522
523 #[test]
524 fn test_add_tag_false_when_not_found() {
525 let mut store = make_store();
526 assert!(!store.add_tag(999, "tag"));
527 }
528
529 #[test]
530 fn test_objects_in_namespace_filtered_and_sorted() {
531 let mut store = make_store();
532 create_simple(&mut store, "zebra", "alpha");
533 create_simple(&mut store, "apple", "alpha");
534 create_simple(&mut store, "mango", "alpha");
535 create_simple(&mut store, "only-here", "beta");
536
537 let result = store.objects_in_namespace("alpha");
538 assert_eq!(result.len(), 3);
539 assert_eq!(result[0].name, "apple");
540 assert_eq!(result[1].name, "mango");
541 assert_eq!(result[2].name, "zebra");
542 }
543
544 #[test]
545 fn test_objects_in_namespace_empty_for_unknown() {
546 let store = make_store();
547 assert!(store.objects_in_namespace("no-such-ns").is_empty());
548 }
549
550 #[test]
551 fn test_objects_with_tag_sorted_by_object_id() {
552 let mut store = make_store();
553 let id1 = create_simple(&mut store, "a", "ns");
554 let id2 = create_simple(&mut store, "b", "ns");
555 let id3 = create_simple(&mut store, "c", "ns");
556 store.add_tag(id3, "hot");
557 store.add_tag(id1, "hot");
558 store.add_tag(id2, "hot");
559
560 let result = store.objects_with_tag("hot");
561 assert_eq!(result.len(), 3);
562 assert_eq!(result[0].object_id, id1);
563 assert_eq!(result[1].object_id, id2);
564 assert_eq!(result[2].object_id, id3);
565 }
566
567 #[test]
568 fn test_objects_with_tag_empty_for_unknown_tag() {
569 let mut store = make_store();
570 create_simple(&mut store, "a", "ns");
571 assert!(store.objects_with_tag("nosuch").is_empty());
572 }
573
574 #[test]
575 fn test_total_size_bytes_sums_all_versions() {
576 let mut store = make_store();
577 let id = store.create("s", "ns", "c1", 100, "a", "m", 0);
578 store.add_version(id, "c2", 200, "a", "m", 0);
579 store.add_version(id, "c3", 300, "a", "m", 0);
580 let obj = store.get(id).unwrap();
581 assert_eq!(obj.total_size_bytes(), 600);
582 }
583
584 #[test]
585 fn test_stats_namespaces_count() {
586 let mut store = make_store();
587 create_simple(&mut store, "a", "alpha");
588 create_simple(&mut store, "b", "alpha");
589 create_simple(&mut store, "c", "beta");
590 create_simple(&mut store, "d", "gamma");
591
592 let stats = store.stats();
593 assert_eq!(stats.namespaces, 3);
594 assert_eq!(stats.total_objects, 4);
595 }
596
597 #[test]
598 fn test_stats_pinned_objects_count() {
599 let mut store = make_store();
600 let id1 = create_simple(&mut store, "x", "ns");
601 let id2 = create_simple(&mut store, "y", "ns");
602 create_simple(&mut store, "z", "ns");
603 store.pin(id1);
604 store.pin(id2);
605
606 let stats = store.stats();
607 assert_eq!(stats.pinned_objects, 2);
608 }
609
610 #[test]
611 fn test_stats_total_versions_and_size() {
612 let mut store = make_store();
613 let id1 = store.create("a", "ns", "c1", 50, "x", "m", 0);
614 store.add_version(id1, "c2", 75, "x", "m", 0);
615 let _id2 = store.create("b", "ns", "c3", 100, "x", "m", 0);
616
617 let stats = store.stats();
618 assert_eq!(stats.total_versions, 3);
619 assert_eq!(stats.total_size_bytes, 225);
620 }
621}