1use std::time::SystemTime;
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum ObservationType {
25 Bugfix,
26 Decision,
27 Pattern,
28 Config,
29 Discovery,
30 Learning,
31 Architecture,
32}
33
34impl ObservationType {
35 pub fn as_db_str(&self) -> &'static str {
38 match self {
39 Self::Bugfix => "bugfix",
40 Self::Decision => "decision",
41 Self::Pattern => "pattern",
42 Self::Config => "config",
43 Self::Discovery => "discovery",
44 Self::Learning => "learning",
45 Self::Architecture => "architecture",
46 }
47 }
48
49 pub fn from_db_str(s: &str) -> Option<Self> {
54 match s {
55 "bugfix" => Some(Self::Bugfix),
56 "decision" => Some(Self::Decision),
57 "pattern" => Some(Self::Pattern),
58 "config" => Some(Self::Config),
59 "discovery" => Some(Self::Discovery),
60 "learning" => Some(Self::Learning),
61 "architecture" => Some(Self::Architecture),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
72#[non_exhaustive]
73pub struct SaveEntry {
74 pub sender_id: String,
75 #[serde(rename = "type")]
76 pub kind: ObservationType,
77 pub title: String,
78 pub what: Option<String>,
79 pub why: Option<String>,
80 #[serde(rename = "where")]
81 pub where_field: Option<String>,
82 pub learned: Option<String>,
83}
84
85impl SaveEntry {
86 pub fn new(
89 sender_id: impl Into<String>,
90 kind: ObservationType,
91 title: impl Into<String>,
92 ) -> Self {
93 Self {
94 sender_id: sender_id.into(),
95 kind,
96 title: title.into(),
97 what: None,
98 why: None,
99 where_field: None,
100 learned: None,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
113#[non_exhaustive]
114pub struct Observation {
115 pub id: String,
116 pub sender_id: String,
117 #[serde(rename = "type")]
118 pub kind: ObservationType,
119 pub title: String,
120 pub what: Option<String>,
121 pub why: Option<String>,
122 #[serde(rename = "where")]
123 pub where_field: Option<String>,
124 pub learned: Option<String>,
125 pub created_at: SystemTime,
126 pub updated_at: SystemTime,
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn observation_type_round_trips_via_db_str() {
135 for kind in [
136 ObservationType::Bugfix,
137 ObservationType::Decision,
138 ObservationType::Pattern,
139 ObservationType::Config,
140 ObservationType::Discovery,
141 ObservationType::Learning,
142 ObservationType::Architecture,
143 ] {
144 assert_eq!(
145 ObservationType::from_db_str(kind.as_db_str()),
146 Some(kind),
147 "round-trip failed for {kind:?}"
148 );
149 }
150 }
151
152 #[test]
153 fn unknown_db_str_returns_none() {
154 assert_eq!(ObservationType::from_db_str("bogus"), None);
155 assert_eq!(ObservationType::from_db_str(""), None);
156 assert_eq!(ObservationType::from_db_str("BUGFIX"), None); }
158
159 #[test]
160 fn save_entry_serde_round_trip() {
161 let entry = SaveEntry {
162 sender_id: "user".into(),
163 kind: ObservationType::Bugfix,
164 title: "Fixed N+1 query".into(),
165 what: Some("added eager loading".into()),
166 why: Some("12s pages on 5k users".into()),
167 where_field: Some("src/users/list.rs".into()),
168 learned: Some("FTS5 rewriter cannot fix N+1".into()),
169 };
170 let json = serde_json::to_string(&entry).unwrap();
171 assert!(json.contains("\"type\":\"bugfix\""), "json was {json}");
173 assert!(
174 json.contains("\"where\":\"src/users/list.rs\""),
175 "json was {json}"
176 );
177 let parsed: SaveEntry = serde_json::from_str(&json).unwrap();
178 assert_eq!(parsed.sender_id, entry.sender_id);
179 assert_eq!(parsed.kind, entry.kind);
180 assert_eq!(parsed.title, entry.title);
181 assert_eq!(parsed.what, entry.what);
182 assert_eq!(parsed.why, entry.why);
183 assert_eq!(parsed.where_field, entry.where_field);
184 assert_eq!(parsed.learned, entry.learned);
185 }
186}