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#[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}