1use crate::namespace::Namespace;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub enum NoteStatus {
8 Open,
9 Resolved,
10 Approved,
11 Rejected,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct Note {
16 pub id: Uuid, pub commit: String, pub file: Option<String>, pub line_start: Option<u32>,
20 pub line_end: Option<u32>,
21 pub body: String, pub author: String, pub timestamp: DateTime<Utc>,
24 pub namespace: Namespace,
25 pub thread_id: Option<Uuid>, pub status: NoteStatus,
27 pub tags: Vec<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub signature: Option<String>,
30}
31
32impl Note {
33 #[allow(clippy::too_many_arguments)]
34 pub fn new(
35 commit: String,
36 file: Option<String>,
37 line_start: Option<u32>,
38 line_end: Option<u32>,
39 body: String,
40 author: String,
41 namespace: Namespace,
42 ) -> Self {
43 Self {
44 id: Uuid::new_v4(),
45 commit,
46 file,
47 line_start,
48 line_end,
49 body,
50 author,
51 timestamp: Utc::now(),
52 namespace,
53 thread_id: None,
54 status: NoteStatus::Open,
55 tags: Vec::new(),
56 signature: None,
57 }
58 }
59
60 pub fn reply(parent: &Note, body: String, author: String) -> Self {
61 Self {
62 id: Uuid::new_v4(),
63 commit: parent.commit.clone(),
64 file: parent.file.clone(),
65 line_start: parent.line_start,
66 line_end: parent.line_end,
67 body,
68 author,
69 timestamp: Utc::now(),
70 namespace: parent.namespace.clone(),
71 thread_id: Some(parent.id),
72 status: NoteStatus::Open,
73 tags: Vec::new(),
74 signature: None,
75 }
76 }
77
78 pub fn signing_payload(&self) -> String {
80 format!(
81 "id: {}\ncommit: {}\nfile: {}\nline_start: {}\nbody: {}\nauthor: {}\ntimestamp: {}\n",
82 self.id,
83 self.commit,
84 self.file.as_deref().unwrap_or(""),
85 self.line_start.map(|l| l.to_string()).unwrap_or_default(),
86 self.body,
87 self.author,
88 self.timestamp.to_rfc3339()
89 )
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_note_new_with_all_fields() {
99 let commit = "a1b2c3d4e5f6".to_string();
100 let file = Some("src/main.rs".to_string());
101 let line_start = Some(10);
102 let line_end = Some(20);
103 let body = "This is a test note body.".to_string();
104 let author = "Tester <test@example.com>".to_string();
105 let namespace = Namespace::Comments;
106
107 let note = Note::new(
108 commit.clone(),
109 file.clone(),
110 line_start,
111 line_end,
112 body.clone(),
113 author.clone(),
114 namespace.clone(),
115 );
116
117 assert!(!note.id.is_nil());
118 assert_eq!(note.commit, commit);
119 assert_eq!(note.file, file);
120 assert_eq!(note.line_start, line_start);
121 assert_eq!(note.line_end, line_end);
122 assert_eq!(note.body, body);
123 assert_eq!(note.author, author);
124 assert_eq!(note.namespace, namespace);
125 assert_eq!(note.thread_id, None);
126 assert_eq!(note.status, NoteStatus::Open);
127 assert!(note.tags.is_empty());
128 }
129
130 #[test]
131 fn test_note_new_with_optional_fields_none() {
132 let commit = "a1b2c3d4e5f6".to_string();
133 let body = "Note without file or lines.".to_string();
134 let author = "Tester <test@example.com>".to_string();
135 let namespace = Namespace::Comments;
136
137 let note = Note::new(
138 commit.clone(),
139 None,
140 None,
141 None,
142 body.clone(),
143 author.clone(),
144 namespace.clone(),
145 );
146
147 assert!(!note.id.is_nil());
148 assert_eq!(note.file, None);
149 assert_eq!(note.line_start, None);
150 assert_eq!(note.line_end, None);
151 assert_eq!(note.status, NoteStatus::Open);
152 }
153
154 #[test]
155 fn test_note_reply() {
156 let parent = Note::new(
157 "a1b2c3d4e5f6".to_string(),
158 Some("src/lib.rs".to_string()),
159 Some(1),
160 Some(5),
161 "Parent note".to_string(),
162 "Parent Author <parent@example.com>".to_string(),
163 Namespace::Comments,
164 );
165
166 let reply_body = "This is a reply".to_string();
167 let reply_author = "Replier <replier@example.com>".to_string();
168
169 let reply = Note::reply(&parent, reply_body.clone(), reply_author.clone());
170
171 assert_ne!(reply.id, parent.id);
172 assert!(!reply.id.is_nil());
173 assert_eq!(reply.commit, parent.commit);
174 assert_eq!(reply.file, parent.file);
175 assert_eq!(reply.line_start, parent.line_start);
176 assert_eq!(reply.line_end, parent.line_end);
177 assert_eq!(reply.namespace, parent.namespace);
178 assert_eq!(reply.thread_id, Some(parent.id));
179 assert_eq!(reply.body, reply_body);
180 assert_eq!(reply.author, reply_author);
181 assert_eq!(reply.status, NoteStatus::Open);
182 assert!(reply.tags.is_empty());
183 assert_eq!(reply.signature, None);
184 }
185
186 #[test]
187 fn test_signing_payload_and_serde() {
188 let note = Note::new(
189 "a1b2c3d4e5f6".to_string(),
190 Some("src/lib.rs".to_string()),
191 Some(1),
192 Some(5),
193 "Parent note".to_string(),
194 "Parent Author <parent@example.com>".to_string(),
195 Namespace::Comments,
196 );
197
198 let payload = note.signing_payload();
199 assert!(payload.contains(&format!("id: {}", note.id)));
200 assert!(payload.contains("commit: a1b2c3d4e5f6"));
201 assert!(payload.contains("file: src/lib.rs"));
202 assert!(payload.contains("body: Parent note"));
203
204 let json = serde_json::to_string(¬e).unwrap();
206 assert!(!json.contains("signature"));
207
208 let mut deserialized: Note = serde_json::from_str(&json).unwrap();
209 assert_eq!(deserialized.signature, None);
210
211 deserialized.signature = Some("-----BEGIN SSH SIGNATURE-----\ntest\n-----END SSH SIGNATURE-----".to_string());
213 let signed_json = serde_json::to_string(&deserialized).unwrap();
214 assert!(signed_json.contains("signature"));
215 let from_signed: Note = serde_json::from_str(&signed_json).unwrap();
216 assert_eq!(from_signed.signature, deserialized.signature);
217 }
218}