Repository: Canop/bacon (language: rust)

Issue:
Allow storing config in Cargo.toml
Cargo manifests have a standardized way to store TOML data that configures third party tooling. The `[package.metadata.bacon]` space in `Cargo.toml` could be used to hold Bacon configs instead of a stand alone `bacon.toml` file. This is supported by many developer tools that are used in Rust or Rust adjacent projects, e.g. `ruff`, `typos`, `git-cliff`, and many more.

Obviously this should be optional, but checking that key in the manifest for config data in addition to the default stand alone config location would be appreciated by projects that already have enough clutter in the project root file space and prefer to group tooling configs together.


Discussion / hints:
Interesting.

A problem is the bacon.toml file is neither small nor flat. This would probably be quite ugly.

Also, the need to prefix all keys would make it harder to copy paste between config files.

Did you try writing a Cargo.toml file with bacon config inside to see how it would look ?
Yes I am aware of exactly how it would look, and yes the extra prefix to the namespace adds verbosity. But "that would be ugly" sees like a subjective metric that should be left up to the end user. I'm not suggesting making it the default or taking away the current stand alone config option. I work on projects that go both ways and there are pros and cons for both. As far as Bacon's side goes all you have to do is load the TOML and query the correct namespace, then remove the prefix from everything. It shouldn't be too much trouble to implement or maintain and it lets projects manage their tooling however they want to along with many other tools that work the same way.
> It shouldn't be too much trouble to implement

Exact.

> then remove the prefix from everything

This won't be necessary if the TOML file is deserialized into the right struct.

Can you build a complete example file to serve as example and as test ?
Currently working `bacon.toml`:

```toml
default_job = "build"

[jobs.build]
command = [
  "cargo", "build",
  "--features", "luajit",
  "--color", "always"
]

```

Proposed lines to enable the same behavior in `Cargo.toml`:

```toml
[package.metadata.bacon]
default_job = "build"

[package.metadata.bacon.jobs.build]
command = [
  "cargo", "build",
  "--features", "luajit",
  "--color", "always"
]
```

Fix patch:
diff --git a/src/cli.rs b/src/cli.rs
index 3a70ff2..c2320f3 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -59,7 +59,7 @@ pub fn run() -> anyhow::Result<()> {
     }
 
     let context = Context::new(&args)?;
-    info!("mission context: {:#?}", &context);
+    debug!("mission context: {:#?}", &context);
 
     if args.init {
         let package_config_path = context.package_config_path();
diff --git a/src/conf/cargo_wrapped_config.rs b/src/conf/cargo_wrapped_config.rs
new file mode 100644
index 0000000..73822e4
--- /dev/null
+++ b/src/conf/cargo_wrapped_config.rs
@@ -0,0 +1,46 @@
+use {
+    super::Config,
+    anyhow::*,
+    serde::Deserialize,
+    std::path::Path,
+};
+
+#[derive(Deserialize)]
+struct CargoWrappedConfig {
+    workspace: Option<WrappedMetadata>,
+    package: Option<WrappedMetadata>,
+}
+#[derive(Deserialize)]
+struct WrappedMetadata {
+    metadata: Option<WrappedConfig>,
+}
+#[derive(Deserialize)]
+struct WrappedConfig {
+    bacon: Option<Config>,
+}
+
+pub fn load_config_from_cargo_toml(cargo_file_path: &Path) -> Result<Vec<Config>> {
+    if !cargo_file_path.exists() {
+        return Ok(Vec::default());
+    }
+    let cargo_toml = std::fs::read_to_string(cargo_file_path)?;
+    let mut cargo: CargoWrappedConfig = toml::from_str(&cargo_toml)?;
+    let mut configs = Vec::new();
+    let worskpace_config = cargo
+        .workspace
+        .take()
+        .and_then(|workspace| workspace.metadata)
+        .and_then(|metadata| metadata.bacon);
+    if let Some(config) = worskpace_config {
+        configs.push(config);
+    }
+    let worskpace_config = cargo
+        .package
+        .take()
+        .and_then(|package| package.metadata)
+        .and_then(|metadata| metadata.bacon);
+    if let Some(config) = worskpace_config {
+        configs.push(config);
+    }
+    Ok(configs)
+}
diff --git a/src/conf/config.rs b/src/conf/config.rs
index 89c7943..93073ae 100644
--- a/src/conf/config.rs
+++ b/src/conf/config.rs
@@ -85,6 +85,23 @@ pub struct Config {
 }
 
 impl Config {
+    /// Load from zero to two configuration items from the provided path which
+    /// must be in TOML format but may not exist.
+    ///
+    /// Expected structures are either bacon config or a cargo.toml file (which
+    /// may contain a workspace.metadata.bacon key and a package.metadata.bacon key).
+    pub fn from_path_detect(path: &Path) -> Result<Vec<Self>> {
+        if !path.exists() {
+            return Ok(Vec::default());
+        }
+        let file_name = path.file_name().and_then(|f| f.to_str());
+        if file_name == Some("Cargo.toml") {
+            load_config_from_cargo_toml(path)
+        } else {
+            Ok(vec![Self::from_path(path)?])
+        }
+    }
+    /// Load a configuration item filling the provided path in TOML
     pub fn from_path(path: &Path) -> Result<Self> {
         let conf = toml::from_str::<Self>(&fs::read_to_string(path)?)
             .with_context(|| format!("Failed to parse configuration file at {:?}", path))?;
diff --git a/src/conf/mod.rs b/src/conf/mod.rs
index 21c7474..b970ea9 100644
--- a/src/conf/mod.rs
+++ b/src/conf/mod.rs
@@ -1,5 +1,6 @@
 mod action;
 mod args;
+mod cargo_wrapped_config;
 mod config;
 mod defaults;
 mod keybindings;
@@ -9,6 +10,7 @@ mod settings;
 pub use {
     action::*,
     args::*,
+    cargo_wrapped_config::*,
     config::*,
     defaults::*,
     keybindings::*,
diff --git a/src/conf/settings.rs b/src/conf/settings.rs
index b853f26..8881baf 100644
--- a/src/conf/settings.rs
+++ b/src/conf/settings.rs
@@ -13,11 +13,6 @@ use {
 
 /// The settings used in the application.
 ///
-/// They're made from default, overriden (in order)
-/// by the general prefs (global prefs.toml file), by
-/// the package config (bacon.toml file in the project
-/// directory) and by the launch arguments.
-///
 /// They're immutable during the execution of the missions.
 #[derive(Debug, Clone)]
 pub struct Settings {
@@ -83,10 +78,15 @@ impl Settings {
     ///
     ///
     /// Hardcoded defaults are overriden by the following configuration elements, in order:
-    /// * the global `prefs.toml`
+    /// * the default `bacon.toml` file (embedded in the binary)
+    /// * the global `prefs.toml`, from user config directory
     /// * the file whose path is in environment variable `BACON_PREFS`
-    /// * the workspace level `bacon.toml` file
-    /// * the package level `bacon.toml` file
+    /// * the workspace.metadata.bacon config in the workspace level `Cargo.toml` file
+    /// * the workspace level `bacon.toml` file in workspace-root/bacon.toml
+    /// * the workspace level `bacon.toml` file in workspace-root/.config/.bacon.toml
+    /// * the package.metadata.bacon config in the package level `Cargo.toml` file
+    /// * the package level `bacon.toml` file in package-root/.bacon.toml
+    /// * the package level `bacon.toml` file in package-root/.config/.bacon.toml
     /// * the file whose path is in environment variable `BACON_CONFIG`
     /// * args given as arguments, coming from the cli call
     pub fn read(
@@ -98,47 +98,30 @@ impl Settings {
         let default_package_config = Config::default_package_config();
         settings.apply_config(&default_package_config);
 
-        if let Some(prefs_path) = bacon_prefs_path() {
-            if prefs_path.exists() {
-                let prefs = Config::from_path(&prefs_path)?;
-                info!("prefs: {:#?}", &prefs);
-                settings.register_config_file(prefs_path);
-                settings.apply_config(&prefs);
+        let paths = vec![
+            bacon_prefs_path(),
+            config_path_from_env("BACON_PREFS"),
+            context.workspace_cargo_path(),
+            context.workspace_config_path(),
+            context.workspace_dot_config_path(),
+            Some(context.package_cargo_path()),
+            Some(context.package_config_path()),
+            Some(context.package_dot_config_path()),
+            config_path_from_env("BACON_CONFIG"),
+        ];
+        for path in paths.into_iter().flatten() {
+            if path.exists() {
+                let configs = Config::from_path_detect(&path)?;
+                if !configs.is_empty() {
+                    info!("config loaded from {:?}", path);
+                    settings.register_config_file(path.clone());
+                    for config in configs {
+                        settings.apply_config(&config);
+                    }
+                }
             }
         }
 
-        if let Some(config_path) = config_path_from_env("BACON_PREFS") {
-            let config = Config::from_path(&config_path)?;
-            info!("config from env: {:#?}", &config);
-            settings.register_config_file(config_path);
-            settings.apply_config(&config);
-        }
-
-        let workspace_config_path = context.workspace_config_path();
-        let package_config_path = context.package_config_path();
-
-        if let Some(workspace_config_path) = workspace_config_path {
-            if workspace_config_path.exists() {
-                info!("loading workspace level bacon.toml");
-                let workspace_config = Config::from_path(&workspace_config_path)?;
-                settings.register_config_file(workspace_config_path);
-                settings.apply_config(&workspace_config);
-            }
-        }
-
-        if package_config_path.exists() {
-            let config = Config::from_path(&package_config_path)?;
-            settings.register_config_file(package_config_path);
-            settings.apply_config(&config);
-        }
-
-        if let Some(config_path) = config_path_from_env("BACON_CONFIG") {
-            let config = Config::from_path(&config_path)?;
-            info!("config from env: {:#?}", &config);
-            settings.register_config_file(config_path);
-            settings.apply_config(&config);
-        }
-
         settings.apply_args(args);
         settings.check()?;
         info!("settings: {:#?}", &settings);
diff --git a/src/context.rs b/src/context.rs
index 058193e..942d48c 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -230,14 +230,28 @@ impl Context {
             settings,
         })
     }
-    pub fn package_config_path(&self) -> PathBuf {
-        self.package_directory.join("bacon.toml")
+    pub fn workspace_cargo_path(&self) -> Option<PathBuf> {
+        self.workspace_root.as_ref().map(|p| p.join("Cargo.toml"))
     }
     /// return the location of the workspace level bacon.toml file
-    /// (it may be the same path than the package config)
+    /// (if it's different from the package level bacon.toml file)
     pub fn workspace_config_path(&self) -> Option<PathBuf> {
         self.workspace_root.as_ref().map(|p| p.join("bacon.toml"))
     }
+    pub fn workspace_dot_config_path(&self) -> Option<PathBuf> {
+        self.workspace_root
+            .as_ref()
+            .map(|p| p.join(".config/bacon.toml"))
+    }
+    pub fn package_cargo_path(&self) -> PathBuf {
+        self.package_directory.join("Cargo.toml")
+    }
+    pub fn package_config_path(&self) -> PathBuf {
+        self.package_directory.join("bacon.toml")
+    }
+    pub fn package_dot_config_path(&self) -> PathBuf {
+        self.package_directory.join(".config/bacon.toml")
+    }
 }
 
 fn cargo_manifest_not_found(err: &str) -> bool {
diff --git a/website/docs/config.md b/website/docs/config.md
index 7064e0f..accdbfd 100644
--- a/website/docs/config.md
+++ b/website/docs/config.md
@@ -10,12 +10,19 @@ Bacon loads in order:
 * its default internal configuration (which includes the default bacon.toml)
 * the global `prefs.toml` ([global preferences](#global-preferences))
 * the file whose path is in environment variable `BACON_PREFS`
+* the `workspace.metadata.bacon` entry in the workspace's `Cargo.toml`
 * the workspace level `bacon.toml` file ([project settings](#project-settings))
-* the package level `bacon.toml` file
+* the `bacon.toml` file in `workspace-root/.config/`
+* the `package.metadata.bacon` entry in the package's `Cargo.toml`
+* the `bacon.toml` file in `package-root/`
+* the `bacon.toml` file in `package-root/.config/`
 * the file whose path is in environment variable `BACON_CONFIG`
 
 Each configuration file overrides the properties of previously loaded ones.
 
+But you don't *need* so many files.
+It's usually enough to have a global `prefs.toml` file and a project specific `bacon.toml`.
+
 When you modified those files and bacon evolved since, you may want to have a look at the current default ones:
 
 * [Current default prefs.toml](https://raw.githubusercontent.com/Canop/bacon/main/defaults/default-prefs.toml)


Test patch: