use crate::core::platform::container::content::{
AudioContent, ContentItem, ContentType, ImageContent, TextContent, VideoContent,
};
use paladin_ports::input::content_input_port::ContentIngestionPort;
use std::fs;
use std::io::Read;
use std::path::Path;
#[doc(hidden)]
pub struct FileContentFetcher;
impl FileContentFetcher {
fn determine_content_type(&self, path: &Path) -> Result<ContentType, String> {
let extension = path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase())
.ok_or("No file extension found")?;
match extension.as_str() {
"txt" | "md" | "rst" | "json" | "xml" | "yaml" | "yml" | "toml" | "csv" | "log" => {
self.create_text_content(path)
}
"mp4" | "avi" | "mkv" | "mov" | "wmv" | "flv" | "webm" | "m4v" => {
self.create_video_content(path)
}
"mp3" | "wav" | "flac" | "aac" | "ogg" | "m4a" | "wma" => {
self.create_audio_content(path)
}
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "tif" | "webp" | "svg" => {
self.create_image_content(path)
}
_ => Err(format!("Unsupported file type: {}", extension)),
}
}
fn create_text_content(&self, path: &Path) -> Result<ContentType, String> {
let path_str = path.to_string_lossy().to_string();
let mut file = fs::File::open(path).map_err(|e| e.to_string())?;
let mut content = String::new();
file.read_to_string(&mut content)
.map_err(|e| e.to_string())?;
let text_content =
TextContent::new(Some(path_str), Some(content)).map_err(|e| e.to_string())?;
Ok(ContentType::Text(text_content))
}
fn create_video_content(&self, path: &Path) -> Result<ContentType, String> {
let path_str = path.to_string_lossy().to_string();
let duration = self.get_video_duration(path).unwrap_or(0);
let video_content =
VideoContent::new(Some(path_str), duration).map_err(|e| e.to_string())?;
Ok(ContentType::Video(video_content))
}
fn create_audio_content(&self, path: &Path) -> Result<ContentType, String> {
let path_str = path.to_string_lossy().to_string();
let duration = self.get_audio_duration(path).unwrap_or(0);
let audio_content =
AudioContent::new(Some(path_str), duration).map_err(|e| e.to_string())?;
Ok(ContentType::Audio(audio_content))
}
fn create_image_content(&self, path: &Path) -> Result<ContentType, String> {
let path_str = path.to_string_lossy().to_string();
let resolution = self.get_image_dimensions(path).unwrap_or((0, 0));
let image_content =
ImageContent::new(Some(path_str), resolution).map_err(|e| e.to_string())?;
Ok(ContentType::Image(image_content))
}
fn get_video_duration(&self, _path: &Path) -> Option<u64> {
None
}
fn get_audio_duration(&self, _path: &Path) -> Option<u64> {
None
}
fn get_image_dimensions(&self, _path: &Path) -> Option<(u32, u32)> {
None
}
}
impl ContentIngestionPort for FileContentFetcher {
fn fetch_content(&self, content: ContentItem) -> Result<ContentItem, String> {
let url = content.url().cloned().ok_or("URL is None")?;
let path = url
.to_file_path()
.map_err(|_| "Failed to convert URL to file path")?;
let content_type = self.determine_content_type(&path)?;
let title = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string());
let mut content_item = ContentItem::new(content_type)
.map_err(|e| format!("Failed to create content item: {:?}", e))?;
content_item.set_url(Some(url));
if let Some(title) = title {
content_item.set_title(Some(title));
}
content_item.set_tags(Some(Vec::new()));
Ok(content_item)
}
fn ingest_content(&self, content: ContentItem) -> Result<(), String> {
match content.content() {
ContentType::Text(_) => {
if content.url().is_none() {
return Err("Text content requires a URL".to_string());
}
}
ContentType::Video(_) => {
if content.url().is_none() {
return Err("Video content requires a URL".to_string());
}
}
ContentType::Audio(_) => {
if content.url().is_none() {
return Err("Audio content requires a URL".to_string());
}
}
ContentType::Image(_) => {
if content.url().is_none() {
return Err("Image content requires a URL".to_string());
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
use url::Url;
#[test]
fn test_fetch_text_content() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let file_path = dir.path().join("test.txt");
let mut file = File::create(&file_path)?;
writeln!(file, "Hello, world!")?;
let fetcher = FileContentFetcher;
let file_url = Url::from_file_path(&file_path).unwrap();
let text_content = TextContent::new(None, Some(String::new()))?;
let mut input_content = ContentItem::new(ContentType::Text(text_content))?;
input_content.set_url(Some(file_url));
let content = fetcher.fetch_content(input_content)?;
match content.content() {
ContentType::Text(text_content) => {
assert!(text_content.content.is_some());
assert_eq!(text_content.content.as_ref().unwrap(), "Hello, world!\n");
}
_ => panic!("Expected text content"),
}
assert_eq!(content.title(), Some(&"test.txt".to_string()));
assert!(content.hash().is_some());
Ok(())
}
#[test]
fn test_fetch_video_content() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let file_path = dir.path().join("test.mp4");
let mut file = File::create(&file_path)?;
writeln!(file, "fake video data")?;
let fetcher = FileContentFetcher;
let file_url = Url::from_file_path(&file_path).unwrap();
let text_content = TextContent::new(None, Some(String::new()))?;
let mut input_content = ContentItem::new(ContentType::Text(text_content))?;
input_content.set_url(Some(file_url));
let content = fetcher.fetch_content(input_content)?;
match content.content() {
ContentType::Video(_) => {
}
_ => panic!("Expected video content"),
}
assert_eq!(content.title(), Some(&"test.mp4".to_string()));
Ok(())
}
#[test]
fn test_determine_content_type() -> Result<(), Box<dyn std::error::Error>> {
let fetcher = FileContentFetcher;
let dir = tempdir()?;
let txt_path = dir.path().join("test.txt");
File::create(&txt_path)?;
let content_type = fetcher.determine_content_type(&txt_path)?;
assert!(matches!(content_type, ContentType::Text(_)));
let mp4_path = dir.path().join("test.mp4");
File::create(&mp4_path)?;
let content_type = fetcher.determine_content_type(&mp4_path)?;
assert!(matches!(content_type, ContentType::Video(_)));
let mp3_path = dir.path().join("test.mp3");
File::create(&mp3_path)?;
let content_type = fetcher.determine_content_type(&mp3_path)?;
assert!(matches!(content_type, ContentType::Audio(_)));
let jpg_path = dir.path().join("test.jpg");
File::create(&jpg_path)?;
let content_type = fetcher.determine_content_type(&jpg_path)?;
assert!(matches!(content_type, ContentType::Image(_)));
Ok(())
}
#[test]
fn test_unsupported_file_type() -> Result<(), Box<dyn std::error::Error>> {
let fetcher = FileContentFetcher;
let dir = tempdir()?;
let unsupported_path = dir.path().join("test.xyz");
File::create(&unsupported_path)?;
let result = fetcher.determine_content_type(&unsupported_path);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unsupported file type"));
Ok(())
}
#[test]
fn test_ingest_content_validation() -> Result<(), Box<dyn std::error::Error>> {
let fetcher = FileContentFetcher;
let text_content = TextContent::new(None, Some("test".to_string()))?;
let mut valid_content = ContentItem::new(ContentType::Text(text_content))?;
valid_content.set_url(Some(Url::parse("file:///test.txt")?));
let result = fetcher.ingest_content(valid_content);
assert!(result.is_ok());
let text_content = TextContent::new(None, Some("test".to_string()))?;
let invalid_content = ContentItem::new(ContentType::Text(text_content))?;
let result = fetcher.ingest_content(invalid_content);
assert!(result.is_err());
Ok(())
}
}