Skip to main content

termdoc_core/
registry.rs

1//! The registry of readers and detectors.
2//!
3//! The key point of the design (docs/DESIGN.md ยง2.4): an external plugin is wrapped in a
4//! proxy that implements `DocumentReader`, so the core never has an "is this a plugin?"
5//! branch. One code path.
6
7use std::sync::Arc;
8
9use crate::{Detection, Detector, DocumentReader, FormatId, Source, confidence};
10
11#[derive(Default)]
12pub struct Registry {
13    readers: Vec<Arc<dyn DocumentReader>>,
14    detectors: Vec<Arc<dyn Detector>>,
15}
16
17impl std::fmt::Debug for Registry {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("Registry")
20            .field("formats", &self.formats())
21            .field("detectors", &self.detectors.len())
22            .finish()
23    }
24}
25
26impl Registry {
27    pub fn new() -> Self {
28        Registry::default()
29    }
30
31    pub fn register_reader(&mut self, reader: Arc<dyn DocumentReader>) -> &mut Self {
32        self.readers.push(reader);
33        self
34    }
35
36    pub fn register_detector(&mut self, detector: Arc<dyn Detector>) -> &mut Self {
37        self.detectors.push(detector);
38        self
39    }
40
41    /// Finds the reader that handles a format. The last one registered wins, so a plugin
42    /// can deliberately replace a built-in.
43    pub fn reader_for(&self, format: FormatId) -> Option<&dyn DocumentReader> {
44        self.readers
45            .iter()
46            .rev()
47            .find(|r| r.id() == format)
48            .map(|r| r.as_ref())
49    }
50
51    pub fn formats(&self) -> Vec<FormatId> {
52        let mut v: Vec<_> = self.readers.iter().map(|r| r.id()).collect();
53        v.dedup();
54        v
55    }
56
57    /// Runs every detector and returns the most confident result.
58    ///
59    /// Ties go to whichever was registered later: plugins register after the built-ins,
60    /// so they can claim a format at equal confidence.
61    pub fn detect(&self, src: &Source) -> Option<Detection> {
62        let mut best: Option<Detection> = None;
63        for d in &self.detectors {
64            if let Some(candidate) = d.sniff(src) {
65                let better = match &best {
66                    None => true,
67                    Some(b) => candidate.confidence >= b.confidence,
68                };
69                if better {
70                    best = Some(candidate);
71                }
72            }
73        }
74        best
75    }
76
77    /// Every candidate, ordered by descending confidence. This is what `--explain`
78    /// prints: seeing what was rejected is half the useful information.
79    pub fn detect_all(&self, src: &Source) -> Vec<Detection> {
80        let mut all: Vec<_> = self.detectors.iter().filter_map(|d| d.sniff(src)).collect();
81        all.sort_by_key(|d| std::cmp::Reverse(d.confidence));
82        all
83    }
84
85    /// Detection with the final safety net: if nobody claims the source, decide between
86    /// text and binary. Never returns `None`, because "I don't know what this is" is not
87    /// an acceptable answer from a universal viewer.
88    pub fn detect_or_fallback(&self, src: &Source) -> Detection {
89        if let Some(d) = self.detect(src) {
90            return d;
91        }
92        if src.looks_binary() {
93            Detection::new(
94                FormatId::Binary,
95                confidence::FALLBACK,
96                "no matches; there is a NUL byte in the prefix",
97            )
98        } else {
99            Detection::new(
100                FormatId::PlainText,
101                confidence::FALLBACK,
102                "no matches; the prefix is text",
103            )
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::{Events, ReadContext, Result};
112
113    struct Fake(FormatId);
114
115    impl DocumentReader for Fake {
116        fn id(&self) -> FormatId {
117            self.0
118        }
119        fn read<'a>(&self, _src: &'a Source, _ctx: &ReadContext) -> Result<Events<'a>> {
120            Ok(Box::new(std::iter::empty()))
121        }
122    }
123
124    struct Sniffer(FormatId, u8);
125
126    impl Detector for Sniffer {
127        fn sniff(&self, _src: &Source) -> Option<Detection> {
128            Some(Detection::new(self.0, self.1, "test"))
129        }
130    }
131
132    #[test]
133    fn the_last_registered_reader_wins() {
134        let mut r = Registry::new();
135        r.register_reader(Arc::new(Fake(FormatId::Markdown)));
136        r.register_reader(Arc::new(Fake(FormatId::Markdown)));
137        // Registering the same format twice must not break resolution: it is the
138        // mechanism by which a plugin replaces a built-in.
139        assert!(r.reader_for(FormatId::Markdown).is_some());
140        assert!(r.reader_for(FormatId::Pdf).is_none());
141    }
142
143    #[test]
144    fn detect_picks_the_highest_confidence() {
145        let mut r = Registry::new();
146        r.register_detector(Arc::new(Sniffer(FormatId::PlainText, 30)));
147        r.register_detector(Arc::new(Sniffer(FormatId::Markdown, 70)));
148        r.register_detector(Arc::new(Sniffer(FormatId::Csv, 50)));
149        assert_eq!(
150            r.detect(&Source::from_bytes("t", "x")).unwrap().format,
151            FormatId::Markdown
152        );
153    }
154
155    #[test]
156    fn detect_all_sorts_descending() {
157        let mut r = Registry::new();
158        r.register_detector(Arc::new(Sniffer(FormatId::PlainText, 30)));
159        r.register_detector(Arc::new(Sniffer(FormatId::Markdown, 70)));
160        let all = r.detect_all(&Source::from_bytes("t", "x"));
161        assert_eq!(all.len(), 2);
162        assert!(all[0].confidence >= all[1].confidence);
163    }
164
165    #[test]
166    fn fallback_distinguishes_text_from_binary() {
167        let r = Registry::new();
168        assert_eq!(
169            r.detect_or_fallback(&Source::from_bytes("t", "hello"))
170                .format,
171            FormatId::PlainText
172        );
173        assert_eq!(
174            r.detect_or_fallback(&Source::from_bytes("t", vec![0, 1, 2]))
175                .format,
176            FormatId::Binary
177        );
178    }
179}