use crate::types::ExtractedContent;
use eyre::Result;
use serde_json::Value;
use tracing::{debug, instrument};
pub trait Extractor: Send + Sync {
fn can_extract(&self, url: &str, schema_org_data: &[Value]) -> bool;
fn extract_from_html(&self, html: &str) -> Result<ExtractedContent>;
fn name(&self) -> &'static str;
}
pub struct ExtractorRegistry {
extractors: Vec<Box<dyn Extractor>>,
}
impl std::fmt::Debug for ExtractorRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExtractorRegistry")
.field("extractors_count", &self.extractors.len())
.finish()
}
}
impl Default for ExtractorRegistry {
fn default() -> Self {
Self::new()
}
}
impl ExtractorRegistry {
pub fn new() -> Self {
Self {
extractors: Vec::new(),
}
}
pub fn register(&mut self, extractor: Box<dyn Extractor>) {
debug!("Registering extractor: {}", extractor.name());
self.extractors.push(extractor);
}
#[instrument(skip(self, schema_org_data))]
pub fn find_extractor_from_data(
&self,
url: &str,
schema_org_data: &[Value],
) -> Option<&dyn Extractor> {
for extractor in &self.extractors {
if extractor.can_extract(url, schema_org_data) {
debug!("Found matching extractor: {}", extractor.name());
return Some(extractor.as_ref());
}
}
None
}
}
pub struct GenericExtractor;
impl Extractor for GenericExtractor {
fn can_extract(&self, _url: &str, _schema_org_data: &[Value]) -> bool {
false
}
fn extract_from_html(&self, html: &str) -> Result<ExtractedContent> {
let mut content = ExtractedContent::default();
if let Some(title_start) = html.find("<title>") {
if let Some(title_end) = html[title_start..].find("</title>") {
let title = &html[title_start + 7..title_start + title_end];
content.title = Some(title.trim().to_string());
}
}
Ok(content)
}
fn name(&self) -> &'static str {
"generic"
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)] mod tests {
use super::*;
#[test]
fn test_generic_extractor() {
let extractor = GenericExtractor;
let html = r"<html><head><title>Test Title</title></head></html>";
let result = extractor.extract_from_html(html).unwrap();
assert_eq!(result.title, Some("Test Title".to_string()));
}
struct TestExtractor;
impl Extractor for TestExtractor {
fn can_extract(&self, url: &str, _schema_org_data: &[Value]) -> bool {
url.contains("test.com")
}
fn extract_from_html(&self, _html: &str) -> Result<ExtractedContent> {
Ok(ExtractedContent::default())
}
fn name(&self) -> &'static str {
"test"
}
}
#[test]
fn test_registry() {
let mut registry = ExtractorRegistry::new();
registry.register(Box::new(GenericExtractor));
let extractor = registry.find_extractor_from_data("https://example.com", &[]);
assert!(extractor.is_none());
registry.register(Box::new(TestExtractor));
let extractor = registry.find_extractor_from_data("https://test.com", &[]);
assert!(extractor.is_some());
assert_eq!(extractor.unwrap().name(), "test");
}
}