1use crate::schema::FileKind;
2
3pub fn cast_version(version: &str, target_kind: FileKind) -> Result<String, String> {
6 match target_kind {
7 FileKind::Any => Ok(version.to_string()),
8 FileKind::Simple => cast_to_simple(version),
9 FileKind::Python => cast_to_python(version),
10 FileKind::Semver => cast_to_semver(version),
11 }
12}
13
14fn cast_to_simple(version: &str) -> Result<String, String> {
17 let version = version.to_lowercase();
18
19 let version = if let Some(pos) = version.find('!') {
21 &version[pos + 1..]
22 } else {
23 version.as_str()
24 };
25
26 let version = if let Some(pos) = version.find('+') {
28 &version[..pos]
29 } else {
30 version
31 };
32
33 let release_end = find_release_end(version);
35 let release = &version[..release_end];
36
37 let parts: Vec<&str> = release.split('.').collect();
39
40 if parts.is_empty() {
41 return Err(format!("Cannot cast '{version}' to simple version: no version parts found"));
42 }
43
44 for part in &parts {
46 if part.parse::<u32>().is_err() {
47 return Err(format!("Cannot cast '{version}' to simple version: invalid part '{part}'"));
48 }
49 }
50
51 let major = parts.first().unwrap_or(&"0");
53 let minor = parts.get(1).unwrap_or(&"0");
54 let patch = parts.get(2).unwrap_or(&"0");
55
56 Ok(format!("{major}.{minor}.{patch}"))
57}
58
59fn cast_to_python(version: &str) -> Result<String, String> {
62 let parts: Vec<&str> = version.split('.').collect();
64 if parts.iter().all(|p| p.parse::<u32>().is_ok()) {
65 return Ok(version.to_string());
66 }
67
68 Ok(version.to_string())
71}
72
73fn cast_to_semver(version: &str) -> Result<String, String> {
77 let version = version.to_lowercase();
78
79 let version = if let Some(pos) = version.find('!') {
81 &version[pos + 1..]
82 } else {
83 version.as_str()
84 };
85
86 let version = if let Some(pos) = version.find('+') {
88 &version[..pos]
89 } else {
90 version
91 };
92
93 let release_end = find_release_end(version);
95 let release = &version[..release_end];
96 let suffix = &version[release_end..];
97
98 let parts: Vec<&str> = release.split('.').collect();
100 if parts.is_empty() {
101 return Err(format!("Cannot cast '{version}' to semver: no version parts found"));
102 }
103
104 for part in &parts {
105 if part.parse::<u32>().is_err() {
106 return Err(format!("Cannot cast '{version}' to semver: invalid part '{part}'"));
107 }
108 }
109
110 let major = parts.first().unwrap_or(&"0");
111 let minor = parts.get(1).unwrap_or(&"0");
112 let patch = parts.get(2).unwrap_or(&"0");
113 let base = format!("{major}.{minor}.{patch}");
114
115 if suffix.is_empty() {
117 return Ok(base);
118 }
119
120 let suffix = suffix
122 .split(".post")
123 .next()
124 .unwrap_or(suffix)
125 .split(".dev")
126 .next()
127 .unwrap_or(suffix);
128
129 if suffix.is_empty() {
130 return Ok(base);
131 }
132
133 let js_prerelease = if let Some(rest) = suffix.strip_prefix("alpha") {
135 format!("-alpha.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
136 } else if let Some(rest) = suffix.strip_prefix('a') {
137 format!("-alpha.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
138 } else if let Some(rest) = suffix.strip_prefix("beta") {
139 format!("-beta.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
140 } else if let Some(rest) = suffix.strip_prefix('b') {
141 format!("-beta.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
142 } else if let Some(rest) = suffix.strip_prefix("rc") {
143 format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
144 } else if let Some(rest) = suffix.strip_prefix('c') {
145 format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
146 } else if let Some(rest) = suffix.strip_prefix("preview") {
147 format!("-rc.{}", rest.trim_start_matches(|c: char| !c.is_ascii_digit()))
148 } else {
149 return Ok(base);
151 };
152
153 Ok(format!("{base}{js_prerelease}"))
154}
155
156fn find_release_end(version: &str) -> usize {
158 let markers = ["a", "b", "c", "alpha", "beta", "preview", "rc", ".post", ".dev", "-"];
159
160 let mut earliest = version.len();
161
162 for marker in markers {
163 if let Some(pos) = version.find(marker) {
164 let before = &version[..pos];
166 if before.is_empty() || before.ends_with('.') || before.chars().last().unwrap().is_ascii_digit() {
167 earliest = earliest.min(pos);
168 }
169 }
170 }
171
172 earliest
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_cast_to_simple() {
181 assert_eq!(cast_to_simple("1.2.3").unwrap(), "1.2.3");
183
184 assert_eq!(cast_to_simple("1").unwrap(), "1.0.0");
186 assert_eq!(cast_to_simple("1.2").unwrap(), "1.2.0");
187
188 assert_eq!(cast_to_simple("1.2.3a1").unwrap(), "1.2.3");
190 assert_eq!(cast_to_simple("1.2.3b2").unwrap(), "1.2.3");
191 assert_eq!(cast_to_simple("1.2.3rc1").unwrap(), "1.2.3");
192 assert_eq!(cast_to_simple("1.2.3alpha1").unwrap(), "1.2.3");
193 assert_eq!(cast_to_simple("1.2.3beta2").unwrap(), "1.2.3");
194
195 assert_eq!(cast_to_simple("1.2.3.post1").unwrap(), "1.2.3");
197
198 assert_eq!(cast_to_simple("1.2.3.dev1").unwrap(), "1.2.3");
200
201 assert_eq!(cast_to_simple("1.2.3+local").unwrap(), "1.2.3");
203
204 assert_eq!(cast_to_simple("1!1.2.3").unwrap(), "1.2.3");
206
207 assert_eq!(cast_to_simple("1!1.2.3a1.post1.dev1+local").unwrap(), "1.2.3");
209 }
210
211 #[test]
212 fn test_cast_to_python() {
213 assert_eq!(cast_to_python("1.2.3").unwrap(), "1.2.3");
215
216 assert_eq!(cast_to_python("1.2.3a1").unwrap(), "1.2.3a1");
218 }
219}