1use std::{
2 fmt::{self, Display, Formatter},
3 str::FromStr,
4 sync::OnceLock,
5};
6
7use regex::Regex;
8
9use super::{error::ParseDependencyError, version::VersionReq};
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub enum DependencyMode {
14 Required,
16
17 Optional { hidden: bool },
20
21 Independent,
24}
25
26impl Display for DependencyMode {
27 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
28 use DependencyMode::*;
29 match self {
30 Required => write!(f, ""),
31 Optional { hidden } => write!(f, "{}", if *hidden { "(?)" } else { "?" }),
32 Independent => write!(f, "~"),
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum Compatibility {
40 Compatible(DependencyMode, VersionReq),
42
43 Incompatible,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct Dependency {
50 pub name: String,
52
53 pub compatibility: Compatibility,
55}
56
57impl Dependency {
58 pub fn new<T: Into<String>>(name: T, compatibility: Compatibility) -> Self {
59 Self {
60 name: name.into(),
61 compatibility,
62 }
63 }
64
65 pub fn required<T: Into<String>>(name: T, version_req: VersionReq) -> Self {
76 Self::new(
77 name,
78 Compatibility::Compatible(DependencyMode::Required, version_req),
79 )
80 }
81
82 pub fn optional<T: Into<String>>(name: T, version_req: VersionReq, hidden: bool) -> Self {
94 Self::new(
95 name,
96 Compatibility::Compatible(DependencyMode::Optional { hidden }, version_req),
97 )
98 }
99
100 pub fn independent<T: Into<String>>(name: T, version_req: VersionReq) -> Self {
111 Self::new(
112 name,
113 Compatibility::Compatible(DependencyMode::Independent, version_req),
114 )
115 }
116
117 pub fn incompatible<T: Into<String>>(name: T) -> Self {
127 Self::new(name, Compatibility::Incompatible)
128 }
129
130 pub fn parse(s: &str) -> Result<Self, ParseDependencyError> {
140 s.parse()
141 }
142}
143
144impl FromStr for Dependency {
145 type Err = ParseDependencyError;
146
147 fn from_str(s: &str) -> Result<Self, Self::Err> {
148 static RE: OnceLock<Regex> = OnceLock::new();
149 let re = RE.get_or_init(|| {
150 Regex::new(
151 r"(?sx)
152 \A\s*
153 (?<mode>!|\?|\(\?\)|~)?\s*
154 (?<name>[a-zA-Z0-9\-_\ ]+?)\s*
155 (?<version_spec>
156 (?: < | <= | = | >= | > )\s*
157 \d+\.\d+\.\d+
158 )?\s*\z",
159 )
160 .unwrap()
161 });
162
163 let captures = re
164 .captures(s)
165 .ok_or(ParseDependencyError::RegexMismatch(s.to_string()))?;
166
167 let name = captures.name("name").unwrap().as_str().to_string();
168 let version_req = captures
169 .name("version_spec")
170 .map_or(VersionReq::Latest, |m| {
171 VersionReq::parse(m.as_str()).unwrap()
173 });
174
175 let compat = captures.name("mode").map_or(
176 Compatibility::Compatible(DependencyMode::Required, version_req),
177 |m| match m.as_str() {
178 "!" => Compatibility::Incompatible,
179 "?" => Compatibility::Compatible(
180 DependencyMode::Optional { hidden: false },
181 version_req,
182 ),
183 "(?)" => Compatibility::Compatible(
184 DependencyMode::Optional { hidden: true },
185 version_req,
186 ),
187 "~" => Compatibility::Compatible(DependencyMode::Independent, version_req),
188 _ => unreachable!(),
189 },
190 );
191
192 Ok(Self::new(name, compat))
193 }
194}
195
196impl Display for Dependency {
197 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
198 use Compatibility::*;
199 match &self.compatibility {
200 Compatible(m, r) => {
201 if *m != DependencyMode::Required {
202 write!(f, "{} ", m)?;
203 }
204
205 match r {
206 VersionReq::Latest => f.write_str(&self.name),
207 VersionReq::Spec(spec) => write!(f, "{} {}", self.name, spec),
208 }
209 }
210 Incompatible => write!(f, "! {}", self.name),
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn parse_required_versioned_dep() {
221 let s = "boblibrary >= 0.17.0";
222 let d: Dependency = s.parse().unwrap();
223 assert_eq!(
224 d,
225 Dependency::new(
226 "boblibrary".to_string(),
227 Compatibility::Compatible(
228 DependencyMode::Required,
229 VersionReq::parse(">= 0.17.0").unwrap()
230 )
231 )
232 );
233 }
234
235 #[test]
236 fn parse_required_unversioned_dep() {
237 let s = "boblibrary";
238 let d: Dependency = s.parse().unwrap();
239 assert_eq!(
240 d,
241 Dependency::new(
242 "boblibrary".to_string(),
243 Compatibility::Compatible(DependencyMode::Required, VersionReq::Latest)
244 )
245 );
246 }
247
248 #[test]
249 fn parse_optional_versioned_dep() {
250 let s = "? boblibrary >= 0.17.0";
251 let d: Dependency = s.parse().unwrap();
252 assert_eq!(
253 d,
254 Dependency::new(
255 "boblibrary".to_string(),
256 Compatibility::Compatible(
257 DependencyMode::Optional { hidden: false },
258 VersionReq::parse(">= 0.17.0").unwrap()
259 )
260 )
261 );
262 }
263
264 #[test]
265 fn parse_optional_unversioned_dep() {
266 let s = "? boblibrary";
267 let d: Dependency = s.parse().unwrap();
268 assert_eq!(
269 d,
270 Dependency::new(
271 "boblibrary".to_string(),
272 Compatibility::Compatible(
273 DependencyMode::Optional { hidden: false },
274 VersionReq::Latest
275 )
276 )
277 );
278 }
279
280 #[test]
281 fn parse_hidden_optional_versioned_dep() {
282 let s = "(?) boblibrary >= 0.17.0";
283 let d: Dependency = s.parse().unwrap();
284 assert_eq!(
285 d,
286 Dependency::new(
287 "boblibrary".to_string(),
288 Compatibility::Compatible(
289 DependencyMode::Optional { hidden: true },
290 VersionReq::parse(">= 0.17.0").unwrap()
291 )
292 )
293 );
294 }
295
296 #[test]
297 fn parse_hidden_optional_unversioned_dep() {
298 let s = "(?) boblibrary";
299 let d: Dependency = s.parse().unwrap();
300 assert_eq!(
301 d,
302 Dependency::new(
303 "boblibrary".to_string(),
304 Compatibility::Compatible(
305 DependencyMode::Optional { hidden: true },
306 VersionReq::Latest
307 )
308 )
309 );
310 }
311
312 #[test]
313 fn parse_incompatible_dep() {
314 let s = "! boblibrary";
315 let d: Dependency = s.parse().unwrap();
316 assert_eq!(
317 d,
318 Dependency::new("boblibrary".to_string(), Compatibility::Incompatible)
319 );
320 }
321
322 #[test]
323 fn parse_independent_versioned_dep() {
324 let s = "~ boblibrary >= 0.17.0";
325 let d: Dependency = s.parse().unwrap();
326 assert_eq!(
327 d,
328 Dependency::new(
329 "boblibrary".to_string(),
330 Compatibility::Compatible(
331 DependencyMode::Independent,
332 VersionReq::parse(">= 0.17.0").unwrap()
333 )
334 )
335 );
336 }
337
338 #[test]
339 fn parse_independent_unversioned_dep() {
340 let s = "~ boblibrary";
341 let d: Dependency = s.parse().unwrap();
342 assert_eq!(
343 d,
344 Dependency::new(
345 "boblibrary".to_string(),
346 Compatibility::Compatible(DependencyMode::Independent, VersionReq::Latest)
347 )
348 );
349 }
350}