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
//! Resolves one `imports:` entry to the concrete manifest file paths it
//! names.
//!
//! An entry can name a file directly, in which case it names exactly that
//! file, or a directory, in which case it expands to every `*.yaml` and
//! `*.yml` file directly inside it (not recursive), sorted lexically by file
//! name — the drop-in ergonomics of a `conf.d` convention while keeping the
//! directory *declared*, so a stray file elsewhere never changes behaviour.
//! [`crate::compose`] resolves each entry through [`expand_import`], in
//! listed order, then loads each resulting file itself: path resolution and
//! the cycle/diamond bookkeeping that loading requires are deliberately
//! separate concerns, so this module owns none of the latter.
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::provenance::Provenance;
/// Resolves one `imports:` entry declared in `importer` (whose directory is
/// `base_dir`) to the manifest files it names: itself, if it names a file, or
/// the `*.yaml`/`*.yml` files directly inside it, lexically sorted, if it
/// names a directory.
///
/// `root` is the project root the paths in an error are rendered against —
/// relative under it, absolute otherwise, as in [`crate::compose::load`]. It
/// takes no part in resolution: that is `base_dir`'s job, and an entry that
/// resolves outside the root is a supported case, not a mistake.
///
/// # Errors
///
/// Returns [`Error::ImportMissing`] if the resolved path is neither a file nor
/// a directory.
pub(crate) fn expand_import(
importer: &Path,
base_dir: &Path,
entry: &str,
root: &Path,
) -> Result<Vec<PathBuf>> {
let candidate = PathBuf::from(entry);
let resolved = if candidate.is_absolute() {
candidate
} else {
base_dir.join(candidate)
};
if resolved.is_dir() {
return list_yaml_files(&resolved, root);
}
if resolved.is_file() {
return Ok(vec![resolved]);
}
Err(Error::ImportMissing {
importer: Provenance::shorten(importer, root),
path: Provenance::shorten(&resolved, root),
})
}
/// Lists the `*.yaml` and `*.yml` files directly inside `dir` (not recursive),
/// sorted lexically by file name. `root` renders the path in an error, as in
/// [`crate::compose::load`].
///
/// # Errors
///
/// Returns [`Error::ImportNotReadable`] if `dir` (or an entry inside it)
/// cannot be read.
fn list_yaml_files(dir: &Path, root: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
let entries = std::fs::read_dir(dir).map_err(|source| Error::ImportNotReadable {
path: Provenance::shorten(dir, root),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| Error::ImportNotReadable {
path: Provenance::shorten(dir, root),
source,
})?;
let candidate = entry.path();
if !candidate.is_file() {
continue;
}
let is_yaml = candidate
.extension()
.and_then(std::ffi::OsStr::to_str)
.is_some_and(|extension| extension == "yaml" || extension == "yml");
if is_yaml {
files.push(candidate);
}
}
files.sort();
Ok(files)
}