1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum DocDiffLineKind {
13 Context,
14 Added,
15 Removed,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum DocDiffBlockKind {
21 Context,
22 Added,
23 Removed,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum DocDiffFileStatus {
29 Added,
30 Modified,
31 Deleted,
32 Renamed,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum DocDiffTimelinePointKind {
38 Commit,
39 Worktree,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct DocDiffLine {
45 pub kind: DocDiffLineKind,
46 pub old_line: Option<u32>,
47 pub new_line: Option<u32>,
48 pub text: String,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct DocDiffHunk {
54 pub old_start: u32,
55 pub old_lines: u32,
56 pub new_start: u32,
57 pub new_lines: u32,
58 pub lines: Vec<DocDiffLine>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct DocDiffBlock {
64 pub id: String,
65 pub kind: DocDiffBlockKind,
66 pub raw: String,
67 pub html: String,
68 pub old_index: Option<usize>,
69 pub new_index: Option<usize>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct DocDiffFile {
75 pub path: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub old_path: Option<String>,
78 pub status: DocDiffFileStatus,
79 pub added_lines: u32,
80 pub removed_lines: u32,
81 pub hunks: Vec<DocDiffHunk>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub blocks: Option<Vec<DocDiffBlock>>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "lowercase")]
88pub enum DocDiffFileTreeNode {
89 Group {
90 id: String,
91 label: String,
92 children: Vec<DocDiffFileTreeNode>,
93 },
94 #[serde(rename_all = "camelCase")]
98 File {
99 id: String,
100 label: String,
101 path: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 old_path: Option<String>,
104 status: DocDiffFileStatus,
105 added_lines: u32,
106 removed_lines: u32,
107 },
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct DocDiffTimelinePoint {
113 pub id: String,
114 pub kind: DocDiffTimelinePointKind,
115 pub hash: Option<String>,
116 pub short_hash: String,
117 pub subject: String,
118 pub author: Option<String>,
119 pub date: Option<String>,
121 pub base_ref: String,
122 pub head_ref: String,
123 pub files: Vec<DocDiffFile>,
124 pub file_tree: Vec<DocDiffFileTreeNode>,
125 pub total_added_lines: u32,
126 pub total_removed_lines: u32,
127 pub warnings: Vec<String>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct DocDiffReport {
133 pub mode: String,
135 pub base_ref: String,
136 pub head_ref: String,
137 pub generated_at: String,
138 pub timeline: Vec<DocDiffTimelinePoint>,
139 pub selected_point_id: Option<String>,
140 pub selected_file_path: Option<String>,
141 pub files: Vec<DocDiffFile>,
142 pub total_added_lines: u32,
143 pub total_removed_lines: u32,
144 pub warnings: Vec<String>,
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn line_kind_serializes_lowercase() {
153 assert_eq!(
154 serde_json::to_string(&DocDiffLineKind::Added).unwrap(),
155 r#""added""#
156 );
157 }
158
159 #[test]
160 fn file_status_serializes_lowercase() {
161 assert_eq!(
162 serde_json::to_string(&DocDiffFileStatus::Renamed).unwrap(),
163 r#""renamed""#
164 );
165 }
166
167 #[test]
168 fn file_omits_old_path_and_blocks_when_absent() {
169 let f = DocDiffFile {
170 path: "docs/a.md".into(),
171 old_path: None,
172 status: DocDiffFileStatus::Modified,
173 added_lines: 1,
174 removed_lines: 2,
175 hunks: vec![],
176 blocks: None,
177 };
178 let v = serde_json::to_string(&f).unwrap();
179 assert!(!v.contains("oldPath"));
180 assert!(!v.contains("blocks"));
181 assert!(v.contains(r#""addedLines":1"#));
182 assert!(v.contains(r#""removedLines":2"#));
183 }
184
185 #[test]
186 fn file_tree_node_uses_type_tag() {
187 let n = DocDiffFileTreeNode::Group {
188 id: "docs/dev".into(),
189 label: "dev".into(),
190 children: vec![],
191 };
192 let v = serde_json::to_string(&n).unwrap();
193 assert!(v.contains(r#""type":"group""#));
194 }
195
196 #[test]
197 fn file_tree_file_node_omits_old_path_when_absent() {
198 let n = DocDiffFileTreeNode::File {
199 id: "docs/a.md".into(),
200 label: "a.md".into(),
201 path: "docs/a.md".into(),
202 old_path: None,
203 status: DocDiffFileStatus::Added,
204 added_lines: 3,
205 removed_lines: 0,
206 };
207 let v = serde_json::to_string(&n).unwrap();
208 assert!(v.contains(r#""type":"file""#));
209 assert!(!v.contains("oldPath"));
210 assert!(v.contains(r#""addedLines":3"#));
213 assert!(v.contains(r#""removedLines":0"#));
214 assert!(!v.contains("added_lines"));
215 }
216}