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}