1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::common::LineRange;
5
6pub use super::v2::{Provenance, ProvenanceSource};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
14pub struct Annotation {
15 pub schema: String,
16 pub commit: String,
17 pub timestamp: String,
18
19 pub summary: String,
21
22 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub wisdom: Vec<WisdomEntry>,
25
26 pub provenance: Provenance,
28}
29
30impl Annotation {
31 pub fn validate(&self) -> Result<(), String> {
33 if self.schema != "chronicle/v3" {
34 return Err(format!("unsupported schema version: {}", self.schema));
35 }
36 if self.commit.is_empty() {
37 return Err("commit SHA is empty".to_string());
38 }
39 if self.summary.is_empty() {
40 return Err("summary is empty".to_string());
41 }
42 for (i, entry) in self.wisdom.iter().enumerate() {
43 if let Err(e) = entry.validate() {
44 return Err(format!("wisdom[{}]: {}", i, e));
45 }
46 }
47 Ok(())
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
56pub struct WisdomEntry {
57 pub category: WisdomCategory,
59
60 pub content: String,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub file: Option<String>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub lines: Option<LineRange>,
70}
71
72impl WisdomEntry {
73 pub fn validate(&self) -> Result<(), String> {
74 if self.content.is_empty() {
75 return Err("content is empty".to_string());
76 }
77 if let Some(lines) = &self.lines {
78 if lines.start > lines.end {
79 return Err(format!(
80 "invalid line range: start ({}) > end ({})",
81 lines.start, lines.end
82 ));
83 }
84 }
85 if self.lines.is_some() && self.file.is_none() {
86 return Err("lines specified without file".to_string());
87 }
88 Ok(())
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum WisdomCategory {
95 DeadEnd,
97 Gotcha,
99 Insight,
101 UnfinishedThread,
103}
104
105impl std::fmt::Display for WisdomCategory {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 match self {
108 Self::DeadEnd => write!(f, "dead_end"),
109 Self::Gotcha => write!(f, "gotcha"),
110 Self::Insight => write!(f, "insight"),
111 Self::UnfinishedThread => write!(f, "unfinished_thread"),
112 }
113 }
114}