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    /// Every engine-native **fragment** file a pack may `ref:` (ADR-0018).
34    /// Return an empty vec when the provider serves no fragments.
35    ///
36    /// Deliberately **not** defaulted. Every implementation lives in this
37    /// workspace, so a default body buys no compatibility — and it cost the
38    /// feature once already: a delegating wrapper that forwarded the other two
39    /// discoveries silently inherited "no fragments", so the editor reported
40    /// every `ref:` as `unknown_ref` while the same suite ran green. A required
41    /// method makes a forwarding provider fail to compile instead.
42    fn discover_fragments(&self) -> Result<Vec<String>, ProviderError>;
43
44    /// The raw bytes of one source, keyed by a name returned from discovery.
45    fn read(&self, name: &str) -> Result<Arc<str>, ProviderError>;
46}
47
48#[cfg(test)]
49mod tests {
50    #![allow(clippy::unwrap_used)]
51
52    use super::*;
53
54    struct Fake;
55    impl SourceProvider for Fake {
56        fn discover_features(&self) -> Result<Vec<String>, ProviderError> {
57            Ok(vec!["a.feature".to_owned()])
58        }
59        fn discover_packs(&self) -> Result<Vec<String>, ProviderError> {
60            Ok(vec!["packs/p.yaml".to_owned()])
61        }
62        fn discover_fragments(&self) -> Result<Vec<String>, ProviderError> {
63            Ok(Vec::new())
64        }
65        fn read(&self, name: &str) -> Result<Arc<str>, ProviderError> {
66            match name {
67                "a.feature" => Ok(Arc::from("Feature: X\n")),
68                _ => Err(ProviderError(format!("no such source: {name}"))),
69            }
70        }
71    }
72
73    #[test]
74    fn trait_is_object_safe_and_usable_as_dyn() {
75        let p: &dyn SourceProvider = &Fake;
76        assert_eq!(p.discover_features().unwrap(), vec!["a.feature".to_owned()]);
77        assert_eq!(&*p.read("a.feature").unwrap(), "Feature: X\n");
78        assert!(p.read("missing").is_err());
79    }
80}