csm_memory/
concept_builder.rs1use serde::Serialize;
4use std::collections::HashMap;
5
6use crate::singularity::Concept;
7use csm_core_lib::error::{MemoryError, Result};
8use csm_core_lib::hyperdim::HVec10240;
9
10#[derive(Debug)]
25pub struct ConceptBuilder {
26 id: String,
27 vector: Option<HVec10240>,
28 metadata: HashMap<String, serde_json::Value>,
29 metadata_error: Option<MemoryError>,
30 ttl_seconds: Option<u64>,
31 canonical_concept_ids: Vec<String>,
32}
33
34impl ConceptBuilder {
35 #[must_use]
37 pub fn new(id: impl Into<String>) -> Self {
38 Self {
39 id: id.into(),
40 vector: None,
41 metadata: HashMap::new(),
42 metadata_error: None,
43 ttl_seconds: None,
44 canonical_concept_ids: Vec::new(),
45 }
46 }
47
48 #[must_use]
50 pub fn with_canonical_concepts(mut self, ids: Vec<String>) -> Self {
51 self.canonical_concept_ids = ids;
52 self
53 }
54
55 #[must_use]
60 pub const fn with_ttl(mut self, ttl_seconds: u64) -> Self {
61 self.ttl_seconds = Some(ttl_seconds);
62 self
63 }
64
65 #[must_use]
67 pub const fn with_vector(mut self, vector: HVec10240) -> Self {
68 self.vector = Some(vector);
69 self
70 }
71
72 #[must_use]
77 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
78 if self.metadata_error.is_none() {
79 match serde_json::to_value(value) {
80 Ok(value) => {
81 self.metadata.insert(key.into(), value);
82 }
83 Err(error) => {
84 self.metadata_error = Some(MemoryError::Serialization(error));
85 }
86 }
87 }
88 self
89 }
90
91 pub fn build(self) -> Result<Concept> {
97 if let Some(error) = self.metadata_error {
98 return Err(error);
99 }
100
101 let now = crate::singularity::unix_now_secs();
102 let expires_at = self.ttl_seconds.map(|ttl| now + ttl);
103
104 Ok(Concept {
105 id: self.id,
106 vector: self.vector.unwrap_or_else(HVec10240::random),
107 metadata: self.metadata,
108 created_at: now,
109 modified_at: now,
110 expires_at,
111 canonical_concept_ids: self.canonical_concept_ids,
112 })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
119 use super::*;
120
121 #[test]
122 fn concept_builder_creates_concept_with_metadata() {
123 let concept = ConceptBuilder::new("test-id")
124 .with_vector(HVec10240::random())
125 .with_metadata("key1", "value1")
126 .with_metadata("key2", 42i32)
127 .build()
128 .unwrap();
129
130 assert_eq!(concept.id, "test-id");
131 assert_eq!(
132 concept.metadata.get("key1").unwrap().as_str().unwrap(),
133 "value1"
134 );
135 assert_eq!(concept.metadata.get("key2").unwrap().as_i64().unwrap(), 42);
136 }
137
138 #[test]
139 fn concept_builder_uses_random_vector_by_default() {
140 let concept = ConceptBuilder::new("test").build().unwrap();
141 assert_eq!(concept.id, "test");
143 }
144
145 #[test]
146 fn concept_builder_with_ttl_sets_expiration() {
147 let now = crate::singularity::unix_now_secs();
148 let concept = ConceptBuilder::new("ttl-test")
149 .with_ttl(3600)
150 .build()
151 .unwrap();
152
153 assert!(concept.expires_at.is_some());
154 let expires_at = concept.expires_at.unwrap();
155 assert!(expires_at >= now + 3600 - 1);
157 assert!(expires_at <= now + 3600 + 1);
158 }
159
160 #[test]
161 fn concept_builder_without_ttl_has_no_expiration() {
162 let concept = ConceptBuilder::new("no-ttl").build().unwrap();
163 assert!(concept.expires_at.is_none());
164 }
165}