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('"') {
121 let after_open = &after_key[start + 1..];
122 if let Some(end) = after_open.find('"') {
123 let v = &after_open[..end];
124 if !v.is_empty() {
125 return Some(v.to_string());
126 }
127 }
128 }
129 }
130 }
131 None
132}
133
134fn extract_kv_yaml_version(content: &str) -> Option<String> {
135 for line in content.lines() {
136 let t = line.trim();
137 if let Some(r) = t.strip_prefix("version:") {
138 let v = r.trim();
139 if !v.is_empty() && !v.starts_with('#') {
140 return Some(v.to_string());
141 }
142 }
143 }
144 None
145}
146
147pub fn normalize_version(version: &str) -> String {
155 let after_scope = version.split('/').next_back().unwrap_or(version);
156 after_scope
157 .strip_prefix('v')
158 .unwrap_or(after_scope)
159 .to_string()
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
169 fn test_validate_version_standard() {
170 assert!(validate_version("v1.2.3"));
171 }
172
173 #[test]
174 fn test_validate_version_prerelease() {
175 assert!(validate_version("v1.2.3-rc.1"));
176 assert!(validate_version("v1.2.3-alpha"));
177 }
178
179 #[test]
180 fn test_validate_version_scoped() {
181 assert!(validate_version("cli/v1.2.3"));
182 assert!(validate_version("pkg.name/v0.1.0"));
183 }
184
185 #[test]
186 fn test_validate_version_no_v() {
187 assert!(!validate_version("1.2.3"));
188 }
189
190 #[test]
191 fn test_validate_version_incomplete() {
192 assert!(!validate_version("v1.2"));
193 assert!(!validate_version("v1"));
194 }
195
196 #[test]
197 fn test_validate_version_empty() {
198 assert!(!validate_version(""));
199 }
200
201 #[test]
202 fn test_validate_version_scope_only() {
203 assert!(!validate_version("cli/"));
204 }
205
206 #[test]
209 fn test_extract_kv_version() {
210 let c = r#"[package]
211name = "test"
212version = "1.2.3"
213"#;
214 assert_eq!(extract_kv_version(c, "version"), Some("1.2.3".into()));
215 }
216
217 #[test]
218 fn test_extract_kv_version_not_found() {
219 assert_eq!(extract_kv_version("", "version"), None);
220 }
221
222 #[test]
223 fn test_extract_json_version() {
224 assert_eq!(
225 extract_json_version(r#"{"version": "1.0.0"}"#),
226 Some("1.0.0".into())
227 );
228 }
229
230 #[test]
231 fn test_extract_json_version_not_found() {
232 assert_eq!(extract_json_version("{}"), None);
233 }
234
235 #[test]
236 fn test_extract_kv_yaml_version() {
237 assert_eq!(
238 extract_kv_yaml_version("version: 0.2.0"),
239 Some("0.2.0".into())
240 );
241 }
242
243 #[test]
244 fn test_extract_kv_yaml_version_commented() {
245 assert_eq!(extract_kv_yaml_version("# version: 0.2.0"), None);
246 }
247
248 #[test]
251 fn test_read_all_config_versions_empty_dir() {
252 let d = tempfile::tempdir().unwrap();
253 assert!(read_all_config_versions(d.path()).is_empty());
254 }
255
256 #[test]
257 fn test_read_all_config_versions_cargo() {
258 let d = tempfile::tempdir().unwrap();
259 std::fs::write(
260 d.path().join("Cargo.toml"),
261 r#"[package]
262name = "test"
263version = "0.1.0"
264"#,
265 )
266 .unwrap();
267 let versions = read_all_config_versions(d.path());
268 assert_eq!(versions.len(), 1);
269 assert_eq!(versions[0].1.as_deref(), Some("0.1.0"));
270 }
271
272 #[test]
275 fn test_normalize_version_v_prefix() {
276 assert_eq!(normalize_version("v1.2.3"), "1.2.3");
277 }
278
279 #[test]
280 fn test_normalize_version_scoped() {
281 assert_eq!(normalize_version("cli/v0.5.0"), "0.5.0");
282 }
283
284 #[test]
285 fn test_normalize_version_no_prefix() {
286 assert_eq!(normalize_version("1.2.3"), "1.2.3");
287 }
288}