mise 2026.4.11

The front-end to your dev env
use std::path::{Path, PathBuf};

use eyre::Result;

use crate::backend::BackendList;
use crate::cli::args::BackendArg;
use crate::config::config_file::ConfigFile;
use crate::toolset::{ToolRequest, ToolRequestSet, ToolSource};

use super::ConfigFileType;

pub mod package_json;

#[derive(Debug, Clone)]
pub struct IdiomaticVersionFile {
    path: PathBuf,
    tools: ToolRequestSet,
}

impl IdiomaticVersionFile {
    #[allow(dead_code)]
    #[cfg(test)]
    pub fn init(path: PathBuf) -> Self {
        Self {
            path,
            tools: ToolRequestSet::new(),
        }
    }

    pub async fn parse(path: PathBuf, plugins: BackendList) -> Result<Self> {
        let source = ToolSource::IdiomaticVersionFile(path.clone());
        let mut tools = ToolRequestSet::new();

        for plugin in plugins {
            match plugin.parse_idiomatic_file(&path).await {
                Ok(versions) => {
                    for v in versions {
                        let tr = ToolRequest::new(plugin.ba().clone(), &v, source.clone())?;
                        tools.add_version(tr, &source);
                    }
                }
                Err(e) => {
                    trace!("skipping {} for {}: {}", path.display(), plugin.id(), e);
                    continue;
                }
            }
        }

        Ok(Self { tools, path })
    }
}

impl ConfigFile for IdiomaticVersionFile {
    fn config_type(&self) -> ConfigFileType {
        ConfigFileType::IdiomaticVersion(vec![])
    }

    fn get_path(&self) -> &Path {
        self.path.as_path()
    }

    #[cfg_attr(coverage_nightly, coverage(off))]
    fn remove_tool(&self, _fa: &BackendArg) -> Result<()> {
        unimplemented!()
    }

    #[cfg_attr(coverage_nightly, coverage(off))]
    fn replace_versions(
        &self,
        _plugin_name: &BackendArg,
        _versions: Vec<ToolRequest>,
    ) -> Result<()> {
        unimplemented!()
    }

    #[cfg_attr(coverage_nightly, coverage(off))]
    fn save(&self) -> Result<()> {
        unimplemented!()
    }

    #[cfg_attr(coverage_nightly, coverage(off))]
    fn dump(&self) -> Result<String> {
        unimplemented!()
    }

    fn source(&self) -> ToolSource {
        ToolSource::IdiomaticVersionFile(self.path.clone())
    }

    fn to_tool_request_set(&self) -> Result<ToolRequestSet> {
        Ok(self.tools.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::backend::{Backend, VersionInfo};
    use crate::cli::args::{BackendArg, BackendResolution};
    use crate::config::Config;
    use crate::install_context::InstallContext;
    use crate::toolset::ToolVersion;
    use async_trait::async_trait;
    use std::sync::Arc;

    #[derive(Debug)]
    struct MockBackend {
        ba: Arc<BackendArg>,
        fail: bool,
        version: Option<String>,
    }

    impl MockBackend {
        fn new(short: &str, fail: bool, version: Option<String>) -> Self {
            let ba = BackendArg::new_raw(
                short.to_string(),
                None,
                short.to_string(),
                None,
                BackendResolution::new(false),
            );
            Self {
                ba: Arc::new(ba),
                fail,
                version,
            }
        }
    }

    #[async_trait]
    impl Backend for MockBackend {
        fn ba(&self) -> &Arc<BackendArg> {
            &self.ba
        }

        async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<VersionInfo>> {
            Ok(vec![])
        }

        async fn install_version_(
            &self,
            _ctx: &InstallContext,
            _tv: ToolVersion,
        ) -> Result<ToolVersion> {
            unimplemented!()
        }

        async fn parse_idiomatic_file(&self, _path: &Path) -> Result<Vec<String>> {
            if self.fail {
                eyre::bail!("mock error");
            }
            if let Some(v) = &self.version {
                Ok(vec![v.clone()])
            } else {
                Ok(vec![])
            }
        }
    }

    #[tokio::test]
    async fn test_idiomatic_parse_error_propagation() {
        let _config = Config::get().await.unwrap();
        let path = PathBuf::from(".tool-versions");
        let backend1 = Arc::new(MockBackend::new("node", true, None));
        let backend2 = Arc::new(MockBackend::new(
            "python",
            false,
            Some("3.10.0".to_string()),
        ));
        let plugins: BackendList = vec![backend1, backend2];

        let result = IdiomaticVersionFile::parse(path, plugins).await;

        assert!(result.is_ok(), "Should not propagate error from backend1");

        let file = result.unwrap();
        let trs = file.to_tool_request_set().unwrap();
        let tools: Vec<_> = trs.into_iter().collect();
        assert_eq!(tools.len(), 1);
        let (ba, versions, _) = &tools[0];
        assert_eq!(ba.short, "python");
        assert_eq!(versions[0].version(), "3.10.0");
    }
}