quanttide_devops/contract/
version.rs1use std::path::Path;
2
3pub fn validate_version(version: &str) -> bool {
19 if version.is_empty() {
20 return false;
21 }
22
23 let ver = if let Some(pos) = version.find('/') {
25 let scope = &version[..pos];
26 if scope.is_empty()
28 || !scope
29 .chars()
30 .all(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '-')
31 {
32 return false;
33 }
34 &version[pos + 1..]
35 } else {
36 version
37 };
38
39 let without_v = match ver.strip_prefix('v') {
41 Some(v) => v,
42 None => return false,
43 };
44
45 let (semver, _prerelease) = if let Some(dash) = without_v.find('-') {
47 let sv = &without_v[..dash];
48 let pr = &without_v[dash + 1..];
49 if pr.is_empty() || pr.starts_with('.') {
51 return false;
52 }
53 (sv, Some(pr))
54 } else {
55 (without_v, None)
56 };
57
58 let parts: Vec<&str> = semver.split('.').collect();
60 if parts.len() != 3 {
61 return false;
62 }
63 parts
64 .iter()
65 .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
66}
67
68type VersionExtract = fn(&str) -> Option<String>;
69
70pub fn read_all_config_versions(dir: &Path) -> Vec<(String, Option<String>)> {
79 let checks: &[(&str, VersionExtract)] = &[
80 ("Cargo.toml", |c| extract_kv_version(c, "version")),
81 ("pyproject.toml", |c| extract_kv_version(c, "version")),
82 ("package.json", extract_json_version),
83 ("pubspec.yaml", |c| extract_kv_yaml_version(c)),
84 ];
85 checks
86 .iter()
87 .filter_map(|(name, extract)| {
88 let path = dir.join(name);
89 if path.exists() {
90 let content = std::fs::read_to_string(&path).ok()?;
91 Some((name.to_string(), extract(&content)))
92 } else {
93 None
94 }
95 })
96 .collect()
97}
98
99fn extract_kv_version(content: &str, key: &str) -> Option<String> {
100 let p = format!("{} = \"", key);
101 for line in content.lines() {
102 let t = line.trim();
103 if let Some(r) = t.strip_prefix(&p)
104 && let Some(end) = r.find('"')
105 {
106 let v = r[..end].to_string();
107 if !v.is_empty() {
108 return Some(v);
109 }
110 }
111 }
112 None
113}
114
115fn extract_json_version(content: &str) -> Option<String> {
116 for line in content.lines() {
117 if let Some(pos) = line.find(r#""version":"#) {
118 let after_key = line[pos + 10..].trim();
119 if let Some(start) = after_key.find('"') {
120 let after_open = &after_key[start + 1..];
121 if let Some(end) = after_open.find('"') {
122 let v = &after_open[..end];
123 if !v.is_empty() {
124 return Some(v.to_string());
125 }
126 }
127 }
128 }
129 }
130 None
131}
132
133fn extract_kv_yaml_version(content: &str) -> Option<String> {
134 for line in content.lines() {
135 let t = line.trim();
136 if let Some(r) = t.strip_prefix("version:") {
137 let v = r.trim();
138 if !v.is_empty() && !v.starts_with('#') {
139 return Some(v.to_string());
140 }
141 }
142 }
143 None
144}
145
146pub fn normalize_version(version: &str) -> String {
154 let after_scope = version.split('/').next_back().unwrap_or(version);
155 after_scope
156 .strip_prefix('v')
157 .unwrap_or(after_scope)
158 .to_string()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
168 fn test_validate_version_standard() {
169 assert!(validate_version("v1.2.3"));
170 }
171
172 #[test]
173 fn test_validate_version_prerelease() {
174 assert!(validate_version("v1.2.3-rc.1"));
175 assert!(validate_version("v1.2.3-alpha"));
176 }
177
178 #[test]
179 fn test_validate_version_scoped() {
180 assert!(validate_version("cli/v1.2.3"));
181 assert!(validate_version("pkg.name/v0.1.0"));
182 }
183
184 #[test]
185 fn test_validate_version_no_v() {
186 assert!(!validate_version("1.2.3"));
187 }
188
189 #[test]
190 fn test_validate_version_incomplete() {
191 assert!(!validate_version("v1.2"));
192 assert!(!validate_version("v1"));
193 }
194
195 #[test]
196 fn test_validate_version_empty() {
197 assert!(!validate_version(""));
198 }
199
200 #[test]
201 fn test_validate_version_scope_only() {
202 assert!(!validate_version("cli/"));
203 }
204
205 #[test]
208 fn test_extract_kv_version() {
209 let c = r#"[package]
210name = "test"
211version = "1.2.3"
212"#;
213 assert_eq!(extract_kv_version(c, "version"), Some("1.2.3".into()));
214 }
215
216 #[test]
217 fn test_extract_kv_version_not_found() {
218 assert_eq!(extract_kv_version("", "version"), None);
219 }
220
221 #[test]
222 fn test_extract_json_version() {
223 assert_eq!(
224 extract_json_version(r#"{"version": "1.0.0"}"#),
225 Some("1.0.0".into())
226 );
227 }
228
229 #[test]
230 fn test_extract_json_version_not_found() {
231 assert_eq!(extract_json_version("{}"), None);
232 }
233
234 #[test]
235 fn test_extract_kv_yaml_version() {
236 assert_eq!(
237 extract_kv_yaml_version("version: 0.2.0"),
238 Some("0.2.0".into())
239 );
240 }
241
242 #[test]
243 fn test_extract_kv_yaml_version_commented() {
244 assert_eq!(extract_kv_yaml_version("# version: 0.2.0"), None);
245 }
246
247 #[test]
250 fn test_read_all_config_versions_empty_dir() {
251 let d = tempfile::tempdir().unwrap();
252 assert!(read_all_config_versions(d.path()).is_empty());
253 }
254
255 #[test]
256 fn test_read_all_config_versions_cargo() {
257 let d = tempfile::tempdir().unwrap();
258 std::fs::write(
259 d.path().join("Cargo.toml"),
260 r#"[package]
261name = "test"
262version = "0.1.0"
263"#,
264 )
265 .unwrap();
266 let versions = read_all_config_versions(d.path());
267 assert_eq!(versions.len(), 1);
268 assert_eq!(versions[0].1.as_deref(), Some("0.1.0"));
269 }
270
271 #[test]
274 fn test_normalize_version_v_prefix() {
275 assert_eq!(normalize_version("v1.2.3"), "1.2.3");
276 }
277
278 #[test]
279 fn test_normalize_version_scoped() {
280 assert_eq!(normalize_version("cli/v0.5.0"), "0.5.0");
281 }
282
283 #[test]
284 fn test_normalize_version_no_prefix() {
285 assert_eq!(normalize_version("1.2.3"), "1.2.3");
286 }
287}