nblm_core/models/
source.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6#[serde(rename_all = "camelCase")]
7pub struct NotebookSource {
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub metadata: Option<NotebookSourceMetadata>,
10    pub name: String,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub settings: Option<NotebookSourceSettings>,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub source_id: Option<NotebookSourceId>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17    #[serde(flatten)]
18    pub extra: HashMap<String, serde_json::Value>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22#[serde(rename_all = "camelCase")]
23pub struct NotebookSourceMetadata {
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub source_added_timestamp: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub word_count: Option<u64>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub youtube_metadata: Option<NotebookSourceYoutubeMetadata>,
30    #[serde(flatten)]
31    pub extra: HashMap<String, serde_json::Value>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35#[serde(rename_all = "camelCase")]
36pub struct NotebookSourceYoutubeMetadata {
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub channel_name: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub video_id: Option<String>,
41    #[serde(flatten)]
42    pub extra: HashMap<String, serde_json::Value>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46#[serde(rename_all = "camelCase")]
47pub struct NotebookSourceSettings {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub status: Option<String>,
50    #[serde(flatten)]
51    pub extra: HashMap<String, serde_json::Value>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55#[serde(rename_all = "camelCase")]
56pub struct NotebookSourceId {
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub id: Option<String>,
59    #[serde(flatten)]
60    pub extra: HashMap<String, serde_json::Value>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum UserContent {
66    Web {
67        #[serde(rename = "webContent")]
68        web_content: WebContent,
69    },
70    Text {
71        #[serde(rename = "textContent")]
72        text_content: TextContent,
73    },
74    GoogleDrive {
75        #[serde(rename = "googleDriveContent")]
76        google_drive_content: GoogleDriveContent,
77    },
78    Video {
79        #[serde(rename = "videoContent")]
80        video_content: VideoContent,
81    },
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, Default)]
85#[serde(rename_all = "camelCase")]
86pub struct WebContent {
87    pub url: String,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub source_name: Option<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93#[serde(rename_all = "camelCase")]
94pub struct TextContent {
95    pub content: String,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub source_name: Option<String>,
98}
99
100/// Google Drive content for adding sources.
101///
102/// # Requirements
103///
104/// - Use credentials initialized with `gcloud auth login --enable-gdrive-access`
105/// - Ensure the authenticated account has at least viewer access to the document
106/// - Provide the MIME type returned by the Drive API (e.g. `application/pdf`)
107#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108#[serde(rename_all = "camelCase")]
109pub struct GoogleDriveContent {
110    pub document_id: String,
111    pub mime_type: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub source_name: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117pub struct VideoContent {
118    #[serde(rename = "youtubeUrl")]
119    pub url: String,
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn user_content_untagged_web() {
128        let json = r#"{"webContent":{"url":"https://example.com"}}"#;
129        let content: UserContent = serde_json::from_str(json).unwrap();
130        match content {
131            UserContent::Web { web_content } => {
132                assert_eq!(web_content.url, "https://example.com");
133            }
134            _ => panic!("expected Web variant"),
135        }
136    }
137
138    #[test]
139    fn user_content_untagged_text() {
140        let json = r#"{"textContent":{"content":"sample text"}}"#;
141        let content: UserContent = serde_json::from_str(json).unwrap();
142        match content {
143            UserContent::Text { text_content } => {
144                assert_eq!(text_content.content, "sample text");
145            }
146            _ => panic!("expected Text variant"),
147        }
148    }
149
150    #[test]
151    fn user_content_untagged_google_drive() {
152        let json = r#"{"googleDriveContent":{"documentId":"123","mimeType":"application/vnd.google-apps.document"}}"#;
153        let content: UserContent = serde_json::from_str(json).unwrap();
154        match content {
155            UserContent::GoogleDrive {
156                google_drive_content,
157            } => {
158                assert_eq!(google_drive_content.document_id, "123");
159                assert_eq!(
160                    google_drive_content.mime_type,
161                    "application/vnd.google-apps.document"
162                );
163            }
164            _ => panic!("expected GoogleDrive variant"),
165        }
166    }
167
168    #[test]
169    fn user_content_untagged_video() {
170        let json = r#"{"videoContent":{"youtubeUrl":"https://youtube.com/watch?v=123"}}"#;
171        let content: UserContent = serde_json::from_str(json).unwrap();
172        match content {
173            UserContent::Video { video_content } => {
174                assert_eq!(video_content.url, "https://youtube.com/watch?v=123");
175            }
176            _ => panic!("expected Video variant"),
177        }
178    }
179
180    #[test]
181    fn user_content_video_serializes_correctly() {
182        let content = UserContent::Video {
183            video_content: VideoContent {
184                url: "https://youtube.com/watch?v=123".to_string(),
185            },
186        };
187        let json = serde_json::to_string(&content).unwrap();
188        assert!(
189            json.contains("videoContent"),
190            "JSON should contain videoContent, got: {}",
191            json
192        );
193        assert!(
194            json.contains(r#""youtubeUrl":"https://youtube.com/watch?v=123""#),
195            "JSON should contain youtubeUrl field, got: {}",
196            json
197        );
198    }
199}