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
59const NOTE_KIND_VALID: &[&str] = &[
60 "observation",
61 "insight",
62 "question",
63 "decision",
64 "reference",
65];
66
67impl core::str::FromStr for NoteKind {
68 type Err = crate::error::UnknownVariant;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 match s.trim().to_ascii_lowercase().as_str() {
72 "observation" | "obs" => Ok(Self::Observation),
73 "insight" | "finding" => Ok(Self::Insight),
74 "question" | "q" => Ok(Self::Question),
75 "decision" | "choice" => Ok(Self::Decision),
76 "reference" | "ref" | "citation" => Ok(Self::Reference),
77 other => Err(crate::error::UnknownVariant::new(
78 "note_kind",
79 other,
80 NOTE_KIND_VALID,
81 )),
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
90pub enum NoteStatus {
91 #[default]
92 Active,
93 Archived,
94}
95
96impl NoteStatus {
97 pub const fn name(self) -> &'static str {
98 match self {
99 Self::Active => "active",
100 Self::Archived => "archived",
101 }
102 }
103}
104
105impl fmt::Display for NoteStatus {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.write_str(self.name())
108 }
109}
110
111#[derive(Clone, Debug)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub struct Note {
115 #[cfg_attr(feature = "serde", serde(flatten))]
116 pub header: Header,
117 pub kind: NoteKind,
118 pub status: NoteStatus,
119 pub content: String,
120 pub properties: BTreeMap<String, PropertyValue>,
121 pub tags: Vec<String>,
122 pub salience: f64,
123 pub decay_factor: f64,
124 pub expires_at: Option<Timestamp>,
125 pub deleted_at: Option<Timestamp>,
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::{Id128, Namespace};
132
133 fn test_header() -> Header {
134 Header::new(
135 Id128::from_u128(1),
136 Namespace::default(),
137 Timestamp::from_secs(1700000000),
138 )
139 }
140
141 #[test]
142 fn note_kind_all_have_names() {
143 for kind in NoteKind::ALL {
144 assert!(!kind.name().is_empty());
145 }
146 }
147
148 #[test]
149 fn note_kind_default_is_observation() {
150 assert_eq!(NoteKind::default(), NoteKind::Observation);
151 }
152
153 #[test]
154 fn note_kind_display_roundtrip() {
155 use core::str::FromStr;
156 for kind in NoteKind::ALL {
157 let s = alloc::format!("{kind}");
158 let parsed = NoteKind::from_str(&s).unwrap();
159 assert_eq!(parsed, kind);
160 }
161 }
162
163 #[test]
164 fn note_kind_from_str_case_insensitive() {
165 use core::str::FromStr;
166 assert_eq!(
167 NoteKind::from_str("OBSERVATION").unwrap(),
168 NoteKind::Observation
169 );
170 assert_eq!(NoteKind::from_str("Insight").unwrap(), NoteKind::Insight);
171 }
172
173 #[test]
174 fn note_kind_from_str_aliases() {
175 use core::str::FromStr;
176 assert_eq!(NoteKind::from_str("obs").unwrap(), NoteKind::Observation);
177 assert_eq!(NoteKind::from_str("finding").unwrap(), NoteKind::Insight);
178 assert_eq!(NoteKind::from_str("q").unwrap(), NoteKind::Question);
179 assert_eq!(NoteKind::from_str("choice").unwrap(), NoteKind::Decision);
180 assert_eq!(NoteKind::from_str("ref").unwrap(), NoteKind::Reference);
181 assert_eq!(NoteKind::from_str("citation").unwrap(), NoteKind::Reference);
182 }
183
184 #[test]
185 fn note_kind_from_str_unknown_errors() {
186 use core::str::FromStr;
187 let err = NoteKind::from_str("garbage").unwrap_err();
188 assert_eq!(err.domain, "note_kind");
189 assert_eq!(err.value, "garbage");
190 assert!(err.valid.contains(&"observation"));
191 }
192
193 #[test]
194 fn note_construction() {
195 let note = Note {
196 header: test_header(),
197 kind: NoteKind::Decision,
198 status: NoteStatus::Active,
199 content: String::from("Use BGE-base for multilingual corpus"),
200 properties: BTreeMap::new(),
201 tags: alloc::vec!["retrieval".into()],
202 salience: 0.8,
203 decay_factor: 0.01,
204 expires_at: None,
205 deleted_at: None,
206 };
207 assert_eq!(note.kind, NoteKind::Decision);
208 assert_eq!(note.tags.len(), 1);
209 }
210}