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}