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)]
110#[serde(rename_all = "camelCase")]
111pub struct GoogleDriveContent {
112 pub document_id: String,
113 pub mime_type: String,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub source_name: Option<String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, Default)]
119pub struct VideoContent {
120 #[serde(rename = "youtubeUrl")]
121 pub url: String,
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn user_content_untagged_web() {
130 let json = r#"{"webContent":{"url":"https://example.com"}}"#;
131 let content: UserContent = serde_json::from_str(json).unwrap();
132 match content {
133 UserContent::Web { web_content } => {
134 assert_eq!(web_content.url, "https://example.com");
135 }
136 _ => panic!("expected Web variant"),
137 }
138 }
139
140 #[test]
141 fn user_content_untagged_text() {
142 let json = r#"{"textContent":{"content":"sample text"}}"#;
143 let content: UserContent = serde_json::from_str(json).unwrap();
144 match content {
145 UserContent::Text { text_content } => {
146 assert_eq!(text_content.content, "sample text");
147 }
148 _ => panic!("expected Text variant"),
149 }
150 }
151
152 #[test]
153 fn user_content_untagged_google_drive() {
154 let json = r#"{"googleDriveContent":{"documentId":"123","mimeType":"application/vnd.google-apps.document"}}"#;
155 let content: UserContent = serde_json::from_str(json).unwrap();
156 match content {
157 UserContent::GoogleDrive {
158 google_drive_content,
159 } => {
160 assert_eq!(google_drive_content.document_id, "123");
161 assert_eq!(
162 google_drive_content.mime_type,
163 "application/vnd.google-apps.document"
164 );
165 }
166 _ => panic!("expected GoogleDrive variant"),
167 }
168 }
169
170 #[test]
171 fn user_content_untagged_video() {
172 let json = r#"{"videoContent":{"youtubeUrl":"https://youtube.com/watch?v=123"}}"#;
173 let content: UserContent = serde_json::from_str(json).unwrap();
174 match content {
175 UserContent::Video { video_content } => {
176 assert_eq!(video_content.url, "https://youtube.com/watch?v=123");
177 }
178 _ => panic!("expected Video variant"),
179 }
180 }
181
182 #[test]
183 fn user_content_video_serializes_correctly() {
184 let content = UserContent::Video {
185 video_content: VideoContent {
186 url: "https://youtube.com/watch?v=123".to_string(),
187 },
188 };
189 let json = serde_json::to_string(&content).unwrap();
190 assert!(
191 json.contains("videoContent"),
192 "JSON should contain videoContent, got: {}",
193 json
194 );
195 assert!(
196 json.contains(r#""youtubeUrl":"https://youtube.com/watch?v=123""#),
197 "JSON should contain youtubeUrl field, got: {}",
198 json
199 );
200 }
201}