Skip to main content

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        // Read a `${...}` substvar, with the leading DOLLAR already consumed,
306        // reassembling its literal text (e.g. `${misc:Depends}`).
307        fn read_substvar(
308            tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>,
309        ) -> Result<String, String> {
310            let mut text = String::from("$");
311            match tokens.next() {
312                Some((L_CURLY, s)) => text.push_str(&s),
313                _ => return Err("Expected '{' after '$' in substvar".to_string()),
314            }
315            loop {
316                match tokens.next() {
317                    Some((R_CURLY, s)) => {
318                        text.push_str(&s);
319                        return Ok(text);
320                    }
321                    Some((_, s)) => text.push_str(&s),
322                    None => return Err("Unterminated substvar".to_string()),
323                }
324            }
325        }
326
327        // A relation's name is either a package name or a substvar like
328        // `${misc:Depends}` (valid in control fields such as Depends).
329        let name = match tokens.next() {
330            Some((IDENT, name)) => name,
331            Some((DOLLAR, _)) => read_substvar(&mut tokens)?,
332            _ => return Err("Expected package name".to_string()),
333        };
334
335        eat_whitespace(&mut tokens);
336
337        let archqual = if let Some((COLON, _)) = tokens.peek() {
338            tokens.next();
339            match tokens.next() {
340                Some((IDENT, s)) => Some(s),
341                _ => return Err("Expected architecture qualifier".to_string()),
342            }
343        } else {
344            None
345        };
346        eat_whitespace(&mut tokens);
347
348        let version = if let Some((L_PARENS, _)) = tokens.peek() {
349            tokens.next();
350            eat_whitespace(&mut tokens);
351            let mut constraint = String::new();
352            while let Some((kind, t)) = tokens.peek() {
353                match kind {
354                    EQUAL | L_ANGLE | R_ANGLE => {
355                        constraint.push_str(t);
356                        tokens.next();
357                    }
358                    _ => break,
359                }
360            }
361            let constraint = constraint.parse()?;
362            eat_whitespace(&mut tokens);
363            // Read IDENT and COLON tokens until we see R_PARENS
364            let mut version_string = String::new();
365            while let Some((kind, s)) = tokens.peek() {
366                match kind {
367                    R_PARENS => break,
368                    IDENT | COLON => version_string.push_str(s),
369                    n => return Err(format!("Unexpected token: {:?}", n)),
370                }
371                tokens.next();
372            }
373            let version = version_string
374                .parse()
375                .map_err(|e: debversion::ParseError| e.to_string())?;
376            eat_whitespace(&mut tokens);
377            if let Some((R_PARENS, _)) = tokens.next() {
378            } else {
379                return Err(format!("Expected ')', found {:?}", tokens.next()));
380            }
381            Some((constraint, version))
382        } else {
383            None
384        };
385
386        eat_whitespace(&mut tokens);
387
388        let architectures = if let Some((L_BRACKET, _)) = tokens.peek() {
389            tokens.next();
390            let mut archs = Vec::new();
391            loop {
392                match tokens.next() {
393                    Some((IDENT, s)) => archs.push(s),
394                    Some((WHITESPACE, _)) => {}
395                    Some((R_BRACKET, _)) => break,
396                    _ => return Err("Expected architecture name".to_string()),
397                }
398            }
399            Some(archs)
400        } else {
401            None
402        };
403
404        eat_whitespace(&mut tokens);
405
406        let mut profiles = Vec::new();
407        while let Some((L_ANGLE, _)) = tokens.peek() {
408            tokens.next();
409            loop {
410                let mut profile = Vec::new();
411                loop {
412                    match tokens.next() {
413                        Some((NOT, _)) => {
414                            let profile_name = match tokens.next() {
415                                Some((IDENT, s)) => s,
416                                _ => return Err("Expected profile name".to_string()),
417                            };
418                            profile.push(BuildProfile::Disabled(profile_name));
419                        }
420                        Some((IDENT, s)) => profile.push(BuildProfile::Enabled(s)),
421                        Some((WHITESPACE, _)) => {}
422                        _ => return Err("Expected profile name".to_string()),
423                    }
424                    if let Some((COMMA, _)) = tokens.peek() {
425                        tokens.next();
426                    } else {
427                        break;
428                    }
429                }
430                profiles.push(profile);
431                if let Some((R_ANGLE, _)) = tokens.next() {
432                    eat_whitespace(&mut tokens);
433                    break;
434                }
435            }
436        }
437
438        eat_whitespace(&mut tokens);
439
440        if let Some((kind, _)) = tokens.next() {
441            return Err(format!("Unexpected token: {:?}", kind));
442        }
443
444        Ok(Relation {
445            name,
446            archqual,
447            architectures,
448            version,
449            profiles,
450        })
451    }
452}
453
454impl std::str::FromStr for Relations {
455    type Err = String;
456
457    fn from_str(s: &str) -> Result<Self, Self::Err> {
458        let mut relations = Vec::new();
459        if s.is_empty() {
460            return Ok(Relations(relations));
461        }
462        for entry in s.split(',') {
463            let entry = entry.trim();
464            if entry.is_empty() {
465                // Ignore empty entries.
466                continue;
467            }
468            let entry_relations = entry.split('|').map(|relation| {
469                let relation = relation.trim();
470                if relation.is_empty() {
471                    return Err("Empty relation".to_string());
472                }
473                relation.parse()
474            });
475            relations.push(entry_relations.collect::<Result<Vec<_>, _>>()?);
476        }
477        Ok(Relations(relations))
478    }
479}
480
481#[cfg(feature = "serde")]
482impl<'de> serde::Deserialize<'de> for Relations {
483    fn deserialize<D>(deserializer: D) -> Result<Relations, D::Error>
484    where
485        D: serde::Deserializer<'de>,
486    {
487        let s = String::deserialize(deserializer)?;
488        s.parse().map_err(serde::de::Error::custom)
489    }
490}
491
492#[cfg(feature = "serde")]
493impl serde::Serialize for Relations {
494    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
495    where
496        S: serde::Serializer,
497    {
498        self.to_string().serialize(serializer)
499    }
500}
501
502/// An Package to Source relation
503#[derive(Debug, Clone, PartialEq, Eq, Hash)]
504pub struct SourceRelation {
505    /// Source package name
506    pub name: String,
507    /// Source package version (if different from binary package one)
508    pub version: Option<debversion::Version>,
509}
510
511impl std::fmt::Display for SourceRelation {
512    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
513        write!(f, "{name}", name = self.name)?;
514        if let Some(ref version) = self.version {
515            write!(f, " (= {version})")?;
516        }
517        Ok(())
518    }
519}
520
521impl std::str::FromStr for SourceRelation {
522    type Err = String;
523
524    fn from_str(s: &str) -> Result<Self, Self::Err> {
525        let tokens = lex(s);
526
527        let mut tokens = tokens.into_iter().peekable();
528
529        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
530            while let Some((WHITESPACE, _)) = tokens.peek() {
531                tokens.next();
532            }
533        }
534
535        let name = match tokens.next() {
536            Some((IDENT, name)) => name,
537
538            _ => return Err("Expected package name".to_string()),
539        };
540
541        eat_whitespace(&mut tokens);
542
543        match tokens.next() {
544            None => {
545                return Ok(Self {
546                    name,
547                    version: None,
548                });
549            }
550            Some((L_PARENS, _)) => {}
551            _ => return Err("Unexpected token after package name".to_string()),
552        };
553
554        let mut version_str = "".to_string();
555        for (token, value) in tokens.by_ref() {
556            if token != R_PARENS {
557                version_str.push_str(&value);
558            } else {
559                break;
560            }
561        }
562
563        let version: debversion::Version = version_str
564            .parse()
565            .map_err(|err| format!("Failed to parse version: {err}"))?;
566
567        eat_whitespace(&mut tokens);
568
569        if tokens.next().is_some() {
570            return Err("Unexpected tokens after version".to_string());
571        }
572
573        Ok(Self {
574            name,
575            version: Some(version),
576        })
577    }
578}
579
580#[cfg(feature = "serde")]
581impl<'de> serde::Deserialize<'de> for SourceRelation {
582    fn deserialize<D>(deserializer: D) -> Result<SourceRelation, D::Error>
583    where
584        D: serde::Deserializer<'de>,
585    {
586        let s = String::deserialize(deserializer)?;
587
588        s.parse().map_err(serde::de::Error::custom)
589    }
590}
591
592#[cfg(feature = "serde")]
593impl serde::Serialize for SourceRelation {
594    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
595    where
596        S: serde::Serializer,
597    {
598        self.to_string().serialize(serializer)
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    fn test_parse() {
608        let input = "python3-dulwich";
609        let parsed: Relations = input.parse().unwrap();
610        assert_eq!(parsed.to_string(), input);
611        assert_eq!(parsed.len(), 1);
612        let entry = &parsed[0];
613        assert_eq!(entry.len(), 1);
614        let relation = &entry[0];
615        assert_eq!(relation.to_string(), "python3-dulwich");
616        assert_eq!(relation.version, None);
617
618        let input = "python3-dulwich (>= 0.20.21)";
619        let parsed: Relations = input.parse().unwrap();
620        assert_eq!(parsed.to_string(), input);
621        assert_eq!(parsed.len(), 1);
622        let entry = &parsed[0];
623        assert_eq!(entry.len(), 1);
624        let relation = &entry[0];
625        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
626        assert_eq!(
627            relation.version,
628            Some((
629                VersionConstraint::GreaterThanEqual,
630                "0.20.21".parse().unwrap()
631            ))
632        );
633    }
634
635    #[test]
636    fn test_substvar() {
637        // A relation that is a substvar (common in Depends fields) parses with
638        // the substvar as its name and round-trips.
639        let input = "${misc:Depends}";
640        let parsed: Relations = input.parse().unwrap();
641        assert_eq!(parsed.to_string(), input);
642        assert_eq!(parsed.len(), 1);
643        let relation = &parsed[0][0];
644        assert_eq!(relation.name, "${misc:Depends}");
645        assert_eq!(relation.version, None);
646
647        // Mixed with ordinary relations across a list.
648        let input = "${misc:Depends}, python3-dulwich (>= 0.20)";
649        let parsed: Relations = input.parse().unwrap();
650        assert_eq!(parsed.len(), 2);
651        assert_eq!(parsed[0][0].name, "${misc:Depends}");
652        assert_eq!(parsed[1][0].name, "python3-dulwich");
653    }
654
655    #[test]
656    fn test_multiple() {
657        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
658        let parsed: Relations = input.parse().unwrap();
659        assert_eq!(parsed.to_string(), input);
660        assert_eq!(parsed.len(), 2);
661        let entry = &parsed[0];
662        assert_eq!(entry.len(), 1);
663        let relation = &entry[0];
664        assert_eq!(relation.to_string(), "python3-dulwich (>= 0.20.21)");
665        assert_eq!(
666            relation.version,
667            Some((
668                VersionConstraint::GreaterThanEqual,
669                "0.20.21".parse().unwrap()
670            ))
671        );
672        let entry = &parsed[1];
673        assert_eq!(entry.len(), 1);
674        let relation = &entry[0];
675        assert_eq!(relation.to_string(), "python3-dulwich (<< 0.21)");
676        assert_eq!(
677            relation.version,
678            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
679        );
680    }
681
682    #[test]
683    fn test_architectures() {
684        let input = "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]";
685        let parsed: Relations = input.parse().unwrap();
686        assert_eq!(parsed.to_string(), input);
687        assert_eq!(parsed.len(), 1);
688        let entry = &parsed[0];
689        assert_eq!(
690            entry[0].to_string(),
691            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
692        );
693        assert_eq!(entry.len(), 1);
694        let relation = &entry[0];
695        assert_eq!(
696            relation.to_string(),
697            "python3-dulwich [amd64 arm64 armhf i386 mips mips64el mipsel ppc64el s390x]"
698        );
699        assert_eq!(relation.version, None);
700        assert_eq!(
701            relation.architectures.as_ref().unwrap(),
702            &vec![
703                "amd64", "arm64", "armhf", "i386", "mips", "mips64el", "mipsel", "ppc64el", "s390x"
704            ]
705            .into_iter()
706            .map(|s| s.to_string())
707            .collect::<Vec<_>>()
708        );
709    }
710
711    #[test]
712    fn test_profiles() {
713        let input = "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>, bar";
714        let parsed: Relations = input.parse().unwrap();
715        assert_eq!(parsed.to_string(), input);
716        assert_eq!(parsed.iter().count(), 2);
717        let entry = parsed.iter().next().unwrap();
718        assert_eq!(
719            entry[0].to_string(),
720            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
721        );
722        assert_eq!(entry.len(), 1);
723        let relation = entry[0];
724        assert_eq!(
725            relation.to_string(),
726            "foo (>= 1.0) [i386 arm] <!nocheck> <!cross>"
727        );
728        assert_eq!(
729            relation.version,
730            Some((VersionConstraint::GreaterThanEqual, "1.0".parse().unwrap()))
731        );
732        assert_eq!(
733            relation.architectures.as_ref().unwrap(),
734            &["i386", "arm"]
735                .into_iter()
736                .map(|s| s.to_string())
737                .collect::<Vec<_>>()
738        );
739        assert_eq!(
740            relation.profiles,
741            vec![
742                vec![BuildProfile::Disabled("nocheck".to_string())],
743                vec![BuildProfile::Disabled("cross".to_string())]
744            ]
745        );
746    }
747
748    #[cfg(feature = "serde")]
749    #[test]
750    fn test_serde_relations() {
751        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
752        let parsed: Relations = input.parse().unwrap();
753        let serialized = serde_json::to_string(&parsed).unwrap();
754        assert_eq!(
755            serialized,
756            r#""python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)""#
757        );
758        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
759        assert_eq!(deserialized, parsed);
760    }
761
762    #[cfg(feature = "serde")]
763    #[test]
764    fn test_serde_relation() {
765        let input = "python3-dulwich (>= 0.20.21)";
766        let parsed: Relation = input.parse().unwrap();
767        let serialized = serde_json::to_string(&parsed).unwrap();
768        assert_eq!(serialized, r#""python3-dulwich (>= 0.20.21)""#);
769        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
770        assert_eq!(deserialized, parsed);
771    }
772
773    #[test]
774    fn test_relations_is_empty() {
775        let input = "python3-dulwich (>= 0.20.21)";
776        let parsed: Relations = input.parse().unwrap();
777        assert!(!parsed.is_empty());
778        let input = "";
779        let parsed: Relations = input.parse().unwrap();
780        assert!(parsed.is_empty());
781    }
782
783    #[test]
784    fn test_relations_len() {
785        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
786        let parsed: Relations = input.parse().unwrap();
787        assert_eq!(parsed.len(), 2);
788    }
789
790    #[test]
791    fn test_relations_remove() {
792        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
793        let mut parsed: Relations = input.parse().unwrap();
794        parsed.remove(1);
795        assert_eq!(parsed.len(), 1);
796        assert_eq!(parsed.to_string(), "python3-dulwich (>= 0.20.21)");
797    }
798
799    #[test]
800    fn test_relations_satisfied_by() {
801        let input = "python3-dulwich (>= 0.20.21), python3-dulwich (<< 0.21)";
802        let parsed: Relations = input.parse().unwrap();
803        assert!(
804            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
805                match name {
806                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
807                    _ => None,
808                }
809            })
810        );
811        assert!(
812            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
813                match name {
814                    "python3-dulwich" => Some("0.21".parse().unwrap()),
815                    _ => None,
816                }
817            })
818        );
819    }
820
821    #[test]
822    fn test_relation_satisfied_by() {
823        let input = "python3-dulwich (>= 0.20.21)";
824        let parsed: Relation = input.parse().unwrap();
825        assert!(
826            parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
827                match name {
828                    "python3-dulwich" => Some("0.20.21".parse().unwrap()),
829                    _ => None,
830                }
831            })
832        );
833        assert!(
834            !parsed.satisfied_by(|name: &str| -> Option<debversion::Version> {
835                match name {
836                    "python3-dulwich" => Some("0.20.20".parse().unwrap()),
837                    _ => None,
838                }
839            })
840        );
841    }
842
843    #[test]
844    fn test_relations_from_iter() {
845        let relation1 = Relation::build("python3-dulwich")
846            .version(VersionConstraint::GreaterThanEqual, "0.19.0")
847            .build();
848        let relation2 = Relation::build("python3-requests").build();
849
850        let relations: Relations = vec![relation1, relation2].into_iter().collect();
851
852        assert_eq!(
853            relations.to_string(),
854            "python3-dulwich (>= 0.19.0), python3-requests"
855        );
856    }
857
858    #[test]
859    fn test_source_relation_without_version() {
860        let input = "some-package";
861        let parsed: SourceRelation = input.parse().unwrap();
862        assert_eq!(parsed.name, input);
863        assert!(parsed.version.is_none());
864    }
865
866    #[test]
867    fn test_source_relation_with_valid_version() {
868        let input = "some-package (1.2.3+dfsg1-5~bpo13+1)";
869        let parsed: SourceRelation = input.parse().unwrap();
870        assert_eq!(parsed.name, "some-package");
871
872        let expected_version = debversion::Version {
873            epoch: None,
874            upstream_version: "1.2.3+dfsg1".to_string(),
875            debian_revision: Some("5~bpo13+1".to_string()),
876        };
877        assert_eq!(parsed.version, Some(expected_version));
878    }
879
880    #[test]
881    fn test_source_relation_with_invalid_version() {
882        let input = "some-package (1.2_@!.3+dfsg1-5~bpo13+1)";
883
884        let attempted_parse: Result<SourceRelation, String> = input.parse();
885        assert!(attempted_parse.is_err())
886    }
887}