Skip to main content

rsynko_memory/
extraction.rs

1use crate::{Extraction, ExtractorKey};
2use rsynko_media::{ExtractionApplyAlg, ExtractionCatalogAlg, MediaSorts};
3use std::cell::RefCell;
4use thiserror::Error;
5
6/// Denotes the result a reference extractor assigns to an accepted URL.
7#[derive(Clone, Debug, PartialEq)]
8pub enum ReferenceExtractionOutcome {
9    /// Denotes successful extraction.
10    Success(Extraction),
11    /// Denotes an extractor failure with a stable message.
12    Failure(String),
13}
14
15/// Interprets one extractor as inspectable prefix matching and a fixed outcome.
16#[derive(Clone, Debug, PartialEq)]
17pub struct ReferenceExtractor {
18    key: ExtractorKey,
19    accepted_prefixes: Vec<String>,
20    outcome: ReferenceExtractionOutcome,
21}
22
23impl ReferenceExtractor {
24    /// Constructs a deterministic extractor interpretation.
25    #[must_use]
26    pub fn new(
27        key: impl Into<String>,
28        accepted_prefixes: impl IntoIterator<Item = impl Into<String>>,
29        outcome: ReferenceExtractionOutcome,
30    ) -> Self {
31        Self {
32            key: ExtractorKey::new(key),
33            accepted_prefixes: accepted_prefixes.into_iter().map(Into::into).collect(),
34            outcome,
35        }
36    }
37
38    /// Constructs a successful deterministic extractor.
39    #[must_use]
40    pub fn succeeds(
41        key: impl Into<String>,
42        accepted_prefix: impl Into<String>,
43        extraction: Extraction,
44    ) -> Self {
45        Self::new(
46            key,
47            [accepted_prefix],
48            ReferenceExtractionOutcome::Success(extraction),
49        )
50    }
51
52    /// Constructs a failing deterministic extractor.
53    #[must_use]
54    pub fn fails(
55        key: impl Into<String>,
56        accepted_prefix: impl Into<String>,
57        message: impl Into<String>,
58    ) -> Self {
59        Self::new(
60            key,
61            [accepted_prefix],
62            ReferenceExtractionOutcome::Failure(message.into()),
63        )
64    }
65}
66
67/// Denotes failures of the deterministic extraction interpreter.
68#[derive(Debug, Error, PartialEq, Eq)]
69pub enum ReferenceExtractionError {
70    /// Denotes application of a key absent from the catalog.
71    #[error("unknown extractor {0:?}")]
72    UnknownExtractor(ExtractorKey),
73    /// Denotes a failure reported by a reference extractor.
74    #[error("extractor {extractor:?} failed: {message}")]
75    ExtractorFailed {
76        /// Names the failing extractor.
77        extractor: ExtractorKey,
78        /// Preserves its failure message.
79        message: String,
80    },
81}
82
83/// Interprets an ordered extractor catalog in memory.
84#[derive(Debug, Default)]
85pub struct ReferenceExtractorRegistry {
86    extractors: Vec<ReferenceExtractor>,
87    applications: RefCell<Vec<ExtractorKey>>,
88}
89
90impl ReferenceExtractorRegistry {
91    /// Appends an extractor at the next catalog selection position.
92    pub fn push(&mut self, extractor: ReferenceExtractor) {
93        self.extractors.push(extractor);
94    }
95
96    /// Observes extractor applications in application order.
97    #[must_use]
98    pub fn applications(&self) -> Vec<ExtractorKey> {
99        self.applications.borrow().clone()
100    }
101}
102
103impl MediaSorts for ReferenceExtractorRegistry {
104    type Value = ();
105    type Metadata = ();
106    type Format = ();
107    type Artifact = ();
108    type Media = ();
109    type Extraction = Extraction;
110    type Extractor = ExtractorKey;
111    type Predicate = ();
112    type Selection = ();
113    type Output = ();
114}
115
116impl ExtractionCatalogAlg for ReferenceExtractorRegistry {
117    fn extractor_keys(&self) -> impl Iterator<Item = &ExtractorKey> {
118        self.extractors.iter().map(|extractor| &extractor.key)
119    }
120
121    fn extractor_accepts(&self, extractor: &ExtractorKey, url: &str) -> bool {
122        self.extractors
123            .iter()
124            .find(|candidate| candidate.key == *extractor)
125            .is_some_and(|candidate| {
126                candidate
127                    .accepted_prefixes
128                    .iter()
129                    .any(|prefix| url.starts_with(prefix))
130            })
131    }
132}
133
134impl ExtractionApplyAlg for ReferenceExtractorRegistry {
135    type Error = ReferenceExtractionError;
136
137    fn extract_with(
138        &self,
139        extractor: &ExtractorKey,
140        _url: &str,
141    ) -> Result<Extraction, Self::Error> {
142        let candidate = self
143            .extractors
144            .iter()
145            .find(|candidate| candidate.key == *extractor)
146            .ok_or_else(|| ReferenceExtractionError::UnknownExtractor(extractor.clone()))?;
147        self.applications.borrow_mut().push(extractor.clone());
148        match &candidate.outcome {
149            ReferenceExtractionOutcome::Success(extraction) => Ok(extraction.clone()),
150            ReferenceExtractionOutcome::Failure(message) => {
151                Err(ReferenceExtractionError::ExtractorFailed {
152                    extractor: extractor.clone(),
153                    message: message.clone(),
154                })
155            }
156        }
157    }
158}