Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement anti-duplicate parsing for config.tools #65

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use std::{collections::HashMap, env, fmt};
use std::{
collections::HashMap,
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,
Expand Down Expand Up @@ -67,20 +74,43 @@ impl fmt::Display for ToolSpec {
}
}

#[derive(Debug, Serialize)]
pub struct ConfigFileTools(HashMap<String, ToolSpec>);

impl ConfigFileTools {
pub fn new() -> ConfigFileTools {
Self(HashMap::new())
}
}

impl Deref for ConfigFileTools {
type Target = HashMap<String, ToolSpec>;

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: HashMap<String, ToolSpec>,
pub tools: ConfigFileTools,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want, I think a neat tiny little improvement would be to make this field private and add a public method like pub fn tools(&self) -> impl Iterator<Item = &(String, ToolSpec)>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since some external files (like src/main.rs) want to perform methods like tools.get, I think it would probably be best to just do

pub fn tools(&self) -> &HashMap<String, ToolSpec> {
    &self.tools.0
}

which would leave us to have to create a tools_mut as well (as ConfigFile::fill_from needs it), or we could leave it the current way it is (implement Deref and DerefMut).

}

impl ConfigFile {
pub fn new() -> Self {
Self {
tools: HashMap::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);
}
}
Expand Down Expand Up @@ -141,6 +171,46 @@ 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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut tools = HashMap::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 `{key}`")));
OverHash marked this conversation as resolved.
Show resolved Hide resolved
}

tools.insert(key, value);
}

Ok(ConfigFileTools(tools))
}
}

impl<'de> Deserialize<'de> for ConfigFileTools {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let tools = deserializer.deserialize_map(ConfigFileVisitor)?;

Ok(tools)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -189,6 +259,17 @@ mod test {
.unwrap();
assert_eq!(gitlab, new_gitlab("user/repo", version("0.1.0")));
}

#[test]
fn duplicate_tools() {
let err = toml::from_str::<ConfigFileTools>(
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 `tool` at line 1 column 1");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding a test case to document the issue you're having with the line/column number being always 1?

I would like to see something like these cases:

tool_a = { github = "user/a", version = "0.1.0" }
tool_b = { github = "user/b", version = "0.1.0" }
tool_a = { github = "user/c", version = "0.1.0" }
tool_b = { github = "user/b", version = "0.1.0" }
tool_a = { github = "user/a", version = "0.1.0" }
tool_a = { github = "user/c", version = "0.1.0" }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added these test cases now.

❯ cat foreman.toml


[tools]
# oops!
selene = { source = "rojo-rbx/rojo", version = "=7.1.1" }

selene = { source = "Kampfkarren/selene", version = "=0.19.1" }
❯ selene --version
unable to parse Foreman configuration file (at ~/project/foreman.toml): duplicate tool name `selene` found for key `tools` at line 3 column 1

A Foreman configuration file looks like this:

[tools] # list the tools you want to install under this header

# each tool is on its own line, the tool name is on the left
# side of `=` and the right side tells Foreman where to find
# it and which version to download
tool_name = { github = "user/repository-name", version = "1.0.0" }

# tools hosted on gitlab follows the same structure, except
# `github` is replaced with `gitlab`

# Examples:
stylua = { github = "JohnnyMorganz/StyLua", version = "0.11.3" }
darklua = { gitlab = "seaofvoices/darklua", version = "0.7.0" }

So I think it's not really an issue, it's just that the line serde points to is where [tools] is in the toml, rather than where the duplicate tool is. Unsure if that is a big enough issue to warrant looking in to.

}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ fn actual_main(paths: ForemanPaths) -> ForemanResult<()> {

let mut cache = ToolCache::load(&paths)?;

for (tool_alias, tool_spec) in &config.tools {
for (tool_alias, tool_spec) in config.tools.iter() {
let providers = ToolProvider::new(&paths);
cache.download_if_necessary(tool_spec, &providers)?;
add_self_alias(tool_alias, &paths.bin_dir())?;
Expand Down