nblm_core/models/enterprise/
source.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7#[serde(rename_all = "camelCase")]
8pub struct NotebookSource {
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub metadata: Option<NotebookSourceMetadata>,
11    pub name: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub settings: Option<NotebookSourceSettings>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub source_id: Option<NotebookSourceId>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub title: Option<String>,
18    #[serde(flatten)]
19    pub extra: HashMap<String, Value>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, Default)]
23#[serde(rename_all = "camelCase")]
24pub struct NotebookSourceMetadata {
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub source_added_timestamp: Option<String>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub word_count: Option<u64>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub youtube_metadata: Option<NotebookSourceYoutubeMetadata>,
31    #[serde(flatten)]
32    pub extra: HashMap<String, Value>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36#[serde(rename_all = "camelCase")]
37pub struct NotebookSourceYoutubeMetadata {
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub channel_name: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub video_id: Option<String>,
42    #[serde(flatten)]
43    pub extra: HashMap<String, Value>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, Default)]
47#[serde(rename_all = "camelCase")]
48pub struct NotebookSourceSettings {
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub status: Option<String>,
51    #[serde(flatten)]
52    pub extra: HashMap<String, Value>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56#[serde(rename_all = "camelCase")]
57pub struct NotebookSourceId {
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub id: Option<String>,
60    #[serde(flatten)]
61    pub extra: HashMap<String, Value>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(untagged)]
66pub enum UserContent {
67    Web {
68        #[serde(rename = "webContent")]
69        web_content: WebContent,
70    },
71    Text {
72        #[serde(rename = "textContent")]
73        text_content: TextContent,
74    },
75    GoogleDrive {
76        #[serde(rename = "googleDriveContent")]
77        google_drive_content: GoogleDriveContent,
78    },
79    Video {
80        #[serde(rename = "videoContent")]
81        video_content: VideoContent,
82    },
83}
84
85impl UserContent {
86    pub fn web(url: String, source_name: Option<String>) -> Self {
87        Self::Web {
88            web_content: WebContent { url, source_name },
89        }
90    }
91
92    pub fn text(content: String, source_name: Option<String>) -> Self {
93        Self::Text {
94            text_content: TextContent {
95                content,
96                source_name,
97            },
98        }
99    }
100
101    pub fn google_drive(
102        document_id: String,
103        mime_type: String,
104        source_name: Option<String>,
105    ) -> Self {
106        Self::GoogleDrive {
107            google_drive_content: GoogleDriveContent {
108                document_id,
109                mime_type,
110                source_name,
111            },
112        }
113    }
114
115    pub fn video(url: String) -> Self {
116        Self::Video {
117            video_content: VideoContent { url },
118        }
119    }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, Default)]
123#[serde(rename_all = "camelCase")]
124pub struct WebContent {
125    pub url: String,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub source_name: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, Default)]
131#[serde(rename_all = "camelCase")]
132pub struct TextContent {
133    pub content: String,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub source_name: Option<String>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139#[serde(rename_all = "camelCase")]
140pub struct GoogleDriveContent {
141    pub document_id: String,
142    pub mime_type: String,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub source_name: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148pub struct VideoContent {
149    #[serde(rename = "youtubeUrl")]
150    pub url: String,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
154#[serde(rename_all = "camelCase")]
155pub struct BatchCreateSourcesRequest {
156    #[serde(rename = "userContents")]
157    pub user_contents: Vec<UserContent>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, Default)]
161#[serde(rename_all = "camelCase")]
162pub struct BatchCreateSourcesResponse {
163    #[serde(default)]
164    pub sources: Vec<NotebookSource>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub error_count: Option<i32>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170#[serde(rename_all = "camelCase")]
171pub struct BatchDeleteSourcesRequest {
172    pub names: Vec<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, Default)]
176#[serde(rename_all = "camelCase")]
177pub struct BatchDeleteSourcesResponse {
178    #[serde(flatten)]
179    pub extra: HashMap<String, Value>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, Default)]
183#[serde(rename_all = "camelCase")]
184pub struct UploadSourceFileResponse {
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub source_id: Option<NotebookSourceId>,
187    #[serde(flatten)]
188    pub extra: HashMap<String, Value>,
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    // Test 1: UserContent constructor methods
196    #[test]
197    fn test_user_content_web_constructor() {
198        let url = "https://example.com".to_string();
199        let source_name = Some("Test Web Source".to_string());
200
201        let content = UserContent::web(url.clone(), source_name.clone());
202
203        match content {
204            UserContent::Web { web_content } => {
205                assert_eq!(web_content.url, url);
206                assert_eq!(web_content.source_name, source_name);
207            }
208            _ => panic!("Expected UserContent::Web variant"),
209        }
210    }
211
212    #[test]
213    fn test_user_content_web_constructor_without_name() {
214        let url = "https://example.com".to_string();
215
216        let content = UserContent::web(url.clone(), None);
217
218        match content {
219            UserContent::Web { web_content } => {
220                assert_eq!(web_content.url, url);
221                assert_eq!(web_content.source_name, None);
222            }
223            _ => panic!("Expected UserContent::Web variant"),
224        }
225    }
226
227    #[test]
228    fn test_user_content_text_constructor() {
229        let text = "Sample text content".to_string();
230        let source_name = Some("Test Text".to_string());
231
232        let content = UserContent::text(text.clone(), source_name.clone());
233
234        match content {
235            UserContent::Text { text_content } => {
236                assert_eq!(text_content.content, text);
237                assert_eq!(text_content.source_name, source_name);
238            }
239            _ => panic!("Expected UserContent::Text variant"),
240        }
241    }
242
243    #[test]
244    fn test_user_content_google_drive_constructor() {
245        let document_id = "doc-12345".to_string();
246        let mime_type = "application/pdf".to_string();
247        let source_name = Some("Drive Document".to_string());
248
249        let content =
250            UserContent::google_drive(document_id.clone(), mime_type.clone(), source_name.clone());
251
252        match content {
253            UserContent::GoogleDrive {
254                google_drive_content,
255            } => {
256                assert_eq!(google_drive_content.document_id, document_id);
257                assert_eq!(google_drive_content.mime_type, mime_type);
258                assert_eq!(google_drive_content.source_name, source_name);
259            }
260            _ => panic!("Expected UserContent::GoogleDrive variant"),
261        }
262    }
263
264    #[test]
265    fn test_user_content_video_constructor() {
266        let url = "https://youtube.com/watch?v=abc123".to_string();
267
268        let content = UserContent::video(url.clone());
269
270        match content {
271            UserContent::Video { video_content } => {
272                assert_eq!(video_content.url, url);
273            }
274            _ => panic!("Expected UserContent::Video variant"),
275        }
276    }
277
278    // Test 2: Serialization/Deserialization correctness
279    #[test]
280    fn test_user_content_web_serialization() {
281        let content = UserContent::web(
282            "https://example.com".to_string(),
283            Some("Web Source".to_string()),
284        );
285
286        let json = serde_json::to_value(&content).unwrap();
287
288        assert_eq!(json["webContent"]["url"], "https://example.com");
289        assert_eq!(json["webContent"]["sourceName"], "Web Source");
290    }
291
292    #[test]
293    fn test_user_content_web_deserialization() {
294        let json = serde_json::json!({
295            "webContent": {
296                "url": "https://example.com",
297                "sourceName": "Web Source"
298            }
299        });
300
301        let content: UserContent = serde_json::from_value(json).unwrap();
302
303        match content {
304            UserContent::Web { web_content } => {
305                assert_eq!(web_content.url, "https://example.com");
306                assert_eq!(web_content.source_name, Some("Web Source".to_string()));
307            }
308            _ => panic!("Expected UserContent::Web variant"),
309        }
310    }
311
312    #[test]
313    fn test_user_content_text_serialization() {
314        let content = UserContent::text("Sample text".to_string(), Some("Text Source".to_string()));
315
316        let json = serde_json::to_value(&content).unwrap();
317
318        assert_eq!(json["textContent"]["content"], "Sample text");
319        assert_eq!(json["textContent"]["sourceName"], "Text Source");
320    }
321
322    #[test]
323    fn test_user_content_text_deserialization() {
324        let json = serde_json::json!({
325            "textContent": {
326                "content": "Sample text",
327                "sourceName": "Text Source"
328            }
329        });
330
331        let content: UserContent = serde_json::from_value(json).unwrap();
332
333        match content {
334            UserContent::Text { text_content } => {
335                assert_eq!(text_content.content, "Sample text");
336                assert_eq!(text_content.source_name, Some("Text Source".to_string()));
337            }
338            _ => panic!("Expected UserContent::Text variant"),
339        }
340    }
341
342    #[test]
343    fn test_user_content_google_drive_serialization() {
344        let content = UserContent::google_drive(
345            "doc-123".to_string(),
346            "application/pdf".to_string(),
347            Some("Drive Doc".to_string()),
348        );
349
350        let json = serde_json::to_value(&content).unwrap();
351
352        assert_eq!(json["googleDriveContent"]["documentId"], "doc-123");
353        assert_eq!(json["googleDriveContent"]["mimeType"], "application/pdf");
354        assert_eq!(json["googleDriveContent"]["sourceName"], "Drive Doc");
355    }
356
357    #[test]
358    fn test_user_content_google_drive_deserialization() {
359        let json = serde_json::json!({
360            "googleDriveContent": {
361                "documentId": "doc-123",
362                "mimeType": "application/pdf",
363                "sourceName": "Drive Doc"
364            }
365        });
366
367        let content: UserContent = serde_json::from_value(json).unwrap();
368
369        match content {
370            UserContent::GoogleDrive {
371                google_drive_content,
372            } => {
373                assert_eq!(google_drive_content.document_id, "doc-123");
374                assert_eq!(google_drive_content.mime_type, "application/pdf");
375                assert_eq!(
376                    google_drive_content.source_name,
377                    Some("Drive Doc".to_string())
378                );
379            }
380            _ => panic!("Expected UserContent::GoogleDrive variant"),
381        }
382    }
383
384    #[test]
385    fn test_user_content_video_serialization() {
386        let content = UserContent::video("https://youtube.com/watch?v=abc".to_string());
387
388        let json = serde_json::to_value(&content).unwrap();
389
390        assert_eq!(
391            json["videoContent"]["youtubeUrl"],
392            "https://youtube.com/watch?v=abc"
393        );
394    }
395
396    #[test]
397    fn test_user_content_video_deserialization() {
398        let json = serde_json::json!({
399            "videoContent": {
400                "youtubeUrl": "https://youtube.com/watch?v=abc"
401            }
402        });
403
404        let content: UserContent = serde_json::from_value(json).unwrap();
405
406        match content {
407            UserContent::Video { video_content } => {
408                assert_eq!(video_content.url, "https://youtube.com/watch?v=abc");
409            }
410            _ => panic!("Expected UserContent::Video variant"),
411        }
412    }
413
414    // Test 3: skip_serializing_if behavior (None fields are omitted)
415    #[test]
416    fn test_web_content_omits_none_source_name() {
417        let content = WebContent {
418            url: "https://example.com".to_string(),
419            source_name: None,
420        };
421
422        let json = serde_json::to_value(&content).unwrap();
423        let obj = json.as_object().unwrap();
424
425        // sourceName should not be present in JSON
426        assert!(!obj.contains_key("sourceName"));
427        assert_eq!(obj.get("url").unwrap(), "https://example.com");
428    }
429
430    #[test]
431    fn test_web_content_includes_some_source_name() {
432        let content = WebContent {
433            url: "https://example.com".to_string(),
434            source_name: Some("My Source".to_string()),
435        };
436
437        let json = serde_json::to_value(&content).unwrap();
438        let obj = json.as_object().unwrap();
439
440        // sourceName should be present
441        assert!(obj.contains_key("sourceName"));
442        assert_eq!(obj.get("sourceName").unwrap(), "My Source");
443    }
444
445    #[test]
446    fn test_text_content_omits_none_source_name() {
447        let content = TextContent {
448            content: "text".to_string(),
449            source_name: None,
450        };
451
452        let json = serde_json::to_value(&content).unwrap();
453        let obj = json.as_object().unwrap();
454
455        assert!(!obj.contains_key("sourceName"));
456        assert_eq!(obj.get("content").unwrap(), "text");
457    }
458
459    #[test]
460    fn test_google_drive_content_omits_none_source_name() {
461        let content = GoogleDriveContent {
462            document_id: "doc-123".to_string(),
463            mime_type: "application/pdf".to_string(),
464            source_name: None,
465        };
466
467        let json = serde_json::to_value(&content).unwrap();
468        let obj = json.as_object().unwrap();
469
470        assert!(!obj.contains_key("sourceName"));
471        assert_eq!(obj.get("documentId").unwrap(), "doc-123");
472        assert_eq!(obj.get("mimeType").unwrap(), "application/pdf");
473    }
474
475    #[test]
476    fn test_notebook_source_omits_none_fields() {
477        let source = NotebookSource {
478            name: "projects/123/notebooks/456/sources/789".to_string(),
479            metadata: None,
480            settings: None,
481            source_id: None,
482            title: None,
483            extra: HashMap::new(),
484        };
485
486        let json = serde_json::to_value(&source).unwrap();
487        let obj = json.as_object().unwrap();
488
489        // Only 'name' should be present
490        assert!(obj.contains_key("name"));
491        assert!(!obj.contains_key("metadata"));
492        assert!(!obj.contains_key("settings"));
493        assert!(!obj.contains_key("sourceId"));
494        assert!(!obj.contains_key("title"));
495    }
496
497    #[test]
498    fn test_notebook_source_includes_some_fields() {
499        let source = NotebookSource {
500            name: "projects/123/notebooks/456/sources/789".to_string(),
501            metadata: Some(NotebookSourceMetadata {
502                word_count: Some(1000),
503                ..Default::default()
504            }),
505            settings: Some(NotebookSourceSettings {
506                status: Some("ACTIVE".to_string()),
507                ..Default::default()
508            }),
509            source_id: Some(NotebookSourceId {
510                id: Some("source-123".to_string()),
511                ..Default::default()
512            }),
513            title: Some("My Document".to_string()),
514            extra: HashMap::new(),
515        };
516
517        let json = serde_json::to_value(&source).unwrap();
518        let obj = json.as_object().unwrap();
519
520        assert!(obj.contains_key("name"));
521        assert!(obj.contains_key("metadata"));
522        assert!(obj.contains_key("settings"));
523        assert!(obj.contains_key("sourceId"));
524        assert!(obj.contains_key("title"));
525    }
526
527    #[test]
528    fn test_batch_create_sources_request_serialization() {
529        let request = BatchCreateSourcesRequest {
530            user_contents: vec![
531                UserContent::web("https://example.com".to_string(), None),
532                UserContent::text("Sample text".to_string(), Some("Text".to_string())),
533            ],
534        };
535
536        let json = serde_json::to_value(&request).unwrap();
537
538        assert!(json["userContents"].is_array());
539        assert_eq!(json["userContents"].as_array().unwrap().len(), 2);
540    }
541
542    #[test]
543    fn test_batch_create_sources_response_deserialization() {
544        let json = serde_json::json!({
545            "sources": [
546                {
547                    "name": "projects/123/notebooks/456/sources/789"
548                }
549            ],
550            "errorCount": 0
551        });
552
553        let response: BatchCreateSourcesResponse = serde_json::from_value(json).unwrap();
554
555        assert_eq!(response.sources.len(), 1);
556        assert_eq!(response.error_count, Some(0));
557    }
558
559    #[test]
560    fn test_batch_create_sources_response_empty_sources() {
561        let json = serde_json::json!({
562            "sources": []
563        });
564
565        let response: BatchCreateSourcesResponse = serde_json::from_value(json).unwrap();
566
567        assert_eq!(response.sources.len(), 0);
568        assert_eq!(response.error_count, None);
569    }
570
571    #[test]
572    fn test_notebook_source_metadata_youtube() {
573        let metadata = NotebookSourceMetadata {
574            source_added_timestamp: Some("2024-01-01T00:00:00Z".to_string()),
575            word_count: Some(5000),
576            youtube_metadata: Some(NotebookSourceYoutubeMetadata {
577                channel_name: Some("Test Channel".to_string()),
578                video_id: Some("abc123".to_string()),
579                extra: HashMap::new(),
580            }),
581            extra: HashMap::new(),
582        };
583
584        let json = serde_json::to_value(&metadata).unwrap();
585
586        assert_eq!(json["sourceAddedTimestamp"], "2024-01-01T00:00:00Z");
587        assert_eq!(json["wordCount"], 5000);
588        assert_eq!(json["youtubeMetadata"]["channelName"], "Test Channel");
589        assert_eq!(json["youtubeMetadata"]["videoId"], "abc123");
590    }
591
592    #[test]
593    fn test_upload_source_file_response_omits_none_source_id() {
594        let response = UploadSourceFileResponse {
595            source_id: None,
596            extra: HashMap::new(),
597        };
598
599        let json = serde_json::to_value(&response).unwrap();
600        let obj = json.as_object().unwrap();
601
602        assert!(!obj.contains_key("sourceId"));
603    }
604
605    #[test]
606    fn test_camel_case_serialization() {
607        // Verify that snake_case fields are converted to camelCase
608        let metadata = NotebookSourceMetadata {
609            source_added_timestamp: Some("2024-01-01T00:00:00Z".to_string()),
610            word_count: Some(100),
611            youtube_metadata: None,
612            extra: HashMap::new(),
613        };
614
615        let json = serde_json::to_string(&metadata).unwrap();
616
617        // Should contain camelCase keys
618        assert!(json.contains("sourceAddedTimestamp"));
619        assert!(json.contains("wordCount"));
620        // Should NOT contain snake_case keys
621        assert!(!json.contains("source_added_timestamp"));
622        assert!(!json.contains("word_count"));
623    }
624}