1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct AtomId(pub String);
12
13impl AtomId {
14 pub fn new(id: impl Into<String>) -> Self {
15 Self(id.into())
16 }
17}
18
19impl fmt::Display for AtomId {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 self.0.fmt(f)
22 }
23}
24
25#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct WorldKey(pub String);
28
29impl WorldKey {
30 pub fn new(key: impl Into<String>) -> Self {
31 Self(key.into())
32 }
33}
34
35impl fmt::Display for WorldKey {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 self.0.fmt(f)
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub struct WorkerKey(pub String);
44
45impl WorkerKey {
46 pub fn new(key: impl Into<String>) -> Self {
47 Self(key.into())
48 }
49}
50
51impl fmt::Display for WorkerKey {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 self.0.fmt(f)
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum AtomKind {
61 Observation,
62 Reflection,
63 Plan,
64 Action,
65 Message,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum LinkKind {
72 Supersedes,
73 References,
74 Confirms,
75 Contradicts,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
80pub struct Link {
81 pub target: AtomId,
82 pub kind: LinkKind,
83}
84
85impl Link {
86 pub fn new(target: AtomId, kind: LinkKind) -> Self {
87 Self { target, kind }
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
93pub struct Timestamp(pub String);
94
95impl Timestamp {
96 pub fn new(ts: impl Into<String>) -> Self {
97 Self(ts.into())
98 }
99}
100
101impl fmt::Display for Timestamp {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 self.0.fmt(f)
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
109pub struct Importance(f32);
110
111#[derive(Debug, thiserror::Error, PartialEq)]
112pub enum ImportanceError {
113 #[error("importance must be between 0.0 and 1.0 inclusive, got {0}")]
114 OutOfRange(f32),
115 #[error("importance cannot be NaN")]
116 NotANumber,
117}
118
119impl Importance {
120 pub fn new(value: f32) -> Result<Self, ImportanceError> {
122 if value.is_nan() {
123 return Err(ImportanceError::NotANumber);
124 }
125 if !(0.0..=1.0).contains(&value) {
126 return Err(ImportanceError::OutOfRange(value));
127 }
128 Ok(Self(value))
129 }
130
131 pub fn clamped(value: f32) -> Self {
133 if value.is_nan() {
134 return Self(0.0);
135 }
136 Self(value.clamp(0.0, 1.0))
137 }
138
139 pub fn get(self) -> f32 {
140 self.0
141 }
142}
143
144impl fmt::Display for Importance {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 self.0.fmt(f)
147 }
148}
149
150#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
155pub struct Atom {
156 id: AtomId,
157 world: WorldKey,
158 worker: WorkerKey,
159 kind: AtomKind,
160 timestamp: Timestamp,
161 importance: Importance,
162 payload_json: String,
163 vector: Option<Vec<f32>>,
164 flags: Vec<String>,
165 labels: Vec<String>,
166 links: Vec<Link>,
167}
168
169impl Atom {
170 #[allow(clippy::too_many_arguments)]
171 pub fn new(
172 id: AtomId,
173 world: WorldKey,
174 worker: WorkerKey,
175 kind: AtomKind,
176 timestamp: Timestamp,
177 importance: Importance,
178 payload_json: impl Into<String>,
179 vector: Option<Vec<f32>>,
180 flags: Vec<String>,
181 labels: Vec<String>,
182 links: Vec<Link>,
183 ) -> Self {
184 Self {
185 id,
186 world,
187 worker,
188 kind,
189 timestamp,
190 importance,
191 payload_json: payload_json.into(),
192 vector,
193 flags,
194 labels,
195 links,
196 }
197 }
198
199 pub fn builder(
200 id: AtomId,
201 world: WorldKey,
202 worker: WorkerKey,
203 kind: AtomKind,
204 timestamp: Timestamp,
205 importance: Importance,
206 payload_json: impl Into<String>,
207 ) -> AtomBuilder {
208 AtomBuilder {
209 id,
210 world,
211 worker,
212 kind,
213 timestamp,
214 importance,
215 payload_json: payload_json.into(),
216 vector: None,
217 flags: Vec::new(),
218 labels: Vec::new(),
219 links: Vec::new(),
220 }
221 }
222
223 pub fn id(&self) -> &AtomId {
224 &self.id
225 }
226
227 pub fn world(&self) -> &WorldKey {
228 &self.world
229 }
230
231 pub fn worker(&self) -> &WorkerKey {
232 &self.worker
233 }
234
235 pub fn kind(&self) -> &AtomKind {
236 &self.kind
237 }
238
239 pub fn timestamp(&self) -> &Timestamp {
240 &self.timestamp
241 }
242
243 pub fn importance(&self) -> Importance {
244 self.importance
245 }
246
247 pub fn payload_json(&self) -> &str {
248 &self.payload_json
249 }
250
251 pub fn vector(&self) -> Option<&[f32]> {
252 self.vector.as_deref()
253 }
254
255 pub fn flags(&self) -> &[String] {
256 &self.flags
257 }
258
259 pub fn labels(&self) -> &[String] {
260 &self.labels
261 }
262
263 pub fn links(&self) -> &[Link] {
264 &self.links
265 }
266}
267
268pub struct AtomBuilder {
270 id: AtomId,
271 world: WorldKey,
272 worker: WorkerKey,
273 kind: AtomKind,
274 timestamp: Timestamp,
275 importance: Importance,
276 payload_json: String,
277 vector: Option<Vec<f32>>,
278 flags: Vec<String>,
279 labels: Vec<String>,
280 links: Vec<Link>,
281}
282
283impl AtomBuilder {
284 pub fn vector(mut self, vector: Option<Vec<f32>>) -> Self {
285 self.vector = vector;
286 self
287 }
288
289 pub fn add_flag(mut self, flag: impl Into<String>) -> Self {
290 self.flags.push(flag.into());
291 self
292 }
293
294 pub fn add_label(mut self, label: impl Into<String>) -> Self {
295 self.labels.push(label.into());
296 self
297 }
298
299 pub fn add_link(mut self, link: AtomId) -> Self {
300 self.links.push(Link::new(link, LinkKind::References));
301 self
302 }
303
304 pub fn add_typed_link(mut self, target: AtomId, kind: LinkKind) -> Self {
305 self.links.push(Link::new(target, kind));
306 self
307 }
308
309 pub fn build(self) -> Atom {
310 Atom {
311 id: self.id,
312 world: self.world,
313 worker: self.worker,
314 kind: self.kind,
315 timestamp: self.timestamp,
316 importance: self.importance,
317 payload_json: self.payload_json,
318 vector: self.vector,
319 flags: self.flags,
320 labels: self.labels,
321 links: self.links,
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn sample_atom() -> Atom {
331 Atom::builder(
332 AtomId::new("a1"),
333 WorldKey::new("world-1"),
334 WorkerKey::new("worker-1"),
335 AtomKind::Observation,
336 Timestamp::new("2024-01-01T00:00:00Z"),
337 Importance::new(0.5).expect("importance"),
338 r#"{"foo":"bar"}"#,
339 )
340 .vector(Some(vec![1.0, 2.0, 3.0]))
341 .add_flag("immutable")
342 .add_label("test")
343 .add_link(AtomId::new("previous"))
344 .build()
345 }
346
347 #[test]
348 fn importance_validation() {
349 assert!(Importance::new(0.0).is_ok());
350 assert!(Importance::new(1.0).is_ok());
351 assert!(Importance::new(1.1).is_err());
352 assert!(Importance::new(f32::NAN).is_err());
353 assert_eq!(Importance::clamped(1.5).get(), 1.0);
354 assert_eq!(Importance::clamped(-1.0).get(), 0.0);
355 assert_eq!(Importance::clamped(f32::NAN).get(), 0.0);
356 }
357
358 #[test]
359 fn atom_kind_serde_names_are_stable() {
360 let kinds = [
361 (AtomKind::Observation, "observation"),
362 (AtomKind::Reflection, "reflection"),
363 (AtomKind::Plan, "plan"),
364 (AtomKind::Action, "action"),
365 (AtomKind::Message, "message"),
366 ];
367
368 for (kind, expected) in kinds {
369 let json = serde_json::to_string(&kind).unwrap();
370 assert_eq!(json, format!("\"{expected}\""));
371 }
372 }
373
374 #[cfg(feature = "bincode")]
375 #[test]
376 fn atom_roundtrip_bincode() {
377 let atom = sample_atom();
378 let bytes = bincode::serialize(&atom).expect("serialize");
379 let decoded: Atom = bincode::deserialize(&bytes).expect("deserialize");
380 assert_eq!(atom, decoded);
381 }
382
383 #[cfg(feature = "rmp-serde")]
384 #[test]
385 fn atom_roundtrip_rmp() {
386 let atom = sample_atom();
387 let bytes = rmp_serde::to_vec(&atom).expect("serialize");
388 let decoded: Atom = rmp_serde::from_slice(&bytes).expect("deserialize");
389 assert_eq!(atom, decoded);
390 }
391}