use std::path::PathBuf;
use tracing::info;
use crate::{
domain::{entities::ExportFormat, Exporter, ExporterConfig, ExporterError},
infrastructure::export::{
jsonl_exporter, state_store::StateStore, zvec_exporter::ZvecExporter,
},
};
pub fn create_exporter(
output_dir: PathBuf,
filename: &str,
format: ExportFormat,
) -> Result<Box<dyn Exporter>, ExporterError> {
let config = ExporterConfig::new(output_dir.clone(), format, filename);
match format {
ExportFormat::Jsonl => {
info!("Creating JSONL exporter: {:?}", config.output_path());
let exporter = jsonl_exporter::JsonlExporter::new(config);
Ok(Box::new(exporter))
}
ExportFormat::Zvec => {
if ZvecExporter::is_available() {
info!("Creating Zvec exporter: {:?}", config.output_path());
let exporter = ZvecExporter::new(config);
Ok(Box::new(exporter))
} else {
Err(ExporterError::InvalidConfig(
"Zvec format requires zvec feature enabled".to_string(),
))
}
}
ExportFormat::Auto => {
info!("Auto-detecting format...");
let jsonl_path = output_dir.join(format!("{}.jsonl", filename));
if jsonl_path.exists() {
info!("Detected JSONL format - {:?} exists", jsonl_path);
let config = ExporterConfig::new(output_dir, ExportFormat::Jsonl, filename);
let exporter = jsonl_exporter::JsonlExporter::new(config);
Ok(Box::new(exporter))
} else {
info!("No existing export, using default Jsonl format");
let config = ExporterConfig::new(output_dir, ExportFormat::Jsonl, filename);
let exporter = jsonl_exporter::JsonlExporter::new(config);
Ok(Box::new(exporter))
}
}
_ => Err(ExporterError::InvalidConfig(format!(
"Format {:?} not yet implemented in Exporter trait",
format
))),
}
}
pub fn create_state_store(
state_dir: PathBuf,
domain: &str,
) -> Result<StateStore, crate::error::ScraperError> {
use crate::infrastructure::export::state_store::StateStore;
info!("Creating StateStore in {:?}", state_dir);
let mut store = StateStore::new(domain);
store.set_cache_dir(state_dir);
Ok(store)
}
pub fn process_results(
results: &[crate::domain::ScrapedContent],
output_dir: PathBuf,
format: ExportFormat,
filename: &str,
state_store: Option<&StateStore>,
resume_mode: bool,
) -> Result<Vec<String>, ExporterError> {
use crate::domain::entities::DocumentChunk;
info!("Processing {} results for export", results.len());
let exporter = create_exporter(output_dir, filename, format)?;
let mut processed_urls = Vec::new();
let mut export_state = if resume_mode {
if let Some(store) = state_store {
Some(store.load_or_default()?)
} else {
None
}
} else {
None
};
for result in results {
let chunk = DocumentChunk::from_scraped_content(result);
exporter.export(chunk)?;
let url_str = result.url.as_str().to_string();
processed_urls.push(url_str.clone());
if resume_mode {
if let Some(store) = state_store {
if let Some(ref mut state) = export_state {
store.mark_processed(state, &url_str);
}
}
}
}
if resume_mode {
if let Some(store) = state_store {
if let Some(state) = export_state {
store.save(&state)?;
}
}
}
info!(
"✅ Export completed: {} documents processed",
processed_urls.len()
);
Ok(processed_urls)
}
pub fn domain_from_url(url: &str) -> String {
url::Url::parse(url)
.ok()
.and_then(|p| p.host_str().map(str::to_string))
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_domain_from_url_extracts_correctly() {
let url = "https://www.example.com/docs/api/";
let domain = domain_from_url(url);
assert_eq!(domain, "www.example.com");
}
#[test]
fn test_create_state_store_creates_directory() {
let temp_dir = TempDir::new().unwrap();
let domain = "example.com";
let store = create_state_store(temp_dir.path().to_path_buf(), domain);
assert!(store.is_ok());
let state_file = temp_dir.path().join("example.com.json");
let store = store.unwrap();
assert_eq!(store.get_state_path(), state_file);
}
}