diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0da09c8..339cd0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - rust_version: [stable, "1.53.0"] + rust_version: [stable, "1.58.0"] steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6704ae0..ba77e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Foreman Changelog +## Unreleased +- Errors when duplicate tools are defined in `foreman.toml` ([#65](https://github.com/Roblox/foreman/pull/65)) + ## 1.0.6 (2022-09-28) - Support `macos-aarch64` as an artifact name for Apple Silicon (arm64) binaries ([#60](https://github.com/Roblox/foreman/pull/60)) diff --git a/src/config.rs b/src/config.rs index e9cdf15..1cb71bf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,14 @@ -use std::{collections::BTreeMap, env, fmt}; +use std::{ + collections::BTreeMap, + env, fmt, + ops::{Deref, DerefMut}, +}; use semver::VersionReq; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{self, MapAccess, Visitor}, + Deserialize, Serialize, +}; use crate::{ ci_string::CiString, error::ForemanError, fs, paths::ForemanPaths, tool_provider::Provider, @@ -67,20 +74,43 @@ impl fmt::Display for ToolSpec { } } +#[derive(Debug, Serialize)] +pub struct ConfigFileTools(BTreeMap); + +impl ConfigFileTools { + pub fn new() -> ConfigFileTools { + Self(BTreeMap::new()) + } +} + +impl Deref for ConfigFileTools { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ConfigFileTools { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct ConfigFile { - pub tools: BTreeMap, + pub tools: ConfigFileTools, } impl ConfigFile { pub fn new() -> Self { Self { - tools: BTreeMap::new(), + tools: ConfigFileTools::new(), } } fn fill_from(&mut self, other: ConfigFile) { - for (tool_name, tool_source) in other.tools { + for (tool_name, tool_source) in other.tools.0 { self.tools.entry(tool_name).or_insert(tool_source); } } @@ -141,6 +171,48 @@ impl fmt::Display for ConfigFile { } } +struct ConfigFileVisitor; + +impl<'de> Visitor<'de> for ConfigFileVisitor { + type Value = ConfigFileTools; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with non-duplicate keys") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut tools = BTreeMap::new(); + + while let Some((key, value)) = map.next_entry()? { + if tools.contains_key(&key) { + // item already existed inside the config + // throw an error as this is unlikely to be the users intention + return Err(de::Error::custom(format!( + "duplicate tool name `{key}` found" + ))); + } + + tools.insert(key, value); + } + + Ok(ConfigFileTools(tools)) + } +} + +impl<'de> Deserialize<'de> for ConfigFileTools { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let tools = deserializer.deserialize_map(ConfigFileVisitor)?; + + Ok(tools) + } +} + #[cfg(test)] mod test { use super::*; @@ -189,6 +261,44 @@ mod test { .unwrap(); assert_eq!(gitlab, new_gitlab("user/repo", version("0.1.0"))); } + + #[test] + fn duplicate_tools() { + let err = toml::from_str::( + r#"tool = { github = "user/repo", version = "0.1.0" } + tool = { github = "user2/repo2", version = "0.2.0" }"#, + ) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "duplicate tool name `tool` found at line 1 column 1" + ); + + let err = toml::from_str::( + r#"tool_a = { github = "user/a", version = "0.1.0" } + tool_b = { github = "user/b", version = "0.2.0" } + tool_a = { gitlab = "user/c", version = "0.3.0" }"#, + ) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "duplicate tool name `tool_a` found at line 1 column 1" + ); + + let err = toml::from_str::( + r#"tool_b = { github = "user/b", version = "0.1.0" } + tool_a = { github = "user/a", version = "0.2.0" } + tool_a = { gitlab = "user/c", version = "0.3.0" }"#, + ) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "duplicate tool name `tool_a` found at line 1 column 1" + ); + } } #[test]