use crate::provider::prelude::*;
use regex::Regex;
#[derive(Default, Debug, PartialEq)]
pub struct Cargo;
impl fmt::Display for Cargo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cargo")
}
}
impl Cargo {
pub fn new() -> Self {
Default::default()
}
pub(crate) fn get_candidates_from_search_output(
&self,
output: &str,
) -> ProviderResult<Vec<Candidate>> {
let err_context = "failed to parse output from cargo";
log::trace!("parsing search output: \n'{}'", output);
let lines = output
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let mut results = vec![];
let cargo_regex = Regex::new(
"^(?P<package>[a-zA-Z0-9-_]+) = \"(?P<version>.*)\"\\s+# (?P<description>.+)$",
)
.context(err_context)?;
for line in lines {
if line.is_empty() {
continue;
}
match cargo_regex.captures(&line) {
Some(caps) => {
let mut candidate = Candidate {
package: match_to_string(&caps, 1).context(err_context)?,
version: match_to_string(&caps, 2).context(err_context)?,
description: match_to_string(&caps, 3).context(err_context)?,
origin: "".to_string(),
..Candidate::default()
};
candidate.actions.install = Some(cmd!(
"cargo".into(),
"install".into(),
"--version".into(),
candidate.version.clone(),
candidate.package.clone()
));
results.push(candidate);
}
None => {
log::trace!("regex didn't match on line '{}'", line);
continue;
}
}
}
Ok(results)
}
}
fn match_to_string(capture: ®ex::Captures, index: usize) -> ProviderResult<String> {
Ok(capture
.get(index)
.with_context(|| format!("failed to retrieve regex capture group {}", index))?
.as_str()
.to_string())
}
#[async_trait]
impl IsProvider for Cargo {
async fn search_internal(
&self,
command: &str,
target_env: Arc<Environment>,
) -> ProviderResult<Vec<Candidate>> {
let stdout = target_env
.output_of(cmd!(
"cargo", "search", "--limit", "5", "--color", "never", command
))
.await?;
let mut candidates = self.get_candidates_from_search_output(&stdout)?;
for c in candidates.iter_mut() {
c.actions.execute = cmd!(command.to_string());
}
Ok(candidates)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
#[test]
fn initialize() {
let _cargo = Cargo::new();
}
test::default_tests!(Cargo::new());
#[test]
fn matches_empty() {
let query = quick_test!(Cargo::new(), Ok("".to_string()));
assert::is_err!(query);
assert::err::not_found!(query);
}
#[test]
fn matches_zellij() {
let query = quick_test!(Cargo::new(), Ok("
zellij = \"0.36.0\" # A terminal workspace with batteries included
zellij-runner = \"0.2.0\" # Session runner/switcher for Zellij
zellij-client = \"0.36.0\" # The client-side library for Zellij
zellij-server = \"0.36.0\" # The server-side library for Zellij
zellij-tile = \"0.36.0\" # A small client-side library for writing Zellij plugins
... and 11 crates more (use --limit N to see more)
".to_string()));
let result = query.results.unwrap();
assert!(result.len() == 5);
assert!(result[0].package == "zellij");
assert!(result[0].version == "0.36.0");
assert!(result[0].origin.is_empty());
assert!(result[0].description == "A terminal workspace with batteries included");
assert!(result[1].description == "Session runner/switcher for Zellij");
}
#[test]
fn no_network() {
let query = quick_test!(Cargo::new(), Err(ExecutionError::NonZero {
command: "cargo".to_string(),
output: std::process::Output {
stdout: r"".into(),
stderr: r"error: failed to retrieve search results from the registry at https://crates.io
Caused by:
[6] Couldn't resolve host name (Could not resolve host: crates.io)
".into(),
status: ExitStatus::from_raw(101),
},
}));
assert::is_err!(query);
assert::err::execution!(query);
}
}