diff --git a/crates/tracel-xtask/src/commands/publish.rs b/crates/tracel-xtask/src/commands/publish.rs index ab5c4b7..8ebe5f3 100644 --- a/crates/tracel-xtask/src/commands/publish.rs +++ b/crates/tracel-xtask/src/commands/publish.rs @@ -2,7 +2,7 @@ use std::{env, process::Command, str}; use anyhow::{anyhow, Ok}; -use crate::{endgroup, group, utils::process::run_process}; +use crate::{endgroup, group, utils::{cargo::parse_cargo_search_output, process::run_process}}; // Crates.io API token const CRATES_IO_API_TOKEN: &str = "CRATES_IO_API_TOKEN"; @@ -56,7 +56,8 @@ fn local_version(crate_name: &str) -> anyhow::Result { Ok(local_version.trim_end().to_string()) } -// Obtain remote crate version + +// Obtain the crate version from crates.io fn remote_version(crate_name: &str) -> anyhow::Result> { // Obtain remote crate version contained in cargo search data let cargo_search_output = Command::new("cargo") @@ -64,19 +65,19 @@ fn remote_version(crate_name: &str) -> anyhow::Result> { .output() .map_err(|e| anyhow!("Failed to execute cargo search: {}", e))?; // Cargo search returns an empty string in case of a crate not present on crates.io - if cargo_search_output.stdout.is_empty() { - Ok(None) - } else { + if !cargo_search_output.stdout.is_empty() { + let output_str = str::from_utf8(&cargo_search_output.stdout).unwrap(); // Convert cargo search output into a str - let remote_version_str = str::from_utf8(&cargo_search_output.stdout) - .expect("Failed to convert cargo search output into a str"); - - // Extract only the remote crate version from str - Ok(remote_version_str - .split_once('=') - .and_then(|(_, second)| second.trim_start().split_once(' ')) - .map(|(s, _)| s.trim_matches('"').to_string())) + // as cargo search does not support exact match only we need to make sure that the + // result returned by cargo search is indeed the crate that we are looking for and not + // a crate whose name contains the name of the crate we are looking for. + if let Some((name, version)) = parse_cargo_search_output(output_str) { + if name == crate_name { + return Ok(Some(version.to_string())); + } + } } + Ok(None) } fn publish(crate_name: String) -> anyhow::Result<()> { diff --git a/crates/tracel-xtask/src/utils/cargo.rs b/crates/tracel-xtask/src/utils/cargo.rs index 71aa0d8..c298212 100644 --- a/crates/tracel-xtask/src/utils/cargo.rs +++ b/crates/tracel-xtask/src/utils/cargo.rs @@ -1,6 +1,7 @@ use std::process::Command; use anyhow::Ok; +use regex::Regex; use crate::{endgroup, group, utils::process::run_process}; @@ -47,3 +48,34 @@ pub fn is_cargo_crate_installed(crate_name: &str) -> bool { let output_str = String::from_utf8_lossy(&output.stdout); output_str.lines().any(|line| line.contains(crate_name)) } + +pub fn parse_cargo_search_output(output: &str) -> Option<(&str, &str)> { + let re = Regex::new(r#"(?P[a-zA-Z0-9_-]+)\s*=\s*"(?P\d+\.\d+\.\d+)""#) + .expect("should compile regex"); + if let Some(captures) = re.captures(output) { + let name = captures.name("name"); + let version = captures.name("version"); + if name.is_some() && version.is_some() { + return Some((name.unwrap().as_str(), version.unwrap().as_str())); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case::valid_input("tracel-xtask-macros = \"1.0.1\"", Some(("tracel-xtask-macros", "1.0.1")))] + #[case::missing_version("tracel-xtask-macros =", None)] + #[case::invalid_format("tracel-xtask-macros: \"1.0.1\"", None)] + #[case::extra_whitespace(" tracel-xtask-macros = \"1.0.1\" ", Some(("tracel-xtask-macros", "1.0.1")))] + #[case::no_quotes("tracel-xtask-macros = 1.0.1", None)] + #[case::wrong_version_format("tracel-xtask-macros = \"1.0\"", None)] + fn test_parse_cargo_search_output(#[case] input: &str, #[case] expected: Option<(&str, &str)>) { + let result = parse_cargo_search_output(input); + assert_eq!(result, expected); + } +}