agm_core/import/
constraint.rs1use semver::VersionReq;
4
5use crate::error::{AgmError, ErrorCode, ErrorLocation};
6use crate::model::imports::ImportEntry;
7
8#[derive(Debug, Clone)]
16pub struct ValidatedImport {
17 pub entry: ImportEntry,
19 pub version_req: Option<semver::VersionReq>,
21}
22
23impl ValidatedImport {
24 #[must_use]
26 pub fn package(&self) -> &str {
27 &self.entry.package
28 }
29
30 #[must_use]
33 pub fn matches_version(&self, version: &semver::Version) -> bool {
34 match &self.version_req {
35 Some(req) => req.matches(version),
36 None => true,
37 }
38 }
39}
40
41pub fn parse_version_constraint(constraint: &str) -> Result<VersionReq, AgmError> {
50 let trimmed = constraint.trim();
51 semver::VersionReq::parse(trimmed).map_err(|e| {
52 AgmError::new(
53 ErrorCode::I002,
54 format!("Invalid version constraint: `{constraint}` ({e})"),
55 ErrorLocation::default(),
56 )
57 })
58}
59
60pub fn validate_import(entry: &ImportEntry) -> Result<ValidatedImport, AgmError> {
70 match &entry.version_constraint {
71 None => Ok(ValidatedImport {
72 entry: entry.clone(),
73 version_req: None,
74 }),
75 Some(constraint) => {
76 let req = parse_version_constraint(constraint)?;
77 Ok(ValidatedImport {
78 entry: entry.clone(),
79 version_req: Some(req),
80 })
81 }
82 }
83}
84
85pub fn validate_all_imports(imports: &[ImportEntry]) -> (Vec<ValidatedImport>, Vec<AgmError>) {
90 let mut validated = Vec::new();
91 let mut errors = Vec::new();
92
93 for entry in imports {
94 match validate_import(entry) {
95 Ok(v) => validated.push(v),
96 Err(e) => errors.push(e),
97 }
98 }
99
100 (validated, errors)
101}
102
103#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::error::ErrorCode;
111 use crate::model::imports::ImportEntry;
112
113 #[test]
116 fn test_parse_version_constraint_caret_returns_req() {
117 let req = parse_version_constraint("^1.0.0").unwrap();
118 assert!(req.matches(&semver::Version::parse("1.2.3").unwrap()));
119 assert!(!req.matches(&semver::Version::parse("2.0.0").unwrap()));
120 }
121
122 #[test]
123 fn test_parse_version_constraint_tilde_returns_req() {
124 let req = parse_version_constraint("~1.2.0").unwrap();
125 assert!(req.matches(&semver::Version::parse("1.2.5").unwrap()));
126 assert!(!req.matches(&semver::Version::parse("1.3.0").unwrap()));
127 }
128
129 #[test]
130 fn test_parse_version_constraint_exact_returns_req() {
131 let req = parse_version_constraint("=2.0.0").unwrap();
134 assert!(req.matches(&semver::Version::parse("2.0.0").unwrap()));
135 assert!(!req.matches(&semver::Version::parse("2.0.1").unwrap()));
136 }
137
138 #[test]
139 fn test_parse_version_constraint_wildcard_returns_req() {
140 let req = parse_version_constraint("1.*").unwrap();
141 assert!(req.matches(&semver::Version::parse("1.0.0").unwrap()));
142 assert!(req.matches(&semver::Version::parse("1.9.9").unwrap()));
143 assert!(!req.matches(&semver::Version::parse("2.0.0").unwrap()));
144 }
145
146 #[test]
147 fn test_parse_version_constraint_invalid_returns_error() {
148 let err = parse_version_constraint("not_semver").unwrap_err();
149 assert_eq!(err.code, ErrorCode::I002);
150 }
151
152 #[test]
155 fn test_validate_import_with_constraint_returns_validated() {
156 let entry = ImportEntry::new("shared.security".to_owned(), Some("^1.0.0".to_owned()));
157 let result = validate_import(&entry).unwrap();
158 assert_eq!(result.package(), "shared.security");
159 assert!(result.version_req.is_some());
160 }
161
162 #[test]
163 fn test_validate_import_without_constraint_returns_none_req() {
164 let entry = ImportEntry::new("shared.http".to_owned(), None);
165 let result = validate_import(&entry).unwrap();
166 assert_eq!(result.package(), "shared.http");
167 assert!(result.version_req.is_none());
168 }
169
170 #[test]
171 fn test_validate_import_invalid_constraint_returns_error() {
172 let entry = ImportEntry::new("pkg".to_owned(), Some("bogus".to_owned()));
173 let err = validate_import(&entry).unwrap_err();
174 assert_eq!(err.code, ErrorCode::I002);
175 }
176
177 #[test]
178 fn test_validate_all_imports_mixed_returns_partial() {
179 let imports = vec![
180 ImportEntry::new("shared.security".to_owned(), Some("^1.0.0".to_owned())),
181 ImportEntry::new("bad.pkg".to_owned(), Some("bogus".to_owned())),
182 ];
183 let (validated, errors) = validate_all_imports(&imports);
184 assert_eq!(validated.len(), 1);
185 assert_eq!(errors.len(), 1);
186 assert_eq!(validated[0].package(), "shared.security");
187 assert_eq!(errors[0].code, ErrorCode::I002);
188 }
189
190 #[test]
193 fn test_matches_version_with_caret_matching_returns_true() {
194 let entry = ImportEntry::new("pkg".to_owned(), Some("^1.0.0".to_owned()));
195 let validated = validate_import(&entry).unwrap();
196 let version = semver::Version::parse("1.5.0").unwrap();
197 assert!(validated.matches_version(&version));
198 }
199
200 #[test]
201 fn test_matches_version_with_caret_not_matching_returns_false() {
202 let entry = ImportEntry::new("pkg".to_owned(), Some("^1.0.0".to_owned()));
203 let validated = validate_import(&entry).unwrap();
204 let version = semver::Version::parse("2.0.0").unwrap();
205 assert!(!validated.matches_version(&version));
206 }
207
208 #[test]
209 fn test_matches_version_none_constraint_returns_true() {
210 let entry = ImportEntry::new("pkg".to_owned(), None);
211 let validated = validate_import(&entry).unwrap();
212 let version = semver::Version::parse("99.99.99").unwrap();
213 assert!(validated.matches_version(&version));
214 }
215}