Skip to main content

brink_source_tree/
lib.rs

1//! The `SourceTree` seam (decision-log "Native source-loading seam: a
2//! `SourceTree` trait with a map-backed impl; the root is caller-supplied",
3//! 2026-07-22; issue #1278): a host-agnostic way to enumerate and read
4//! native `.brink` source files.
5//!
6//! Extracted from `brink-db` into this L0 leaf crate (decision-log
7//! 2026-07-23, issue #1323 ruling on #1325) so both `brink-db` (native
8//! discovery) and `brink-project-config` (config discovery, #1312) can
9//! depend on it without a
10//! `project-config -> brink-db -> brink-analyzer -> project-config` cycle.
11//! `brink-db` re-exports [`SourceTree`] so `brink_db::SourceTree` still
12//! resolves for existing consumers.
13//!
14//! `InMemory` is `brink-web`'s discovery seam directly; the host-only
15//! implementations (`RealFs`, `GitRev`) live in `brink-driver` and back
16//! `brink_driver::discover_native` (issue #1288) — a normal native compile
17//! and the `brink ide` git-baseline diff path, respectively.
18//!
19//! # The contract
20//!
21//! [`SourceTree::list`] enumerates every source key under a caller-supplied
22//! `root`, **sorted deterministically by key** — never in filesystem/OS
23//! iteration order, which is unspecified and can vary between runs. Keys are
24//! root-relative (forward-slash-joined, matching how `.brink` module paths
25//! are derived downstream). [`SourceTree::read`] reads the source text for a
26//! key previously returned by `list`.
27//!
28//! The root itself is never discovered inside the seam (no implementation
29//! walks upward looking for a project marker) — it is always supplied by the
30//! caller, which resolves it however is appropriate for that host (a
31//! `brink.toml` walk-up for the CLI, a pushed project root for web/LSP).
32
33use std::collections::BTreeMap;
34use std::io;
35use std::path::Path;
36
37/// A source of `.brink` files: enumerate what exists under a root, and read
38/// any key that enumeration returned.
39///
40/// See the [module docs](self) for the full contract. Implementations must
41/// return `list()` results sorted by key, regardless of what order the
42/// underlying storage (filesystem, git tree, in-memory map) happens to
43/// iterate in.
44pub trait SourceTree {
45    /// Enumerate every source key under `root`, sorted deterministically by
46    /// key.
47    fn list(&self, root: &Path) -> io::Result<Vec<String>>;
48
49    /// Read the source text for `key` (a key previously returned by
50    /// [`list`](Self::list)).
51    fn read(&self, key: &str) -> io::Result<String>;
52}
53
54/// Map-backed [`SourceTree`]: the test and web seam.
55///
56/// Built from a `BTreeMap<key, source>`, so `list()`'s sortedness falls out
57/// of `BTreeMap`'s own ordering guarantee rather than an extra sort step —
58/// the map stays sorted by key no matter what order entries were inserted
59/// in.
60#[derive(Debug, Clone, Default)]
61pub struct InMemory {
62    files: BTreeMap<String, String>,
63}
64
65impl InMemory {
66    /// Build an in-memory `SourceTree` from a root-relative key → source map.
67    #[must_use]
68    pub fn new(files: BTreeMap<String, String>) -> Self {
69        Self { files }
70    }
71}
72
73impl SourceTree for InMemory {
74    /// `root` is unused: an `InMemory` tree's keys are already root-relative
75    /// by construction, so there is nothing further to scope by directory.
76    fn list(&self, _root: &Path) -> io::Result<Vec<String>> {
77        Ok(self.files.keys().cloned().collect())
78    }
79
80    fn read(&self, key: &str) -> io::Result<String> {
81        self.files
82            .get(key)
83            .cloned()
84            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{key}: not found")))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    /// Feeding keys in a hostile (reverse-sorted) insertion order must not
93    /// affect `list()`'s output — it always comes back key-sorted.
94    #[test]
95    fn in_memory_list_is_sorted_despite_hostile_reverse_insertion_order() {
96        let mut files = BTreeMap::new();
97        // Insert in reverse-sorted order.
98        for key in ["c/z.brink", "b/m.brink", "a/a.brink"] {
99            files.insert(key.to_string(), format!("-- {key} --"));
100        }
101        let tree = InMemory::new(files);
102
103        let keys = tree.list(Path::new(".")).expect("list succeeds");
104
105        assert_eq!(keys, vec!["a/a.brink", "b/m.brink", "c/z.brink"]);
106    }
107
108    /// Feeding keys in a hostile (shuffled, non-monotonic) insertion order
109    /// must also not affect `list()`'s output.
110    #[test]
111    fn in_memory_list_is_sorted_despite_hostile_shuffled_insertion_order() {
112        let mut files = BTreeMap::new();
113        for key in ["m/mid.brink", "a/first.brink", "z/last.brink", "b/b.brink"] {
114            files.insert(key.to_string(), format!("-- {key} --"));
115        }
116        let tree = InMemory::new(files);
117
118        let keys = tree.list(Path::new(".")).expect("list succeeds");
119
120        assert_eq!(
121            keys,
122            vec!["a/first.brink", "b/b.brink", "m/mid.brink", "z/last.brink"]
123        );
124    }
125
126    /// `read()` returns exactly the source text a key was constructed with.
127    #[test]
128    fn in_memory_read_round_trips() {
129        let mut files = BTreeMap::new();
130        files.insert(
131            "market/barter.brink".to_string(),
132            "flow barter() {}".to_string(),
133        );
134        files.insert("main.brink".to_string(), "flow main() {}".to_string());
135        let tree = InMemory::new(files);
136
137        assert_eq!(
138            tree.read("market/barter.brink").expect("key exists"),
139            "flow barter() {}"
140        );
141        assert_eq!(
142            tree.read("main.brink").expect("key exists"),
143            "flow main() {}"
144        );
145    }
146
147    /// Reading a key that was never inserted is a `NotFound` I/O error, not
148    /// a panic — `InMemory` is a real `SourceTree`, not a test-only stub
149    /// that can assume well-formed callers.
150    #[test]
151    fn in_memory_read_missing_key_is_not_found() {
152        let tree = InMemory::new(BTreeMap::new());
153
154        let err = tree.read("missing.brink").expect_err("key absent");
155
156        assert_eq!(err.kind(), io::ErrorKind::NotFound);
157    }
158
159    /// `list()` on an empty tree is `Ok(vec![])`, not an error.
160    #[test]
161    fn in_memory_list_empty_is_ok_empty() {
162        let tree = InMemory::new(BTreeMap::new());
163
164        assert_eq!(
165            tree.list(Path::new(".")).expect("list succeeds"),
166            Vec::<String>::new()
167        );
168    }
169}