Skip to main content

gn_core/
note.rs

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,             // content-addressed UUID v4
17    pub commit: String,       // git commit SHA this note is anchored to
18    pub file: Option<String>, // relative file path
19    pub line_start: Option<u32>,
20    pub line_end: Option<u32>,
21    pub body: String,   // markdown body
22    pub author: String, // "Name <email>"
23    pub timestamp: DateTime<Utc>,
24    pub namespace: Namespace,
25    pub thread_id: Option<Uuid>, // for replies — parent note id
26    pub status: NoteStatus,
27    pub tags: Vec<String>,
28}
29
30impl Note {
31    #[allow(clippy::too_many_arguments)]
32    pub fn new(
33        commit: String,
34        file: Option<String>,
35        line_start: Option<u32>,
36        line_end: Option<u32>,
37        body: String,
38        author: String,
39        namespace: Namespace,
40    ) -> Self {
41        Self {
42            id: Uuid::new_v4(),
43            commit,
44            file,
45            line_start,
46            line_end,
47            body,
48            author,
49            timestamp: Utc::now(),
50            namespace,
51            thread_id: None,
52            status: NoteStatus::Open,
53            tags: Vec::new(),
54        }
55    }
56
57    pub fn reply(parent: &Note, body: String, author: String) -> Self {
58        Self {
59            id: Uuid::new_v4(),
60            commit: parent.commit.clone(),
61            file: parent.file.clone(),
62            line_start: parent.line_start,
63            line_end: parent.line_end,
64            body,
65            author,
66            timestamp: Utc::now(),
67            namespace: parent.namespace.clone(),
68            thread_id: Some(parent.id),
69            status: NoteStatus::Open,
70            tags: Vec::new(),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_note_new_with_all_fields() {
81        let commit = "a1b2c3d4e5f6".to_string();
82        let file = Some("src/main.rs".to_string());
83        let line_start = Some(10);
84        let line_end = Some(20);
85        let body = "This is a test note body.".to_string();
86        let author = "Tester <test@example.com>".to_string();
87        let namespace = Namespace::Comments;
88
89        let note = Note::new(
90            commit.clone(),
91            file.clone(),
92            line_start,
93            line_end,
94            body.clone(),
95            author.clone(),
96            namespace.clone(),
97        );
98
99        assert!(!note.id.is_nil());
100        assert_eq!(note.commit, commit);
101        assert_eq!(note.file, file);
102        assert_eq!(note.line_start, line_start);
103        assert_eq!(note.line_end, line_end);
104        assert_eq!(note.body, body);
105        assert_eq!(note.author, author);
106        assert_eq!(note.namespace, namespace);
107        assert_eq!(note.thread_id, None);
108        assert_eq!(note.status, NoteStatus::Open);
109        assert!(note.tags.is_empty());
110    }
111
112    #[test]
113    fn test_note_new_with_optional_fields_none() {
114        let commit = "a1b2c3d4e5f6".to_string();
115        let body = "Note without file or lines.".to_string();
116        let author = "Tester <test@example.com>".to_string();
117        let namespace = Namespace::Comments;
118
119        let note = Note::new(
120            commit.clone(),
121            None,
122            None,
123            None,
124            body.clone(),
125            author.clone(),
126            namespace.clone(),
127        );
128
129        assert!(!note.id.is_nil());
130        assert_eq!(note.file, None);
131        assert_eq!(note.line_start, None);
132        assert_eq!(note.line_end, None);
133        assert_eq!(note.status, NoteStatus::Open);
134    }
135
136    #[test]
137    fn test_note_reply() {
138        let parent = Note::new(
139            "a1b2c3d4e5f6".to_string(),
140            Some("src/lib.rs".to_string()),
141            Some(1),
142            Some(5),
143            "Parent note".to_string(),
144            "Parent Author <parent@example.com>".to_string(),
145            Namespace::Comments,
146        );
147
148        let reply_body = "This is a reply".to_string();
149        let reply_author = "Replier <replier@example.com>".to_string();
150
151        let reply = Note::reply(&parent, reply_body.clone(), reply_author.clone());
152
153        assert_ne!(reply.id, parent.id);
154        assert!(!reply.id.is_nil());
155        assert_eq!(reply.commit, parent.commit);
156        assert_eq!(reply.file, parent.file);
157        assert_eq!(reply.line_start, parent.line_start);
158        assert_eq!(reply.line_end, parent.line_end);
159        assert_eq!(reply.namespace, parent.namespace);
160        assert_eq!(reply.thread_id, Some(parent.id));
161        assert_eq!(reply.body, reply_body);
162        assert_eq!(reply.author, reply_author);
163        assert_eq!(reply.status, NoteStatus::Open);
164        assert!(reply.tags.is_empty());
165    }
166}