debian_control/lossy/
relations.rs

1//! Parser for relationship fields like `Depends`, `Recommends`, etc.
2//!
3//! # Example
4//! ```
5//! use debian_control::lossy::{Relations, Relation};
6//! use debian_control::relations::VersionConstraint;
7//!
8//! let mut relations: Relations = r"python3-dulwich (>= 0.19.0), python3-requests, python3-urllib3 (<< 1.26.0)".parse().unwrap();
9//! assert_eq!(relations.to_string(), "python3-dulwich (>= 0.19.0), python3-requests, python3-urllib3 (<< 1.26.0)");
10//! assert!(relations.satisfied_by(|name: &str| -> Option<debversion::Version> {
11//!    match name {
12//!    "python3-dulwich" => Some("0.19.0".parse().unwrap()),
13//!    "python3-requests" => Some("2.25.1".parse().unwrap()),
14//!    "python3-urllib3" => Some("1.25.11".parse().unwrap()),
15//!    _ => None
16//!    }}));
17//! relations.remove(1);
18//! relations[0][0].archqual = Some("amd64".to_string());
19//! assert_eq!(relations.to_string(), "python3-dulwich:amd64 (>= 0.19.0), python3-urllib3 (<< 1.26.0)");
20//! ```
21
22use std::iter::Peekable;
23
24use crate::relations::SyntaxKind::*;
25use crate::relations::{lex, BuildProfile, SyntaxKind, VersionConstraint};
26
27/// A relation entry in a relationship field.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct Relation {
30    /// Package name.
31    pub name: String,
32    /// Architecture qualifier.
33    pub archqual: Option<String>,
34    /// Architectures that this relation is only valid for.
35    pub architectures: Option<Vec<String>>,
36    /// Version constraint and version.
37    pub version: Option<(VersionConstraint, debversion::Version)>,
38    /// Build profiles that this relation is only valid for.
39    pub profiles: Vec<Vec<BuildProfile>>,
40}
41
42impl Default for Relation {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl Relation {
49    /// Create an empty relation.
50    pub fn new() -> Self {
51        Self {
52            name: String::new(),
53            archqual: None,
54            architectures: None,
55            version: None,
56            profiles: Vec::new(),
57        }
58    }
59
60    /// Build a new relation
61    pub fn build(name: &str) -> RelationBuilder {
62        RelationBuilder::new(name)
63    }
64
65    /// Check if this entry is satisfied by the given package versions.
66    ///
67    /// # Arguments
68    /// * `package_version` - A function that returns the version of a package.
69    ///
70    /// # Example
71    /// ```
72    /// use debian_control::lossy::Relation;
73    /// let entry: Relation = "samba (>= 2.0)".parse().unwrap();
74    /// assert!(entry.satisfied_by(|name: &str| -> Option<debversion::Version> {
75    ///    match name {
76    ///    "samba" => Some("2.0".parse().unwrap()),
77    ///    _ => None
78    /// }}));
79    /// ```
80    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup) -> bool {
81        let actual = package_version.lookup_version(self.name.as_str());
82        if let Some((vc, version)) = &self.version {
83            if let Some(actual) = actual {
84                match vc {
85                    VersionConstraint::GreaterThanEqual => actual.as_ref() >= version,
86                    VersionConstraint::LessThanEqual => actual.as_ref() <= version,
87                    VersionConstraint::Equal => actual.as_ref() == version,
88                    VersionConstraint::GreaterThan => actual.as_ref() > version,
89                    VersionConstraint::LessThan => actual.as_ref() < version,
90                }
91            } else {
92                false
93            }
94        } else {
95            actual.is_some()
96        }
97    }
98}
99
100/// A builder for a relation entry in a relationship field.
101pub struct RelationBuilder {
102    name: String,
103
104    archqual: Option<String>,
105    architectures: Option<Vec<String>>,
106    version: Option<(VersionConstraint, debversion::Version)>,
107    profiles: Vec<Vec<BuildProfile>>,
108}
109
110impl RelationBuilder {
111    /// Create a new relation builder.
112    pub fn new(name: &str) -> Self {
113        Self {
114            name: name.to_string(),
115            archqual: None,
116            architectures: None,
117            version: None,
118            profiles: Vec::new(),
119        }
120    }
121
122    /// Set the architecture qualifier.
123    pub fn archqual(mut self, archqual: &str) -> Self {
124        self.archqual = Some(archqual.to_string());
125        self
126    }
127
128    /// Set the architectures that this relation is only valid for.
129    pub fn architectures(mut self, architectures: Vec<&str>) -> Self {
130        self.architectures = Some(architectures.into_iter().map(|s| s.to_string()).collect());
131        self
132    }
133
134    /// Set the version constraint and version.
135    pub fn version(mut self, constraint: VersionConstraint, version: &str) -> Self {
136        self.version = Some((constraint, version.parse().unwrap()));
137        self
138    }
139
140    /// Add a build profile that this relation is only valid for.
141    pub fn profile(mut self, profile: Vec<BuildProfile>) -> Self {
142        self.profiles.push(profile);
143        self
144    }
145
146    /// Build the relation.
147    pub fn build(self) -> Relation {
148        Relation {
149            name: self.name,
150            archqual: self.archqual,
151            architectures: self.architectures,
152            version: self.version,
153            profiles: self.profiles,
154        }
155    }
156}
157
158impl std::fmt::Display for Relation {
159    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
160        write!(f, "{}", self.name)?;
161        if let Some(archqual) = &self.archqual {
162            write!(f, ":{}", archqual)?;
163        }
164        if let Some((constraint, version)) = &self.version {
165            write!(f, " ({} {})", constraint, version)?;
166        }
167        if let Some(archs) = &self.architectures {
168            write!(f, " [{}]", archs.join(" "))?;
169        }
170        for profile in &self.profiles {
171            write!(f, " <")?;
172            for (i, profile) in profile.iter().enumerate() {
173                if i > 0 {
174                    write!(f, ", ")?;
175                }
176                write!(f, "{}", profile)?;
177            }
178            write!(f, ">")?;
179        }
180        Ok(())
181    }
182}
183
184#[cfg(feature = "serde")]
185impl<'de> serde::Deserialize<'de> for Relation {
186    fn deserialize<D>(deserializer: D) -> Result<Relation, D::Error>
187    where
188        D: serde::Deserializer<'de>,
189    {
190        let s = String::deserialize(deserializer)?;
191        s.parse().map_err(serde::de::Error::custom)
192    }
193}
194
195#[cfg(feature = "serde")]
196impl serde::Serialize for Relation {
197    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198    where
199        S: serde::Serializer,
200    {
201        self.to_string().serialize(serializer)
202    }
203}
204
205/// A collection of relation entries in a relationship field.
206#[derive(Debug, Clone, PartialEq, Eq, Hash)]
207pub struct Relations(pub Vec<Vec<Relation>>);
208
209impl std::ops::Index<usize> for Relations {
210    type Output = Vec<Relation>;
211
212    fn index(&self, index: usize) -> &Self::Output {
213        &self.0[index]
214    }
215}
216
217impl std::ops::IndexMut<usize> for Relations {
218    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
219        &mut self.0[index]
220    }
221}
222
223impl FromIterator<Vec<Relation>> for Relations {
224    fn from_iter<I: IntoIterator<Item = Vec<Relation>>>(iter: I) -> Self {
225        Self(iter.into_iter().collect())
226    }
227}
228
229impl Default for Relations {
230    fn default() -> Self {
231        Self::new()
232    }
233}
234
235impl Relations {
236    /// Create an empty relations.
237    pub fn new() -> Self {
238        Self(Vec::new())
239    }
240
241    /// Remove an entry from the relations.
242    pub fn remove(&mut self, index: usize) {
243        self.0.remove(index);
244    }
245
246    /// Iterate over the entries in the relations.
247    pub fn iter(&self) -> impl Iterator<Item = Vec<&Relation>> {
248        self.0.iter().map(|entry| entry.iter().collect())
249    }
250
251    /// Number of entries in the relations.
252    pub fn len(&self) -> usize {
253        self.0.len()
254    }
255
256    /// Check if the relations are empty.
257    pub fn is_empty(&self) -> bool {
258        self.0.is_empty()
259    }
260
261    /// Check if the relations are satisfied by the given package versions.
262    pub fn satisfied_by(&self, package_version: impl crate::VersionLookup + Copy) -> bool {
263        self.0
264            .iter()
265            .all(|e| e.iter().any(|r| r.satisfied_by(package_version)))
266    }
267}
268
269impl FromIterator<Relation> for Relations {
270    fn from_iter<I: IntoIterator<Item = Relation>>(iter: I) -> Self {
271        Self(iter.into_iter().map(|r| vec![r]).collect())
272    }
273}
274
275impl std::fmt::Display for Relations {
276    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
277        for (i, entry) in self.0.iter().enumerate() {
278            if i > 0 {
279                f.write_str(", ")?;
280            }
281            for (j, relation) in entry.iter().enumerate() {
282                if j > 0 {
283                    f.write_str(" | ")?;
284                }
285                write!(f, "{}", relation)?;
286            }
287        }
288        Ok(())
289    }
290}
291
292impl std::str::FromStr for Relation {
293    type Err = String;
294
295    fn from_str(s: &str) -> Result<Self, Self::Err> {
296        let tokens = lex(s);
297        let mut tokens = tokens.into_iter().peekable();
298
299        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
300            while let Some((WHITESPACE, _)) = tokens.peek() {
301                tokens.next();
302            }
303        }
304
305        let name = match tokens.next() {
306            Some((IDENT, name)) => name,
307            _ => return Err("Expected package name".to_string()),
308        };
309
310        eat_whitespace(&mut tokens);
311
312        let archqual = if let Some((COLON, _)) = tokens.peek() {
313            tokens.next();
314            match tokens.next() {
315                Some((IDENT, s)) => Some(s),
316                _ => return Err("Expected architecture qualifier".to_string()),
317            }
318        } else {
319            None
320        };
321        eat_whitespace(&mut tokens);
322
323        let version = if let Some((L_PARENS, _)) = tokens.peek() {
324            tokens.next();
325            eat_whitespace(&mut tokens);
326            let mut constraint = String::new();
327            while let Some((kind, t)) = tokens.peek() {
328                match kind {
329                    EQUAL | L_ANGLE | R_ANGLE => {
330                        constraint.push_str(t);
331                        tokens.next();
332                    }
333                    _ => break,
334                }
335            }
336            let constraint = constraint.parse()?;
337            eat_whitespace(&mut tokens);
338            // Read IDENT and COLON tokens until we see R_PARENS
339            let mut version_string = String::new();
340            while let Some((kind, s)) = tokens.peek() {
341                match kind {
342                    R_PARENS => break,
343                    IDENT | COLON => version_string.push_str(s),
344                    n => return Err(format!("Unexpected token: {:?}", n)),
345                }
346                tokens.next();
347            }
348            let version = version_string
349                .parse()
350                .map_err(|e: debversion::ParseError| e.to_string())?;
351            eat_whitespace(&mut tokens);
352            if let Some((R_PARENS, _)) = tokens.next() {
353            } else {
354                return Err(format!("Expected ')', found {:?}", tokens.next()));
355            }
356            Some((constraint, version))
357        } else {
358            None
359        };
360
361        eat_whitespace(&mut tokens);
362
363        let architectures = if let Some((L_BRACKET, _)) = tokens.peek() {
364            tokens.next();
365            let mut archs = Vec::new();
366            loop {
367                match tokens.next() {
368                    Some((IDENT, s)) => archs.push(s),
369                    Some((WHITESPACE, _)) => {}
370                    Some((R_BRACKET, _)) => break,
371                    _ => return Err("Expected architecture name".to_string()),
372                }
373            }
374            Some(archs)
375        } else {
376            None
377        };
378
379        eat_whitespace(&mut tokens);
380
381        let mut profiles = Vec::new();
382        while let Some((L_ANGLE, _)) = tokens.peek() {
383            tokens.next();
384            loop {
385                let mut profile = Vec::new();
386                loop {
387                    match tokens.next() {
388                        Some((NOT, _)) => {
389                            let profile_name = match tokens.next() {
390                                Some((IDENT, s)) => s,
391                                _ => return Err("Expected profile name".to_string()),
392                            };
393                            profile.push(BuildProfile::Disabled(profile_name));
394                        }
395                        Some((IDENT, s)) => profile.push(BuildProfile::Enabled(s)),
396                        Some((WHITESPACE, _)) => {}
397                        _ => return Err("Expected profile name".to_string()),
398                    }
399                    if let Some((COMMA, _)) = tokens.peek() {
400                        tokens.next();
401                    } else {
402                        break;
403                    }
404                }
405                profiles.push(profile);
406                if let Some((R_ANGLE, _)) = tokens.next() {
407                    eat_whitespace(&mut tokens);
408                    break;
409                }
410            }
411        }
412
413        eat_whitespace(&mut tokens);
414
415        if let Some((kind, _)) = tokens.next() {
416            return Err(format!("Unexpected token: {:?}", kind));
417        }
418
419        Ok(Relation {
420            name,
421            archqual,
422            architectures,
423            version,
424            profiles,
425        })
426    }
427}
428
429impl std::str::FromStr for Relations {
430    type Err = String;
431
432    fn from_str(s: &str) -> Result<Self, Self::Err> {
433        let mut relations = Vec::new();
434        if s.is_empty() {
435            return Ok(Relations(relations));
436        }
437        for entry in s.split(',') {
438            let entry = entry.trim();
439            if entry.is_empty() {
440                // Ignore empty entries.
441                continue;
442            }
443            let entry_relations = entry.split('|').map(|relation| {
444                let relation = relation.trim();
445                if relation.is_empty() {
446                    return Err("Empty relation".to_string());
447                }
448                relation.parse()
449            });
450            relations.push(entry_relations.collect::<Result<Vec<_>, _>>()?);
451        }
452        Ok(Relations(relations))
453    }
454}
455
456#[cfg(feature = "serde")]
457impl<'de> serde::Deserialize<'de> for Relations {
458    fn deserialize<D>(deserializer: D) -> Result<Relations, D::Error>
459    where
460        D: serde::Deserializer<'de>,
461    {
462        let s = String::deserialize(deserializer)?;
463        s.parse().map_err(serde::de::Error::custom)
464    }
465}
466
467#[cfg(feature = "serde")]
468impl serde::Serialize for Relations {
469    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
470    where
471        S: serde::Serializer,
472    {
473        self.to_string().serialize(serializer)
474    }
475}
476
477/// An Package to Source relation
478#[derive(Debug, Clone, PartialEq, Eq, Hash)]
479pub struct SourceRelation {
480    /// Source package name
481    pub name: String,
482    /// Source package version (if different from binary package one)
483    pub version: Option<debversion::Version>,
484}
485
486impl std::fmt::Display for SourceRelation {
487    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
488        write!(f, "{name}", name = self.name)?;
489        if let Some(ref version) = self.version {
490            write!(f, " (= {version})")?;
491        }
492        Ok(())
493    }
494}
495
496impl std::str::FromStr for SourceRelation {
497    type Err = String;
498
499    fn from_str(s: &str) -> Result<Self, Self::Err> {
500        let tokens = lex(s);
501
502        let mut tokens = tokens.into_iter().peekable();
503
504        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
505            while let Some((WHITESPACE, _)) = tokens.peek() {
506                tokens.next();
507            }
508        }
509
510        let name = match tokens.next() {
511            Some((IDENT, name)) => name,
512
513            _ => return Err("Expected package name".to_string()),
514        };
515
516        eat_whitespace(&mut tokens);
517
518        match tokens.next() {
519            None => {
520                return Ok(Self {
521                    name,
522                    version: None,
523                });
524            }
525            Some((L_PARENS, _)) => {}
526            _ => return Err("Unexpected token after package name".to_string()),
527        };
528
529        let mut version_str = "".to_string();
530        while let Some((token, value)) = tokens.next() {
531            if token != R_PARENS {
532                version_str.push_str(&value);
533            } else {
534                break;
535            }
536        }
537
538        let version: debversion::Version = version_str
539            .parse()
540            .map_err(|err| format!("Failed to parse version: {err}"))?;
541
542        eat_whitespace(&mut tokens);
543
544        if tokens.next().is_some() {
545            return Err("Unexpected tokens after version".to_string());
546        }
547
548        Ok(Self {
549            name,
550            version: Some(version),
551        })
552    }
553}
554
555#[cfg(feature = "serde")]
556impl<'de> serde::Deserialize<'de> for SourceRelation {
557    fn deserialize<D>(deserializer: D) -> Result<SourceRelation, D::Error>
558    where
559        D: serde::Deserializer<'de>,
560    {
561        let s = String::deserialize(deserializer)?;
562
563        s.parse().map_err(serde::de::Error::custom)
564    }
565}
566
567#[cfg(feature = "serde")]
568impl serde::Serialize for SourceRelation {
569    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
570    where
571        S: serde::Serializer,
572    {
573        self.to_string().serialize(serializer)
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580
581    #[test]
582    fn test_parse() {
583        let input = "python3-dulwich";
584        let parsed: Relations = input.parse().unwrap();
585        assert_eq!(parsed.to_string(), input);
586        assert_eq!(parsed.len(), 1);
587        let entry = &parsed[0];
588        assert_eq!(entry.len(), 1);
589        let relation = &entry[0];
590        assert_eq!(relation.to_string(), "python3-dulwich");
591        assert_eq!(relation.version, None);
592
593        let input = "python3-dulwich (>= 0.20.21)";
594        let parsed: Relations = input.parse().unwrap();
595        assert_eq!(parsed.to_string(), input);
596        assert_eq!(parsed.len(), 1);
597        let entry = &parsed[0];
598        assert_eq!(entry.len(), 1);
599        let relation = &entry[0];
600        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
601        assert_eq!(
602            relation.version,
603            Some((
604                VersionConstraint::GreaterThanEqual,
605                "0.20.21".parse().unwrap()
606            ))
607        );
608    }
609
610    #[test]
611    fn test_multiple() {
612        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
613        let parsed: Relations = input.parse().unwrap();
614        assert_eq!(parsed.to_string(), input);
615        assert_eq!(parsed.len(), 2);
616        let entry = &parsed[0];
617        assert_eq!(entry.len(), 1);
618        let relation = &entry[0];
619        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
620        assert_eq!(
621            relation.version,
622            Some((
623                VersionConstraint::GreaterThanEqual,
624                "0.20.21".parse().unwrap()
625            ))
626        );
627        let entry = &parsed[1];
628        assert_eq!(entry.len(), 1);
629        let relation = &entry[0];
630        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
631        assert_eq!(
632            relation.version,
633            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
634        );
635    }
636
637    #[test]
638    fn test_architectures() {
639        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
640        let parsed: Relations = input.parse().unwrap();
641        assert_eq!(parsed.to_string(), input);
642        assert_eq!(parsed.len(), 1);
643        let entry = &parsed[0];
644        assert_eq!(
645            entry[0].to_string(),
646            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
647        );
648        assert_eq!(entry.len(), 1);
649        let relation = &entry[0];
650        assert_eq!(
651            relation.to_string(),
652            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
653        );
654        assert_eq!(relation.version, None);
655        assert_eq!(
656            relation.architectures.as_ref().unwrap(),
657            &vec![
658                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
659            ]
660            .into_iter()
661            .map(|s| s.to_string())
662            .collect::<Vec<_>>()
663        );
664    }
665
666    #[test]
667    fn test_profiles() {
668        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
669        let parsed: Relations = input.parse().unwrap();
670        assert_eq!(parsed.to_string(), input);
671        assert_eq!(parsed.iter().count(), 2);
672        let entry = parsed.iter().next().unwrap();
673        assert_eq!(
674            entry[0].to_string(),
675            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
676        );
677        assert_eq!(entry.len(), 1);
678        let relation = entry[0];
679        assert_eq!(
680            relation.to_string(),
681            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
682        );
683        assert_eq!(
684            relation.version,
685            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
686        );
687        assert_eq!(
688            relation.architectures.as_ref().unwrap(),
689            &["i386", "arm"]
690                .into_iter()
691                .map(|s| s.to_string())
692                .collect::<Vec<_>>()
693        );
694        assert_eq!(
695            relation.profiles,
696            vec![
697                vec![BuildProfile::Disabled("nocheck".to_string())],
698                vec![BuildProfile::Disabled("cross".to_string())]
699            ]
700        );
701    }
702
703    #[cfg(feature = "serde")]
704    #[test]
705    fn test_serde_relations() {
706        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
707        let parsed: Relations = input.parse().unwrap();
708        let serialized = serde_json::to_string(&parsed).unwrap();
709        assert_eq!(
710            serialized,
711            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
712        );
713        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
714        assert_eq!(deserialized, parsed);
715    }
716
717    #[cfg(feature = "serde")]
718    #[test]
719    fn test_serde_relation() {
720        let input = "python3-dulwich (>= 0.20.21)";
721        let parsed: Relation = input.parse().unwrap();
722        let serialized = serde_json::to_string(&parsed).unwrap();
723        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
724        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
725        assert_eq!(deserialized, parsed);
726    }
727
728    #[test]
729    fn test_relations_is_empty() {
730        let input = "python3-dulwich (>= 0.20.21)";
731        let parsed: Relations = input.parse().unwrap();
732        assert!(!parsed.is_empty());
733        let input = "";
734        let parsed: Relations = input.parse().unwrap();
735        assert!(parsed.is_empty());
736    }
737
738    #[test]
739    fn test_relations_len() {
740        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
741        let parsed: Relations = input.parse().unwrap();
742        assert_eq!(parsed.len(), 2);
743    }
744
745    #[test]
746    fn test_relations_remove() {
747        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
748        let mut parsed: Relations = input.parse().unwrap();
749        parsed.remove(1);
750        assert_eq!(parsed.len(), 1);
751        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
752    }
753
754    #[test]
755    fn test_relations_satisfied_by() {
756        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
757        let parsed: Relations = input.parse().unwrap();
758        assert!(
759            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
760                match name {
761                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
762                    _ => None,
763                }
764            })
765        );
766        assert!(
767            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
768                match name {
769                    "python3-dulwich" => Some("0.21".parse().unwrap()),
770                    _ => None,
771                }
772            })
773        );
774    }
775
776    #[test]
777    fn test_relation_satisfied_by() {
778        let input = "python3-dulwich (>= 0.20.21)";
779        let parsed: Relation = input.parse().unwrap();
780        assert!(
781            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
782                match name {
783                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
784                    _ => None,
785                }
786            })
787        );
788        assert!(
789            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
790                match name {
791                    "python3-dulwich" => Some("0.20.20".parse().unwrap()),
792                    _ => None,
793                }
794            })
795        );
796    }
797
798    #[test]
799    fn test_relations_from_iter() {
800        let relation1 = Relation::build("python3-dulwich")
801            .version(VersionConstraint::GreaterThanEqual, "0.19.0")
802            .build();
803        let relation2 = Relation::build("python3-requests").build();
804
805        let relations: Relations = vec![relation1, relation2].into_iter().collect();
806
807        assert_eq!(
808            relations.to_string(),
809            "python3-dulwich (>= 0.19.0), python3-requests"
810        );
811    }
812
813    #[test]
814    fn test_source_relation_without_version() {
815        let input = "some-package";
816        let parsed: SourceRelation = input.parse().unwrap();
817        assert_eq!(parsed.name, input);
818        assert!(parsed.version.is_none());
819    }
820
821    #[test]
822    fn test_source_relation_with_valid_version() {
823        let input = "some-package (1.2.3+dfsg1-5~bpo13+1)";
824        let parsed: SourceRelation = input.parse().unwrap();
825        assert_eq!(parsed.name, "some-package");
826
827        let expected_version = debversion::Version {
828            epoch: None,
829            upstream_version: "1.2.3+dfsg1".to_string(),
830            debian_revision: Some("5~bpo13+1".to_string()),
831        };
832        assert_eq!(parsed.version, Some(expected_version));
833    }
834
835    #[test]
836    fn test_source_relation_with_invalid_version() {
837        let input = "some-package (1.2_@!.3+dfsg1-5~bpo13+1)";
838
839        let attempted_parse: Result<SourceRelation, String> = input.parse();
840        assert!(attempted_parse.is_err())
841    }
842}