Skip to main content

routa_core/models/
note.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use super::task::TaskStatus;
6
7pub const SPEC_NOTE_ID: &str = "spec";
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum NoteType {
12    Spec,
13    Task,
14    General,
15}
16
17impl NoteType {
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            Self::Spec => "spec",
21            Self::Task => "task",
22            Self::General => "general",
23        }
24    }
25
26    #[allow(clippy::should_implement_trait)]
27    pub fn from_str(s: &str) -> Self {
28        match s {
29            "spec" => Self::Spec,
30            "task" => Self::Task,
31            _ => Self::General,
32        }
33    }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct NoteMetadata {
39    #[serde(rename = "type")]
40    pub note_type: NoteType,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub task_status: Option<TaskStatus>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub assigned_agent_ids: Option<Vec<String>>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub parent_note_id: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub linked_task_id: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub custom: Option<HashMap<String, String>>,
51}
52
53impl Default for NoteMetadata {
54    fn default() -> Self {
55        Self {
56            note_type: NoteType::General,
57            task_status: None,
58            assigned_agent_ids: None,
59            parent_note_id: None,
60            linked_task_id: None,
61            custom: None,
62        }
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Note {
69    pub id: String,
70    pub title: String,
71    pub content: String,
72    pub workspace_id: String,
73    /// Session ID that created this note (for session-scoped grouping)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub session_id: Option<String>,
76    pub metadata: NoteMetadata,
77    pub created_at: DateTime<Utc>,
78    pub updated_at: DateTime<Utc>,
79}
80
81impl Note {
82    pub fn new(
83        id: String,
84        title: String,
85        content: String,
86        workspace_id: String,
87        metadata: Option<NoteMetadata>,
88    ) -> Self {
89        let now = Utc::now();
90        Self {
91            id,
92            title,
93            content,
94            workspace_id,
95            session_id: None,
96            metadata: metadata.unwrap_or_default(),
97            created_at: now,
98            updated_at: now,
99        }
100    }
101
102    /// Create a new note with session ID
103    pub fn new_with_session(
104        id: String,
105        title: String,
106        content: String,
107        workspace_id: String,
108        session_id: Option<String>,
109        metadata: Option<NoteMetadata>,
110    ) -> Self {
111        let now = Utc::now();
112        Self {
113            id,
114            title,
115            content,
116            workspace_id,
117            session_id,
118            metadata: metadata.unwrap_or_default(),
119            created_at: now,
120            updated_at: now,
121        }
122    }
123
124    pub fn new_spec(workspace_id: String) -> Self {
125        Self::new(
126            SPEC_NOTE_ID.to_string(),
127            "Spec".to_string(),
128            String::new(),
129            workspace_id,
130            Some(NoteMetadata {
131                note_type: NoteType::Spec,
132                ..Default::default()
133            }),
134        )
135    }
136}