Skip to main content

agm_core/model/
imports.rs

1//! Import model (spec S10).
2
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6#[derive(Debug, Clone, PartialEq, thiserror::Error)]
7#[error("invalid import entry: {input:?}")]
8pub struct ParseImportError {
9    pub input: String,
10}
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct ImportEntry {
14    pub package: String,
15    pub version_constraint: Option<String>,
16}
17
18impl ImportEntry {
19    #[must_use]
20    pub fn new(package: String, version_constraint: Option<String>) -> Self {
21        Self {
22            package,
23            version_constraint,
24        }
25    }
26}
27
28impl FromStr for ImportEntry {
29    type Err = ParseImportError;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        let s = s.trim();
33        if s.is_empty() {
34            return Err(ParseImportError {
35                input: s.to_owned(),
36            });
37        }
38        if let Some((package, constraint)) = s.split_once('@') {
39            let package = package.trim();
40            let constraint = constraint.trim();
41            if package.is_empty() {
42                return Err(ParseImportError {
43                    input: s.to_owned(),
44                });
45            }
46            Ok(Self {
47                package: package.to_owned(),
48                version_constraint: if constraint.is_empty() {
49                    None
50                } else {
51                    Some(constraint.to_owned())
52                },
53            })
54        } else {
55            Ok(Self {
56                package: s.to_owned(),
57                version_constraint: None,
58            })
59        }
60    }
61}
62
63impl std::fmt::Display for ImportEntry {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.package)?;
66        if let Some(ref c) = self.version_constraint {
67            write!(f, "@{c}")?;
68        }
69        Ok(())
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_import_entry_from_str_package_only() {
79        let entry: ImportEntry = "shared.security".parse().unwrap();
80        assert_eq!(entry.package, "shared.security");
81        assert_eq!(entry.version_constraint, None);
82    }
83
84    #[test]
85    fn test_import_entry_from_str_with_caret_constraint() {
86        let entry: ImportEntry = "shared.security@^1.0.0".parse().unwrap();
87        assert_eq!(entry.package, "shared.security");
88        assert_eq!(entry.version_constraint, Some("^1.0.0".to_owned()));
89    }
90
91    #[test]
92    fn test_import_entry_from_str_with_exact_version() {
93        let entry: ImportEntry = "shared.http@2.0.0".parse().unwrap();
94        assert_eq!(entry.package, "shared.http");
95        assert_eq!(entry.version_constraint, Some("2.0.0".to_owned()));
96    }
97
98    #[test]
99    fn test_import_entry_from_str_with_tilde() {
100        let entry: ImportEntry = "core.utils@~1.2.0".parse().unwrap();
101        assert_eq!(entry.version_constraint, Some("~1.2.0".to_owned()));
102    }
103
104    #[test]
105    fn test_import_entry_from_str_with_wildcard() {
106        let entry: ImportEntry = "core.utils@1.*".parse().unwrap();
107        assert_eq!(entry.version_constraint, Some("1.*".to_owned()));
108    }
109
110    #[test]
111    fn test_import_entry_from_str_empty_returns_error() {
112        assert!("".parse::<ImportEntry>().is_err());
113    }
114
115    #[test]
116    fn test_import_entry_from_str_no_package_returns_error() {
117        assert!("@^1.0.0".parse::<ImportEntry>().is_err());
118    }
119
120    #[test]
121    fn test_import_entry_display_without_constraint() {
122        let entry = ImportEntry::new("shared.http".to_owned(), None);
123        assert_eq!(entry.to_string(), "shared.http");
124    }
125
126    #[test]
127    fn test_import_entry_display_with_constraint() {
128        let entry = ImportEntry::new("shared.http".to_owned(), Some("^1.0.0".to_owned()));
129        assert_eq!(entry.to_string(), "shared.http@^1.0.0");
130    }
131
132    #[test]
133    fn test_import_entry_roundtrip() {
134        let input = "shared.security@^1.0.0";
135        let entry: ImportEntry = input.parse().unwrap();
136        assert_eq!(entry.to_string(), input);
137    }
138
139    #[test]
140    fn test_import_entry_serde_roundtrip() {
141        let entry = ImportEntry::new("shared.security".to_owned(), Some("^1.0.0".to_owned()));
142        let json = serde_json::to_string(&entry).unwrap();
143        let back: ImportEntry = serde_json::from_str(&json).unwrap();
144        assert_eq!(entry, back);
145    }
146}