alpm_types/relation/base.rs
1//! Basic relation types used in metadata files.
2
3use std::{
4 fmt::{Display, Formatter},
5 str::FromStr,
6};
7
8use alpm_parsers::traits::{AlpmParser, ParserUntil, ParserUntilInclusive};
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11use winnow::{
12 ModalResult,
13 Parser,
14 ascii::space1,
15 combinator::{opt, peek, seq, terminated},
16 error::{StrContext, StrContextValue},
17 token::{none_of, take_till},
18};
19
20use crate::{
21 Epoch,
22 Error,
23 Name,
24 PackageRelease,
25 PackageVersion,
26 Version,
27 VersionComparison,
28 VersionRequirement,
29};
30
31/// A package relation
32///
33/// Describes a relation to a component.
34/// Package relations may either consist of only a [`Name`] *or* of a [`Name`] and a
35/// [`VersionRequirement`].
36///
37/// ## Note
38///
39/// A [`PackageRelation`] covers all [alpm-package-relations] *except* optional
40/// dependencies, as those behave differently.
41///
42/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
43#[derive(Clone, Debug, Eq, PartialEq)]
44#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
45pub struct PackageRelation {
46 /// The name of the package
47 pub name: Name,
48 /// The version requirement for the package
49 pub version_requirement: Option<VersionRequirement>,
50}
51
52impl PackageRelation {
53 /// Creates a new [`PackageRelation`]
54 ///
55 /// # Examples
56 ///
57 /// ```
58 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
59 ///
60 /// # fn main() -> Result<(), alpm_types::Error> {
61 /// PackageRelation::new(
62 /// "example".parse()?,
63 /// Some(VersionRequirement {
64 /// comparison: VersionComparison::Less,
65 /// version: "1.0.0".parse()?,
66 /// }),
67 /// );
68 ///
69 /// PackageRelation::new("example".parse()?, None);
70 /// # Ok(())
71 /// # }
72 /// ```
73 pub fn new(name: Name, version_requirement: Option<VersionRequirement>) -> Self {
74 Self {
75 name,
76 version_requirement,
77 }
78 }
79}
80
81impl AlpmParser for PackageRelation {
82 /// Recognizes a [`PackageRelation`] in a string slice.
83 ///
84 /// # Examples
85 ///
86 /// See [`Self::from_str`] for code examples.
87 ///
88 /// # Errors
89 ///
90 /// Returns an error if `input` does not begin with a valid [alpm-package-relation].
91 ///
92 /// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
93 fn parser(input: &mut &str) -> ModalResult<Self> {
94 seq!(Self {
95 name: Name::parser.context(StrContext::Label("package name")),
96 version_requirement: opt(VersionRequirement::parser),
97 })
98 .parse_next(input)
99 }
100
101 fn delimiter_error_context<'a, O, P>(
102 parser: P,
103 ) -> impl Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>
104 where
105 P: Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>,
106 {
107 parser
108 .context(StrContext::Label("alpm-package-relation"))
109 .context(StrContext::Expected(StrContextValue::Description(
110 "end of input after version requirement",
111 )))
112 }
113}
114
115impl Display for PackageRelation {
116 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
117 if let Some(version_requirement) = self.version_requirement.as_ref() {
118 write!(f, "{}{}", self.name, version_requirement)
119 } else {
120 write!(f, "{}", self.name)
121 }
122 }
123}
124
125impl FromStr for PackageRelation {
126 type Err = Error;
127 /// Parses a [`PackageRelation`] from a string slice.
128 ///
129 /// Delegates to [`PackageRelation::parser`].
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if [`PackageRelation::parser`] fails.
134 ///
135 /// # Examples
136 ///
137 /// ```
138 /// use std::str::FromStr;
139 ///
140 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
141 ///
142 /// # fn main() -> Result<(), alpm_types::Error> {
143 /// assert_eq!(
144 /// PackageRelation::from_str("example<1.0.0")?,
145 /// PackageRelation::new(
146 /// "example".parse()?,
147 /// Some(VersionRequirement {
148 /// comparison: VersionComparison::Less,
149 /// version: "1.0.0".parse()?
150 /// })
151 /// ),
152 /// );
153 ///
154 /// assert_eq!(
155 /// PackageRelation::from_str("example<=1.0.0")?,
156 /// PackageRelation::new(
157 /// "example".parse()?,
158 /// Some(VersionRequirement {
159 /// comparison: VersionComparison::LessOrEqual,
160 /// version: "1.0.0".parse()?
161 /// })
162 /// ),
163 /// );
164 ///
165 /// assert_eq!(
166 /// PackageRelation::from_str("example=1.0.0")?,
167 /// PackageRelation::new(
168 /// "example".parse()?,
169 /// Some(VersionRequirement {
170 /// comparison: VersionComparison::Equal,
171 /// version: "1.0.0".parse()?
172 /// })
173 /// ),
174 /// );
175 ///
176 /// assert_eq!(
177 /// PackageRelation::from_str("example>1.0.0")?,
178 /// PackageRelation::new(
179 /// "example".parse()?,
180 /// Some(VersionRequirement {
181 /// comparison: VersionComparison::Greater,
182 /// version: "1.0.0".parse()?
183 /// })
184 /// ),
185 /// );
186 ///
187 /// assert_eq!(
188 /// PackageRelation::from_str("example>=1.0.0")?,
189 /// PackageRelation::new(
190 /// "example".parse()?,
191 /// Some(VersionRequirement {
192 /// comparison: VersionComparison::GreaterOrEqual,
193 /// version: "1.0.0".parse()?
194 /// })
195 /// ),
196 /// );
197 ///
198 /// assert_eq!(
199 /// PackageRelation::from_str("example")?,
200 /// PackageRelation::new("example".parse()?, None),
201 /// );
202 ///
203 /// assert!(PackageRelation::from_str("example<").is_err());
204 /// # Ok(())
205 /// # }
206 /// ```
207 fn from_str(s: &str) -> Result<Self, Self::Err> {
208 Ok(Self::parser.parse(s)?)
209 }
210}
211
212/// An optional dependency for a package.
213///
214/// This type is used for representing dependencies that are not essential for base functionality
215/// of a package, but may be necessary to make use of certain features of a package.
216///
217/// An [`OptionalDependency`] consists of a package relation and an optional description separated
218/// by a colon (`:`).
219///
220/// - The package relation component must be a valid [`PackageRelation`].
221/// - If a description is provided it must be at least one character long.
222///
223/// Refer to [alpm-package-relation] of type [optional dependency] for details on the format.
224/// ## Examples
225///
226/// ```
227/// use std::str::FromStr;
228///
229/// use alpm_types::{Name, OptionalDependency};
230///
231/// # fn main() -> Result<(), alpm_types::Error> {
232/// // Create OptionalDependency from &str
233/// let opt_depend = OptionalDependency::from_str("example: this is an example dependency")?;
234///
235/// // Get the name
236/// assert_eq!("example", opt_depend.name().as_ref());
237///
238/// // Get the description
239/// assert_eq!(
240/// Some("this is an example dependency"),
241/// opt_depend.description().as_deref()
242/// );
243///
244/// // Format as String
245/// assert_eq!(
246/// "example: this is an example dependency",
247/// format!("{opt_depend}")
248/// );
249/// # Ok(())
250/// # }
251/// ```
252///
253/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
254/// [optional dependency]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html#optional-dependency
255#[derive(Clone, Debug, Eq, PartialEq)]
256#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
257pub struct OptionalDependency {
258 package_relation: PackageRelation,
259 description: Option<String>,
260}
261
262impl OptionalDependency {
263 /// Create a new OptionalDependency in a Result
264 pub fn new(
265 package_relation: PackageRelation,
266 description: Option<String>,
267 ) -> OptionalDependency {
268 OptionalDependency {
269 package_relation,
270 description,
271 }
272 }
273
274 /// Return the name of the optional dependency
275 pub fn name(&self) -> &Name {
276 &self.package_relation.name
277 }
278
279 /// Return the version requirement of the optional dependency
280 pub fn version_requirement(&self) -> &Option<VersionRequirement> {
281 &self.package_relation.version_requirement
282 }
283
284 /// Return the description for the optional dependency, if it exists
285 pub fn description(&self) -> &Option<String> {
286 &self.description
287 }
288
289 /// Returns a reference to the tracked [`PackageRelation`].
290 pub fn package_relation(&self) -> &PackageRelation {
291 &self.package_relation
292 }
293}
294
295impl AlpmParser for OptionalDependency {
296 /// Recognizes an [`OptionalDependency`] in a string slice.
297 ///
298 /// This format is inherently flawed, as the `:` delimiter may exist in two different, optional
299 /// places.
300 /// 1. **After** the optional epoch
301 /// 2. **Before** the optional description
302 ///
303 /// The `:` delimiter may also appear **inside** the description, although that isn't an issue
304 /// during parsing.
305 ///
306 /// ```text
307 /// why>=1:17.0.1-5: my dependency
308 /// is>=1:17.0.1-5
309 /// it>=17.0.1-5: my other dependency :::::
310 /// this: 1:17.0.1-5 my other dependency
311 /// way>1: 17.0.1-5 ambiguous.
312 /// ```
313 ///
314 /// Due to this, the parser disambiguates the two cases as follows:
315 ///
316 /// - A `:` directly followed by a non-whitespace character is considered an epoch delimiter.
317 /// - A `:` followed by whitespace starts a description.
318 ///
319 /// As such, ambiguous input like `example>=1:foo bar` is treated as containing an epoch and
320 /// rejected, as `foo bar` is not a valid version.
321 /// Input like `example>=1: 3.2.1-5 foo bar` is successfully parsed with the version being `1`
322 /// and the description being `3.2.1-5 foo bar`.
323 ///
324 /// # Errors
325 ///
326 /// Returns an error if `input` is not a valid _alpm-package-relation_ of type _optional
327 /// dependency_.
328 fn parser(input: &mut &str) -> ModalResult<Self> {
329 // Due to the ambiguous nature of this format, we must implement our own PackageRelation and
330 // VersionRequirement parser handling.
331
332 // Handle the dependency name:
333 // `example>=1.0.0: my-description` -> `>=1.0.0: my-description`
334 let name = Name::parser
335 .context(StrContext::Label("package name"))
336 .parse_next(input)?;
337
338 // Handle the optional Comparison operator:
339 // `example>=1.0.0: my-description` -> `1.0.0: my-description`
340 let comparison = opt(VersionComparison::parser).parse_next(input)?;
341
342 // Branch into the path where a comparison exists.
343 let version_requirement = if let Some(comparison) = comparison {
344 // Parse an optional epoch, e.g.:
345 // "1:17.0.1-5: my-description" -> "17.0.1-5: my-description"
346 //
347 // An epoch delimiter ':' must always be directly followed by a non-whitespace
348 // character, while a description ':' delimiter is always followed by whitespace.
349 // The lookahead on the character after the ':' disambiguates the two.
350 let epoch = opt(terminated(
351 Epoch::parser_until_inclusive(":"),
352 peek(none_of(|c: char| c.is_whitespace())),
353 ))
354 .parse_next(input)?;
355
356 // Advance the parser until the next '-', e.g.:
357 // "17.0.1-5: my-description" -> "-5: my-description"
358 let pkgver = PackageVersion::parser.parse_next(input)?;
359
360 // Parse an optional PackageRelease, e.g.:
361 // "-5: my-description" -> ": my-description"
362 //
363 // If an `-` is found, the PackageRelease is expected and must exist
364 let delimiter = opt('-').parse_next(input)?;
365 let pkgrel = if delimiter.is_some() {
366 Some(PackageRelease::parser.parse_next(input)?)
367 } else {
368 None
369 };
370
371 Some(VersionRequirement {
372 comparison,
373 version: Version::new(pkgver, epoch, pkgrel),
374 })
375 } else {
376 None
377 };
378
379 let package_relation = PackageRelation::new(name, version_requirement);
380
381 // Check if there's a `:`, which indicates the existence of an description.
382 let delimiter = opt(":").parse_next(input)?;
383 if delimiter.is_some() {
384 space1
385 .context(StrContext::Label(
386 "dependency delimiter in optional dependency",
387 ))
388 .context(StrContext::Expected(StrContextValue::Description(
389 "A colon followed by a whitespace ': '",
390 )))
391 .parse_next(input)?;
392 }
393
394 let description = if delimiter.is_some() {
395 // Descriptions are at the end of a `OptionalDependency` and may contain any character,
396 // except '\n' or '\r'. So this parser consumes everything till newline or `eof`.
397 let description = take_till(0.., ('\n', '\r'))
398 .context(StrContext::Label("optional dependency description"))
399 .parse_next(input)?
400 .trim_ascii();
401
402 if description.is_empty() {
403 None
404 } else {
405 Some(description.to_string())
406 }
407 } else {
408 None
409 };
410
411 Ok(Self {
412 package_relation,
413 description,
414 })
415 }
416
417 fn delimiter_error_context<'a, O, P>(
418 parser: P,
419 ) -> impl Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>
420 where
421 P: Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>,
422 {
423 parser
424 .context(StrContext::Label("character in optional dependency"))
425 .context(StrContext::Expected(StrContextValue::Description(
426 "end of input.",
427 )))
428 }
429}
430
431impl FromStr for OptionalDependency {
432 type Err = Error;
433
434 /// Creates a new [`OptionalDependency`] from a string slice.
435 ///
436 /// Delegates to [`OptionalDependency::parser`].
437 ///
438 /// # Errors
439 ///
440 /// Returns an error if [`OptionalDependency::parser`] fails.
441 fn from_str(s: &str) -> Result<Self, Self::Err> {
442 Ok(Self::parser_until_eof.parse(s)?)
443 }
444}
445
446impl Display for OptionalDependency {
447 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
448 match self.description {
449 Some(ref description) => write!(fmt, "{}: {}", self.package_relation, description),
450 None => write!(fmt, "{}", self.package_relation),
451 }
452 }
453}
454
455/// Group of a package
456///
457/// Represents an arbitrary collection of packages that share a common
458/// characteristic or functionality.
459///
460/// While group names can be any valid UTF-8 string, it is recommended to follow
461/// the format of [`Name`] (`[a-z\d\-._@+]` but must not start with `[-.]`)
462/// to ensure consistency and ease of use.
463///
464/// This is a type alias for [`String`].
465///
466/// ## Examples
467/// ```
468/// use alpm_types::Group;
469///
470/// // Create a Group
471/// let group: Group = "package-group".to_string();
472/// ```
473pub type Group = String;
474
475#[cfg(test)]
476mod tests {
477 use insta::assert_snapshot;
478 use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
479 use rstest::rstest;
480
481 use super::*;
482 use crate::{VersionComparison, configure_insta};
483
484 const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
485 /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
486 /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
487 /// we don't test that fully
488 const EPOCH_REGEX: &str = r"(0|[1-9][0-9]{0,10})";
489 const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
490 const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
491 const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
492 const DESCRIPTION_REGEX: &str = "[^\n\r]*";
493
494 proptest! {
495 #![proptest_config(ProptestConfig::with_cases(1000))]
496
497
498 #[test]
499 fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
500 println!("s: {s}");
501 let name = PackageRelation::from_str(&s).unwrap();
502 prop_assert_eq!(s, format!("{}", name));
503 }
504 }
505
506 proptest! {
507 #[test]
508 fn opt_depend_from_str(
509 name in NAME_REGEX,
510 desc in DESCRIPTION_REGEX,
511 use_desc in proptest::bool::ANY
512 ) {
513 let desc_trimmed = desc.trim_ascii();
514 let desc_is_blank = desc_trimmed.is_empty();
515
516 let (raw_in, formatted_expected) = if use_desc {
517 // Raw input and expected formatted output.
518 // These are different because `desc` will be trimmed by the parser;
519 // if it is *only* ascii whitespace then it will be skipped altogether.
520 (
521 format!("{name}: {desc}"),
522 if !desc_is_blank {
523 format!("{name}: {desc_trimmed}")
524 } else {
525 name.clone()
526 }
527 )
528 } else {
529 (name.clone(), name.clone())
530 };
531
532 println!("input string: {raw_in}");
533 let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
534 let formatted_actual = format!("{opt_depend}");
535 prop_assert_eq!(
536 formatted_expected,
537 formatted_actual,
538 "Formatted output doesn't match input"
539 );
540 }
541 }
542
543 #[rstest]
544 #[case(
545 "python>=3",
546 Ok(PackageRelation {
547 name: Name::new("python").unwrap(),
548 version_requirement: Some(VersionRequirement {
549 comparison: VersionComparison::GreaterOrEqual,
550 version: "3".parse().unwrap(),
551 }),
552 }),
553 )]
554 #[case(
555 "java-environment>=17",
556 Ok(PackageRelation {
557 name: Name::new("java-environment").unwrap(),
558 version_requirement: Some(VersionRequirement {
559 comparison: VersionComparison::GreaterOrEqual,
560 version: "17".parse().unwrap(),
561 }),
562 }),
563 )]
564 fn valid_package_relation(
565 #[case] input: &str,
566 #[case] expected: Result<PackageRelation, Error>,
567 ) {
568 assert_eq!(PackageRelation::from_str(input), expected);
569 }
570
571 #[rstest]
572 #[case(
573 "example: this is an example dependency",
574 OptionalDependency {
575 package_relation: PackageRelation {
576 name: Name::new("example").unwrap(),
577 version_requirement: None,
578 },
579 description: Some("this is an example dependency".to_string()),
580 },
581 )]
582 #[case(
583 "example-two: a description with lots of whitespace padding ",
584 OptionalDependency {
585 package_relation: PackageRelation {
586 name: Name::new("example-two").unwrap(),
587 version_requirement: None,
588 },
589 description: Some("a description with lots of whitespace padding".to_string())
590 },
591 )]
592 #[case(
593 "dep_name",
594 OptionalDependency {
595 package_relation: PackageRelation {
596 name: Name::new("dep_name").unwrap(),
597 version_requirement: None,
598 },
599 description: None,
600 },
601 )]
602 #[case(
603 "dep_name: ",
604 OptionalDependency {
605 package_relation: PackageRelation {
606 name: Name::new("dep_name").unwrap(),
607 version_requirement: None,
608 },
609 description: None,
610 },
611 )]
612 #[case(
613 "dep_name_with_special_chars-123: description with !@#$%^&*",
614 OptionalDependency {
615 package_relation: PackageRelation {
616 name: Name::new("dep_name_with_special_chars-123").unwrap(),
617 version_requirement: None,
618 },
619 description: Some("description with !@#$%^&*".to_string()),
620 },
621 )]
622 // versioned optional dependencies
623 #[case(
624 "elfutils=0.192: for translations",
625 OptionalDependency {
626 package_relation: PackageRelation {
627 name: Name::new("elfutils").unwrap(),
628 version_requirement: Some(VersionRequirement {
629 comparison: VersionComparison::Equal,
630 version: "0.192".parse().unwrap(),
631 }),
632 },
633 description: Some("for translations".to_string()),
634 },
635 )]
636 #[case(
637 "python>=3: For Python bindings",
638 OptionalDependency {
639 package_relation: PackageRelation {
640 name: Name::new("python").unwrap(),
641 version_requirement: Some(VersionRequirement {
642 comparison: VersionComparison::GreaterOrEqual,
643 version: "3".parse().unwrap(),
644 }),
645 },
646 description: Some("For Python bindings".to_string()),
647 },
648 )]
649 #[case(
650 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
651 OptionalDependency {
652 package_relation: PackageRelation {
653 name: Name::new("java-environment").unwrap(),
654 version_requirement: Some(VersionRequirement {
655 comparison: VersionComparison::GreaterOrEqual,
656 version: "17".parse().unwrap(),
657 }),
658 },
659 description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
660 },
661 )]
662 // A ':' directly followed by a non-whitespace character acts as an epoch delimiter.
663 #[case(
664 "example>=1:17.0.1-5: my dependency",
665 OptionalDependency {
666 package_relation: PackageRelation {
667 name: Name::new("example").unwrap(),
668 version_requirement: Some(VersionRequirement {
669 comparison: VersionComparison::GreaterOrEqual,
670 version: "1:17.0.1-5".parse().unwrap(),
671 }),
672 },
673 description: Some("my dependency".to_string()),
674 },
675 )]
676 // A ':' followed by whitespace acts as a description delimiter.
677 #[case(
678 "example>1: 17.0.1-5 ambiguous.",
679 OptionalDependency {
680 package_relation: PackageRelation {
681 name: Name::new("example").unwrap(),
682 version_requirement: Some(VersionRequirement {
683 comparison: VersionComparison::Greater,
684 version: "1".parse().unwrap(),
685 }),
686 },
687 description: Some("17.0.1-5 ambiguous.".to_string()),
688 },
689 )]
690 fn opt_depend_from_string(#[case] input: &str, #[case] expected: OptionalDependency) {
691 let opt_depend_result = OptionalDependency::from_str(input);
692 let optional_dependency = match opt_depend_result {
693 Ok(dep) => dep,
694 Err(err) => {
695 panic!("Encountered unexpected error when parsing optional dependency:\n {err}")
696 }
697 };
698
699 assert_eq!(
700 expected, optional_dependency,
701 "Optional dependency has not been correctly parsed."
702 );
703 }
704
705 #[rstest]
706 #[case(
707 "example: this is an example dependency",
708 "example: this is an example dependency"
709 )]
710 #[case(
711 "example-two: a description with lots of whitespace padding ",
712 "example-two: a description with lots of whitespace padding"
713 )]
714 #[case(
715 "tabs: a description with a tab directly after the colon",
716 "tabs: a description with a tab directly after the colon"
717 )]
718 #[case("dep_name", "dep_name")]
719 #[case("dep_name: ", "dep_name")]
720 #[case(
721 "dep_name_with_special_chars-123: description with !@#$%^&*",
722 "dep_name_with_special_chars-123: description with !@#$%^&*"
723 )]
724 // versioned optional dependencies
725 #[case("elfutils=0.192: for translations", "elfutils=0.192: for translations")]
726 #[case("python>=3: For Python bindings", "python>=3: For Python bindings")]
727 #[case(
728 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
729 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver"
730 )]
731 fn opt_depend_to_string(#[case] input: &str, #[case] expected: &str) {
732 let opt_depend_result = OptionalDependency::from_str(input);
733 let Ok(optional_dependency) = opt_depend_result else {
734 panic!(
735 "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
736 )
737 };
738 assert_eq!(
739 expected,
740 optional_dependency.to_string(),
741 "OptionalDependency to_string is erroneous."
742 );
743 }
744
745 #[rstest]
746 #[case("#invalid-name: this is an example dependency")]
747 #[case(": no_name_colon")]
748 #[case("name:description with no leading whitespace")]
749 #[case("dep-name>=10: \n\ndescription with\rnewlines")]
750 fn opt_depend_invalid_string_parse_error(#[case] input: &str) {
751 let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
752 panic!("'{input}' erroneously parsed as a OptionalDependency")
753 };
754
755 let (test_name, _guard) = configure_insta();
756 assert_snapshot!(test_name, err_msg.to_string());
757 }
758}