use std::time::SystemTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ObservationType {
Bugfix,
Decision,
Pattern,
Config,
Discovery,
Learning,
Architecture,
}
impl ObservationType {
pub fn as_db_str(&self) -> &'static str {
match self {
Self::Bugfix => "bugfix",
Self::Decision => "decision",
Self::Pattern => "pattern",
Self::Config => "config",
Self::Discovery => "discovery",
Self::Learning => "learning",
Self::Architecture => "architecture",
}
}
pub fn from_db_str(s: &str) -> Option<Self> {
match s {
"bugfix" => Some(Self::Bugfix),
"decision" => Some(Self::Decision),
"pattern" => Some(Self::Pattern),
"config" => Some(Self::Config),
"discovery" => Some(Self::Discovery),
"learning" => Some(Self::Learning),
"architecture" => Some(Self::Architecture),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SaveEntry {
pub sender_id: String,
#[serde(rename = "type")]
pub kind: ObservationType,
pub title: String,
pub what: Option<String>,
pub why: Option<String>,
#[serde(rename = "where")]
pub where_field: Option<String>,
pub learned: Option<String>,
}
impl SaveEntry {
pub fn new(
sender_id: impl Into<String>,
kind: ObservationType,
title: impl Into<String>,
) -> Self {
Self {
sender_id: sender_id.into(),
kind,
title: title.into(),
what: None,
why: None,
where_field: None,
learned: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Observation {
pub id: String,
pub sender_id: String,
#[serde(rename = "type")]
pub kind: ObservationType,
pub title: String,
pub what: Option<String>,
pub why: Option<String>,
#[serde(rename = "where")]
pub where_field: Option<String>,
pub learned: Option<String>,
pub created_at: SystemTime,
pub updated_at: SystemTime,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observation_type_round_trips_via_db_str() {
for kind in [
ObservationType::Bugfix,
ObservationType::Decision,
ObservationType::Pattern,
ObservationType::Config,
ObservationType::Discovery,
ObservationType::Learning,
ObservationType::Architecture,
] {
assert_eq!(
ObservationType::from_db_str(kind.as_db_str()),
Some(kind),
"round-trip failed for {kind:?}"
);
}
}
#[test]
fn unknown_db_str_returns_none() {
assert_eq!(ObservationType::from_db_str("bogus"), None);
assert_eq!(ObservationType::from_db_str(""), None);
assert_eq!(ObservationType::from_db_str("BUGFIX"), None); }
#[test]
fn save_entry_serde_round_trip() {
let entry = SaveEntry {
sender_id: "user".into(),
kind: ObservationType::Bugfix,
title: "Fixed N+1 query".into(),
what: Some("added eager loading".into()),
why: Some("12s pages on 5k users".into()),
where_field: Some("src/users/list.rs".into()),
learned: Some("FTS5 rewriter cannot fix N+1".into()),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"type\":\"bugfix\""), "json was {json}");
assert!(
json.contains("\"where\":\"src/users/list.rs\""),
"json was {json}"
);
let parsed: SaveEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.sender_id, entry.sender_id);
assert_eq!(parsed.kind, entry.kind);
assert_eq!(parsed.title, entry.title);
assert_eq!(parsed.what, entry.what);
assert_eq!(parsed.why, entry.why);
assert_eq!(parsed.where_field, entry.where_field);
assert_eq!(parsed.learned, entry.learned);
}
}