cnf_lib/provider/
cargo.rs1use crate::provider::prelude::*;
7
8use regex::Regex;
9
10#[derive(Default, Debug, PartialEq)]
12#[allow(missing_copy_implementations)]
14pub struct Cargo;
15
16impl fmt::Display for Cargo {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 write!(f, "cargo")
19 }
20}
21
22impl Cargo {
23 pub fn new() -> Self {
25 Default::default()
26 }
27
28 fn get_candidates_from_search_output(&self, output: &str) -> ProviderResult<Vec<Candidate>> {
30 let err_context = "failed to parse output from cargo";
31 trace!(output, "parsing search output");
32
33 let lines = output
34 .lines()
35 .map(|s| s.to_string())
36 .collect::<Vec<String>>();
37
38 let mut results = vec![];
39
40 let cargo_regex = Regex::new(
41 "^(?P<package>[a-zA-Z0-9-_]+) = \"(?P<version>.*)\"\\s+# (?P<description>.+)$",
42 )
43 .context(err_context)?;
44
45 for line in lines {
46 if line.is_empty() {
47 continue;
48 }
49 match cargo_regex.captures(&line) {
50 Some(caps) => {
51 let mut candidate = Candidate {
52 package: match_to_string(&caps, 1).context(err_context)?,
53 version: match_to_string(&caps, 2).context(err_context)?,
54 description: match_to_string(&caps, 3).context(err_context)?,
55 origin: "".to_string(),
56 ..Candidate::default()
57 };
58 candidate.actions.install = Some(cmd!(
59 "cargo".into(),
60 "install".into(),
61 "--version".into(),
62 candidate.version.clone(),
63 candidate.package.clone()
64 ));
65
66 results.push(candidate);
67 }
68 None => {
69 trace!("regex didn't match on line '{}'", line);
70 continue;
71 }
72 }
73 }
74
75 Ok(results)
76 }
77}
78
79fn match_to_string<'r>(capture: &'r regex::Captures<'r>, index: usize) -> ProviderResult<String> {
80 Ok(capture
81 .get(index)
82 .with_context(|| format!("failed to retrieve regex capture group {}", index))?
83 .as_str()
84 .to_string())
85}
86
87#[async_trait]
88impl IsProvider for Cargo {
89 async fn search_internal(
90 &self,
91 command: &str,
92 target_env: Arc<Environment>,
93 ) -> ProviderResult<Vec<Candidate>> {
94 let stdout = target_env
95 .output_of(cmd!(
96 "cargo", "search", "--limit", "5", "--color", "never", command
97 ))
98 .await?;
99
100 let mut candidates = self.get_candidates_from_search_output(&stdout)?;
101 for c in candidates.iter_mut() {
103 c.actions.execute = cmd!(command.to_string());
104 }
105
106 Ok(candidates)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::test::prelude::*;
114
115 #[test]
116 fn initialize() {
117 let _cargo = Cargo::new();
118 }
119
120 test::default_tests!(Cargo::new());
121
122 #[test]
127 fn matches_empty() {
128 let query = quick_test!(Cargo::new(), Ok("".to_string()));
129
130 assert::is_err!(query);
131 assert::err::not_found!(query);
132 }
133
134 #[test]
139 fn matches_zellij() {
140 let query = quick_test!(Cargo::new(), Ok("
141zellij = \"0.36.0\" # A terminal workspace with batteries included
142zellij-runner = \"0.2.0\" # Session runner/switcher for Zellij
143zellij-client = \"0.36.0\" # The client-side library for Zellij
144zellij-server = \"0.36.0\" # The server-side library for Zellij
145zellij-tile = \"0.36.0\" # A small client-side library for writing Zellij plugins
146... and 11 crates more (use --limit N to see more)
147".to_string()));
148
149 let result = query.results.unwrap();
150
151 assert!(result.len() == 5);
152 assert!(result[0].package == "zellij");
153 assert!(result[0].version == "0.36.0");
154 assert!(result[0].origin.is_empty());
155 assert!(result[0].description == "A terminal workspace with batteries included");
156 assert!(result[1].description == "Session runner/switcher for Zellij");
157 }
158
159 #[test]
164 fn no_network() {
165 let query = quick_test!(Cargo::new(), Err(ExecutionError::NonZero {
166 command: "cargo".to_string(),
167 output: std::process::Output {
168 stdout: r"".into(),
169 stderr: r"error: failed to retrieve search results from the registry at https://crates.io
170
171Caused by:
172 [6] Couldn't resolve host name (Could not resolve host: crates.io)
173".into(),
174 status: ExitStatus::from_raw(101),
175 },
176 }));
177
178 assert::is_err!(query);
179 assert::err::execution!(query);
180 }
181}