1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! Which file contributed each scope, probe and command in a composed
//! manifest.
//!
//! [`crate::compose::load`] builds a [`Provenance`] alongside the merged
//! [`crate::manifest::Manifest`], and it rides on
//! [`crate::manifest::Located`] from there — the duplicate-key errors the
//! loader raises cannot name both source files without recording provenance
//! in the first place, and having paid for it, later tooling gets to read it
//! too: `--status` grows a `source` field per rule and `--dump-config` prints
//! the merged manifest with origins attached, both reading this type rather
//! than reaching back into the loader.
//!
//! The root manifest is recorded as the source of its own entries rather than
//! as a special case: a single-file project — no imports at all — is the
//! degenerate case of composition, not a different code path, so a lookup
//! never needs to ask "did this come from an import" before it can answer
//! "which file".
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// Which file contributed each scope, probe and command in a merged manifest.
///
/// The root manifest is recorded as the source of its own entries — a
/// single-file project is the degenerate case, not a special case — so a
/// lookup never needs to special-case "no imports". Every path is
/// canonicalized; use [`Provenance::display`] to render one the way a user
/// should see it.
#[derive(Debug, Clone, Default)]
pub struct Provenance {
/// Every file that contributed to the merge, in load order: the root
/// manifest first, then each import depth-first — the same order
/// `imports:` lists them in. A file reached twice by different routes (a
/// diamond) appears once, at the point it was first visited, matching the
/// "loads once" rule for diamonds themselves. `mmz --dump-config` numbers
/// this list to show the import graph before the entries it fed.
pub sources: Vec<PathBuf>,
/// Source file of each scope, keyed by scope name.
pub scopes: BTreeMap<String, PathBuf>,
/// Source file of each probe, keyed by probe name.
pub probes: BTreeMap<String, PathBuf>,
/// Source file of each command, keyed by command name (unique once the
/// merged manifest has validated).
pub commands: BTreeMap<String, PathBuf>,
}
impl Provenance {
/// Renders `path` project-root-relative when it is under `root`, absolute
/// otherwise — so an out-of-tree fragment (a Nix store path, say) stays
/// recognisable instead of collapsing into a long `../../..` climb.
#[must_use]
pub fn display(path: &Path, root: &Path) -> String {
Self::shorten(path, root).display().to_string()
}
/// [`Provenance::display`]'s rule as a path rather than a string: `path`
/// relative to `root` when it sits under it, unchanged otherwise.
///
/// Reports render through `display`; [`crate::compose::load`] builds the
/// paths its errors name through this, so a composition error names a
/// file the way `--status` and `--dump-config` do rather than answering
/// the same question with a second rule. Both readings only line up
/// because every path either side holds is canonical —
/// [`crate::manifest::Located::at`] canonicalizes the project root for
/// exactly that reason.
#[must_use]
pub fn shorten(path: &Path, root: &Path) -> PathBuf {
path.strip_prefix(root).unwrap_or(path).to_path_buf()
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::Provenance;
#[test]
fn display_is_root_relative_under_root_and_absolute_otherwise() {
let root = Path::new("/tmp/project");
let under = Path::new("/tmp/project/.mmz/conf.d/a.yaml");
let outside = Path::new("/nix/store/xyz/rules.yaml");
assert_eq!(Provenance::display(under, root), ".mmz/conf.d/a.yaml");
assert_eq!(
Provenance::display(outside, root),
"/nix/store/xyz/rules.yaml"
);
}
#[test]
fn shorten_is_the_same_rule_as_display() {
let root = Path::new("/tmp/project");
for path in [
Path::new("/tmp/project/.mmz/conf.d/a.yaml"),
Path::new("/nix/store/xyz/rules.yaml"),
] {
assert_eq!(
Provenance::shorten(path, root).display().to_string(),
Provenance::display(path, root),
"the loader's error paths and a report's rows answer alike",
);
}
}
}