1use crate::types::ExtractedContent;
4use eyre::Result;
5use serde_json::Value;
6use tracing::{debug, instrument};
7
8pub trait Extractor: Send + Sync {
10 fn can_extract(&self, url: &str, schema_org_data: &[Value]) -> bool;
12
13 fn extract_from_html(&self, html: &str) -> Result<ExtractedContent>;
15
16 fn name(&self) -> &'static str;
18}
19
20pub struct ExtractorRegistry {
22 extractors: Vec<Box<dyn Extractor>>,
23}
24
25impl std::fmt::Debug for ExtractorRegistry {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("ExtractorRegistry")
28 .field("extractors_count", &self.extractors.len())
29 .finish()
30 }
31}
32
33impl Default for ExtractorRegistry {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl ExtractorRegistry {
40 pub fn new() -> Self {
42 Self {
46 extractors: Vec::new(),
47 }
48 }
49
50 pub fn register(&mut self, extractor: Box<dyn Extractor>) {
52 debug!("Registering extractor: {}", extractor.name());
53 self.extractors.push(extractor);
54 }
55
56 #[instrument(skip(self, schema_org_data))]
58 pub fn find_extractor_from_data(
59 &self,
60 url: &str,
61 schema_org_data: &[Value],
62 ) -> Option<&dyn Extractor> {
63 for extractor in &self.extractors {
64 if extractor.can_extract(url, schema_org_data) {
65 debug!("Found matching extractor: {}", extractor.name());
66 return Some(extractor.as_ref());
67 }
68 }
69 None
70 }
71}
72
73pub struct GenericExtractor;
75
76impl Extractor for GenericExtractor {
77 fn can_extract(&self, _url: &str, _schema_org_data: &[Value]) -> bool {
78 false
81 }
82
83 fn extract_from_html(&self, html: &str) -> Result<ExtractedContent> {
84 let mut content = ExtractedContent::default();
86
87 if let Some(title_start) = html.find("<title>") {
89 if let Some(title_end) = html[title_start..].find("</title>") {
90 let title = &html[title_start + 7..title_start + title_end];
91 content.title = Some(title.trim().to_string());
92 }
93 }
94
95 Ok(content)
96 }
97
98 fn name(&self) -> &'static str {
99 "generic"
100 }
101}
102
103#[cfg(test)]
104#[allow(clippy::disallowed_methods)] mod tests {
106 use super::*;
107
108 #[test]
109 fn test_generic_extractor() {
110 let extractor = GenericExtractor;
111 let html = r"<html><head><title>Test Title</title></head></html>";
112
113 let result = extractor.extract_from_html(html).unwrap();
114 assert_eq!(result.title, Some("Test Title".to_string()));
115 }
116
117 struct TestExtractor;
118
119 impl Extractor for TestExtractor {
120 fn can_extract(&self, url: &str, _schema_org_data: &[Value]) -> bool {
121 url.contains("test.com")
122 }
123 fn extract_from_html(&self, _html: &str) -> Result<ExtractedContent> {
124 Ok(ExtractedContent::default())
125 }
126 fn name(&self) -> &'static str {
127 "test"
128 }
129 }
130
131 #[test]
132 fn test_registry() {
133 let mut registry = ExtractorRegistry::new();
134 registry.register(Box::new(GenericExtractor));
135
136 let extractor = registry.find_extractor_from_data("https://example.com", &[]);
138 assert!(extractor.is_none());
139
140 registry.register(Box::new(TestExtractor));
142 let extractor = registry.find_extractor_from_data("https://test.com", &[]);
143 assert!(extractor.is_some());
144 assert_eq!(extractor.unwrap().name(), "test");
145 }
146}