use async_trait::async_trait;
use paladin_core::platform::container::document::{Document, DocumentError};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DocumentSource {
File(PathBuf),
Bytes {
data: Vec<u8>,
format: DocumentFormat,
},
Url(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DocumentFormat {
Pdf,
Txt,
Md,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkConfig {
pub chunk_size: usize,
pub chunk_overlap: usize,
pub separator: String,
}
impl Default for ChunkConfig {
fn default() -> Self {
Self {
chunk_size: 1000,
chunk_overlap: 100,
separator: "\n\n".to_string(),
}
}
}
impl ChunkConfig {
pub fn new(chunk_size: usize, chunk_overlap: usize, separator: String) -> Self {
Self {
chunk_size,
chunk_overlap,
separator,
}
}
pub fn validate(&self) -> Result<(), DocumentError> {
if self.chunk_size == 0 {
return Err(DocumentError::InvalidDocument(
"Chunk size must be greater than 0".to_string(),
));
}
if self.chunk_overlap >= self.chunk_size {
return Err(DocumentError::InvalidDocument(
"Chunk overlap must be less than chunk size".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocumentChunk {
pub content: String,
pub metadata: ChunkMetadata,
pub chunk_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ChunkMetadata {
pub page_number: Option<usize>,
pub char_position: usize,
pub total_chunks: Option<usize>,
}
#[async_trait]
pub trait DocumentPort: Send + Sync {
async fn ingest(&self, source: DocumentSource) -> Result<Document, DocumentError>;
async fn chunk(
&self,
document: &Document,
config: ChunkConfig,
) -> Result<Vec<DocumentChunk>, DocumentError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_document_source_file() {
let source = DocumentSource::File(PathBuf::from("/path/to/doc.pdf"));
match source {
DocumentSource::File(path) => {
assert_eq!(path, PathBuf::from("/path/to/doc.pdf"));
}
_ => panic!("Expected File variant"),
}
}
#[test]
fn test_document_source_bytes() {
let data = vec![1, 2, 3, 4, 5];
let source = DocumentSource::Bytes {
data: data.clone(),
format: DocumentFormat::Pdf,
};
match source {
DocumentSource::Bytes { data: d, format } => {
assert_eq!(d, data);
assert_eq!(format, DocumentFormat::Pdf);
}
_ => panic!("Expected Bytes variant"),
}
}
#[test]
fn test_document_source_url() {
let source = DocumentSource::Url("https://example.com/doc.pdf".to_string());
match source {
DocumentSource::Url(url) => {
assert_eq!(url, "https://example.com/doc.pdf");
}
_ => panic!("Expected Url variant"),
}
}
#[test]
fn test_document_format() {
assert_eq!(DocumentFormat::Pdf, DocumentFormat::Pdf);
assert_eq!(DocumentFormat::Txt, DocumentFormat::Txt);
assert_eq!(DocumentFormat::Md, DocumentFormat::Md);
let json = serde_json::to_string(&DocumentFormat::Pdf).unwrap();
assert_eq!(json, "\"pdf\"");
}
#[test]
fn test_chunk_config_default() {
let config = ChunkConfig::default();
assert_eq!(config.chunk_size, 1000);
assert_eq!(config.chunk_overlap, 100);
assert_eq!(config.separator, "\n\n");
}
#[test]
fn test_chunk_config_custom() {
let config = ChunkConfig::new(500, 50, "\n".to_string());
assert_eq!(config.chunk_size, 500);
assert_eq!(config.chunk_overlap, 50);
assert_eq!(config.separator, "\n");
}
#[test]
fn test_chunk_config_validation() {
let valid_config = ChunkConfig::new(1000, 100, "\n\n".to_string());
assert!(valid_config.validate().is_ok());
let invalid_size = ChunkConfig::new(0, 0, "\n\n".to_string());
assert!(invalid_size.validate().is_err());
let invalid_overlap = ChunkConfig::new(100, 100, "\n\n".to_string());
assert!(invalid_overlap.validate().is_err());
let invalid_overlap2 = ChunkConfig::new(100, 150, "\n\n".to_string());
assert!(invalid_overlap2.validate().is_err());
}
#[test]
fn test_document_chunk() {
let metadata = ChunkMetadata {
page_number: Some(1),
char_position: 0,
total_chunks: Some(5),
};
let chunk = DocumentChunk {
content: "This is a chunk of text.".to_string(),
metadata: metadata.clone(),
chunk_index: 0,
};
assert_eq!(chunk.content, "This is a chunk of text.");
assert_eq!(chunk.chunk_index, 0);
assert_eq!(chunk.metadata, metadata);
}
#[test]
fn test_chunk_metadata() {
let metadata = ChunkMetadata {
page_number: Some(3),
char_position: 1500,
total_chunks: Some(10),
};
assert_eq!(metadata.page_number, Some(3));
assert_eq!(metadata.char_position, 1500);
assert_eq!(metadata.total_chunks, Some(10));
}
#[test]
fn test_document_chunk_serialization() {
let chunk = DocumentChunk {
content: "Test content".to_string(),
metadata: ChunkMetadata::default(),
chunk_index: 0,
};
let json = serde_json::to_string(&chunk).unwrap();
let deserialized: DocumentChunk = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, chunk);
}
}