Skip to main content

proef_core/
provider.rs

1//! The injected source-access seam. `proef-core` never reads a file; it asks a
2//! `SourceProvider` for the units under a suite and for their bytes. The CLI
3//! provides a disk-backed impl; the LSP provides an overlay-then-disk impl.
4//! This is the ADR-0012 pattern — IO at the edge, injected into the sans-IO core.
5
6use std::sync::Arc;
7
8/// A source-access failure (missing file, unreadable path, walk error).
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ProviderError(pub String);
11
12impl std::fmt::Display for ProviderError {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        f.write_str(&self.0)
15    }
16}
17
18impl std::error::Error for ProviderError {}
19
20/// Discovers and reads the feature files and macro packs of one suite.
21///
22/// Source *names* are filesystem paths rendered as strings — the identity used
23/// by `Diag.source_name` and `PackSource.name` throughout the pipeline. `read`
24/// returns **raw** bytes; normalization (BOM strip, trailing newline) is the
25/// parser's job, so spans stay consistent with the CLI.
26pub trait SourceProvider {
27    /// Every feature source name under the suite, in the order the provider finds them.
28    fn discover_features(&self) -> Result<Vec<String>, ProviderError>;
29
30    /// Every macro pack source name under the suite, in the order the provider finds them.
31    fn discover_packs(&self) -> Result<Vec<String>, ProviderError>;
32
33    /// The raw bytes of one source, keyed by a name returned from discovery.
34    fn read(&self, name: &str) -> Result<Arc<str>, ProviderError>;
35}
36
37#[cfg(test)]
38mod tests {
39    #![allow(clippy::unwrap_used)]
40
41    use super::*;
42
43    struct Fake;
44    impl SourceProvider for Fake {
45        fn discover_features(&self) -> Result<Vec<String>, ProviderError> {
46            Ok(vec!["a.feature".to_owned()])
47        }
48        fn discover_packs(&self) -> Result<Vec<String>, ProviderError> {
49            Ok(vec!["packs/p.yaml".to_owned()])
50        }
51        fn read(&self, name: &str) -> Result<Arc<str>, ProviderError> {
52            match name {
53                "a.feature" => Ok(Arc::from("Feature: X\n")),
54                _ => Err(ProviderError(format!("no such source: {name}"))),
55            }
56        }
57    }
58
59    #[test]
60    fn trait_is_object_safe_and_usable_as_dyn() {
61        let p: &dyn SourceProvider = &Fake;
62        assert_eq!(p.discover_features().unwrap(), vec!["a.feature".to_owned()]);
63        assert_eq!(&*p.read("a.feature").unwrap(), "Feature: X\n");
64        assert!(p.read("missing").is_err());
65    }
66}