Introduce scoped config with ConfigSource
Replace the flat config_mounts Vec in Ramekin with a ResolvedConfig
that pairs resolved mounts with their ConfigSource (Default or User).
Config::load() now returns a ResolvedConfig directly, resolving mounts
internally. The config command displays the source in the section
header, e.g. 'Config volume mounts (default)' or
'Config volume mounts (~/.config/ramekin/config.kdl)'.
This sets up the config system for adding project-level configs later.
diff --git a/src/config.rs b/src/config.rs
index 78be814..b3a2052 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,3 +1,4 @@
+use std::fmt;
use std::path::PathBuf;
use color_eyre::eyre::{Context, Result};
@@ -19,6 +20,31 @@ pub struct Mount {
pub writable: bool,
}
+/// Where a configuration was loaded from.
+#[derive(Debug, PartialEq)]
+pub enum ConfigSource {
+ /// Hardcoded defaults (no config file found).
+ Default,
+ /// User-level config file at the given path.
+ User(PathBuf),
+}
+
+impl fmt::Display for ConfigSource {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Default => write!(f, "default"),
+ Self::User(path) => write!(f, "{}", path.display()),
+ }
+ }
+}
+
+/// A loaded configuration together with its source.
+#[derive(Debug)]
+pub struct ResolvedConfig {
+ pub source: ConfigSource,
+ pub mounts: Vec<ResolvedMount>,
+}
+
/// A mount with tilde-expanded paths ready for Docker.
#[derive(Debug, PartialEq)]
pub struct ResolvedMount {
@@ -34,24 +60,35 @@ impl Default for Config {
}
impl Config {
- /// Load configuration from `~/.config/ramekin/config.kdl`.
+ /// Load configuration and resolve mounts.
///
- /// Falls back to hardcoded defaults when the file doesn't exist.
- /// Returns an error if the file exists but can't be parsed.
- pub fn load() -> Result<Self> {
+ /// Tries `~/.config/ramekin/config.kdl` first; falls back to hardcoded
+ /// defaults when the file doesn't exist. Returns an error if the file
+ /// exists but can't be parsed.
+ pub fn load() -> Result<ResolvedConfig> {
let xdg = xdg::BaseDirectories::with_prefix("ramekin");
let config_path = xdg
.place_config_file("config.kdl")
.wrap_err("failed to determine config file path")?;
- if !config_path.exists() {
- return Ok(Self::fallback());
+ if config_path.exists() {
+ let content =
+ fs_err::read_to_string(&config_path).wrap_err("failed to read config file")?;
+ let config: Config =
+ serde_kdl2::from_str(&content).wrap_err("failed to parse config file")?;
+ let mounts = config.resolve_mounts();
+ Ok(ResolvedConfig {
+ source: ConfigSource::User(config_path),
+ mounts,
+ })
+ } else {
+ let config = Self::fallback();
+ let mounts = config.resolve_mounts();
+ Ok(ResolvedConfig {
+ source: ConfigSource::Default,
+ mounts,
+ })
}
-
- let content =
- fs_err::read_to_string(&config_path).wrap_err("failed to read config file")?;
-
- serde_kdl2::from_str(&content).wrap_err("failed to parse config file")
}
/// Fallback configuration with hardcoded mounts.
@@ -76,11 +113,9 @@ impl Config {
],
}
}
-}
-impl Config {
/// Resolve all mounts, skipping any whose source directory does not exist.
- pub fn resolve_mounts(&self) -> Vec<ResolvedMount> {
+ fn resolve_mounts(&self) -> Vec<ResolvedMount> {
self.mounts.iter().filter_map(|m| m.resolve()).collect()
}
}
@@ -318,4 +353,16 @@ mod tests {
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].target, "/container/tmp");
}
+
+ #[test]
+ fn config_source_display_default() {
+ let source = ConfigSource::Default;
+ assert_eq!(source.to_string(), "default");
+ }
+
+ #[test]
+ fn config_source_display_user() {
+ let source = ConfigSource::User(PathBuf::from("/home/user/.config/ramekin/config.kdl"));
+ assert_eq!(source.to_string(), "/home/user/.config/ramekin/config.kdl");
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 8d182f9..c45ada5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -68,7 +68,7 @@ struct Ramekin {
cache_dir: PathBuf,
custom_dockerfile: Option<PathBuf>,
builtin_mounts: Vec<config::ResolvedMount>,
- config_mounts: Vec<config::ResolvedMount>,
+ config: config::ResolvedConfig,
}
impl Ramekin {
@@ -141,7 +141,6 @@ impl Ramekin {
// Config mounts (user-configurable, skipped when source doesn't exist)
let config = config::Config::load().wrap_err("failed to load ramekin configuration")?;
- let config_mounts = config.resolve_mounts();
Ok(Self {
workspace,
@@ -151,7 +150,7 @@ impl Ramekin {
cache_dir,
custom_dockerfile,
builtin_mounts,
- config_mounts,
+ config,
})
}
@@ -196,11 +195,11 @@ impl Ramekin {
}
println!();
- println!("Config volume mounts");
- if self.config_mounts.is_empty() {
+ println!("Config volume mounts ({})", self.config.source);
+ if self.config.mounts.is_empty() {
println!(" (none)");
} else {
- for m in &self.config_mounts {
+ for m in &self.config.mounts {
println!(
" {} {} → {}",
check(&m.source),
@@ -272,7 +271,7 @@ impl Ramekin {
let all_mounts: Vec<_> = self
.builtin_mounts
.iter()
- .chain(&self.config_mounts)
+ .chain(&self.config.mounts)
.collect();
let compose = generate_compose(&dockerfile, &build_context, &all_mounts);
let compose_file = session_dir.join("compose.yml");