1extern crate alloc;
4use alloc::collections::BTreeMap;
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::entity::PropertyValue;
10use crate::{Header, Timestamp};
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
19pub enum NoteKind {
20 #[default]
22 Observation,
23 Insight,
25 Question,
27 Decision,
29 Reference,
31}
32
33impl NoteKind {
34 pub const ALL: [Self; 5] = [
35 Self::Observation,
36 Self::Insight,
37 Self::Question,
38 Self::Decision,
39 Self::Reference,
40 ];
41
42 pub const fn name(self) -> &'static str {
43 match self {
44 Self::Observation => "observation",
45 Self::Insight => "insight",
46 Self::Question => "question",
47 Self::Decision => "decision",
48 Self::Reference => "reference",
49 }
50 }
51}
52
53impl fmt::Display for NoteKind {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(self.name())
56 }
57}
58
59impl core::str::FromStr for NoteKind {
60 type Err = alloc::string::String;
61
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 match s.trim().to_ascii_lowercase().as_str() {
64 "observation" | "obs" => Ok(Self::Observation),
65 "insight" | "finding" => Ok(Self::Insight),
66 "question" | "q" => Ok(Self::Question),
67 "decision" | "choice" => Ok(Self::Decision),
68 "reference" | "ref" | "citation" => Ok(Self::Reference),
69 other => Err(alloc::format!(
70 "unknown note kind: {other:?}. Valid: observation | insight | question | decision | reference"
71 )),
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
80pub enum NoteStatus {
81 #[default]
82 Active,
83 Archived,
84}
85
86impl NoteStatus {
87 pub const fn name(self) -> &'static str {
88 match self {
89 Self::Active => "active",
90 Self::Archived => "archived",
91 }
92 }
93}
94
95impl fmt::Display for NoteStatus {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(self.name())
98 }
99}
100
101#[derive(Clone, Debug)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub struct Note {
105 #[cfg_attr(feature = "serde", serde(flatten))]
106 pub header: Header,
107 pub kind: NoteKind,
108 pub status: NoteStatus,
109 pub content: String,
110 pub properties: BTreeMap<String, PropertyValue>,
111 pub tags: Vec<String>,
112 pub salience: f64,
113 pub decay_factor: f64,
114 pub expires_at: Option<Timestamp>,
115 pub deleted_at: Option<Timestamp>,
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::{Id128, Namespace};
122
123 fn test_header() -> Header {
124 Header::new(
125 Id128::from_u128(1),
126 Namespace::default(),
127 Timestamp::from_secs(1700000000),
128 )
129 }
130
131 #[test]
132 fn note_kind_all_have_names() {
133 for kind in NoteKind::ALL {
134 assert!(!kind.name().is_empty());
135 }
136 }
137
138 #[test]
139 fn note_kind_default_is_observation() {
140 assert_eq!(NoteKind::default(), NoteKind::Observation);
141 }
142
143 #[test]
144 fn note_kind_display_roundtrip() {
145 use core::str::FromStr;
146 for kind in NoteKind::ALL {
147 let s = alloc::format!("{kind}");
148 let parsed = NoteKind::from_str(&s).unwrap();
149 assert_eq!(parsed, kind);
150 }
151 }
152
153 #[test]
154 fn note_kind_from_str_case_insensitive() {
155 use core::str::FromStr;
156 assert_eq!(
157 NoteKind::from_str("OBSERVATION").unwrap(),
158 NoteKind::Observation
159 );
160 assert_eq!(NoteKind::from_str("Insight").unwrap(), NoteKind::Insight);
161 }
162
163 #[test]
164 fn note_kind_from_str_aliases() {
165 use core::str::FromStr;
166 assert_eq!(NoteKind::from_str("obs").unwrap(), NoteKind::Observation);
167 assert_eq!(NoteKind::from_str("finding").unwrap(), NoteKind::Insight);
168 assert_eq!(NoteKind::from_str("q").unwrap(), NoteKind::Question);
169 assert_eq!(NoteKind::from_str("choice").unwrap(), NoteKind::Decision);
170 assert_eq!(NoteKind::from_str("ref").unwrap(), NoteKind::Reference);
171 assert_eq!(NoteKind::from_str("citation").unwrap(), NoteKind::Reference);
172 }
173
174 #[test]
175 fn note_kind_from_str_unknown_errors() {
176 use core::str::FromStr;
177 let err = NoteKind::from_str("garbage").unwrap_err();
178 assert!(err.contains("unknown note kind"));
179 assert!(err.contains("observation"));
180 }
181
182 #[test]
183 fn note_construction() {
184 let note = Note {
185 header: test_header(),
186 kind: NoteKind::Decision,
187 status: NoteStatus::Active,
188 content: String::from("Use BGE-base for multilingual corpus"),
189 properties: BTreeMap::new(),
190 tags: alloc::vec!["retrieval".into()],
191 salience: 0.8,
192 decay_factor: 0.01,
193 expires_at: None,
194 deleted_at: None,
195 };
196 assert_eq!(note.kind, NoteKind::Decision);
197 assert_eq!(note.tags.len(), 1);
198 }
199}