1use core::fmt;
5
6#[derive(Clone, PartialEq, Eq, Debug, Default)]
12pub struct Version {
13 pub version: String,
15 pub major: u32,
17 pub minor: u32,
19 pub patch: u32,
21 pub extra: Option<String>,
27}
28
29fn take_u32(s: &str) -> Option<(u32, &str)> {
31 let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
32 if end == 0 {
33 return None;
34 }
35 let value = s[..end].parse::<u32>().unwrap_or(u32::MAX);
38 Some((value, &s[end..]))
39}
40
41impl Version {
42 pub fn parse(version: &str) -> Self {
47 let mut out = Version { version: version.to_string(), ..Default::default() };
48
49 let Some((major, rest)) = take_u32(version) else {
51 return out;
52 };
53 out.major = major;
54
55 let Some(rest) = rest.strip_prefix('.') else {
56 if !rest.is_empty() {
57 out.extra = Some(rest.to_string());
58 }
59 return out;
60 };
61
62 let Some((minor, rest)) = take_u32(rest) else {
63 out.extra = Some(rest.to_string());
64 return out;
65 };
66 out.minor = minor;
67
68 let Some(rest) = rest.strip_prefix('.') else {
69 if !rest.is_empty() {
70 out.extra = Some(rest.to_string());
71 }
72 return out;
73 };
74
75 let Some((patch, rest)) = take_u32(rest) else {
76 out.extra = Some(rest.to_string());
77 return out;
78 };
79 out.patch = patch;
80
81 if !rest.is_empty() {
83 let tail = rest.strip_prefix(['.', '-']).unwrap_or(rest);
84 if !tail.is_empty() {
85 out.extra = Some(tail.to_string());
86 }
87 }
88 out
89 }
90
91 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
93 Self { version: format!("{major}.{minor}.{patch}"), major, minor, patch, extra: None }
94 }
95
96 pub fn triple(&self) -> (u32, u32, u32) {
98 (self.major, self.minor, self.patch)
99 }
100}
101
102impl fmt::Display for Version {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(&self.version)
105 }
106}
107
108pub const ASDF_FORMAT_VERSION: &str = "1.0.0";
110
111pub const ASDF_STANDARD_VERSION: &str = "1.6.0";
113
114pub const SUPPORTED_STANDARD_VERSIONS: &[&str] =
116 &["1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "1.6.0"];
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn parses_plain_semver() {
124 let v = Version::parse("1.6.0");
125 assert_eq!(v.triple(), (1, 6, 0));
126 assert_eq!(v.extra, None);
127 assert_eq!(v.version, "1.6.0");
128 }
129
130 #[test]
131 fn separator_after_patch_is_absorbed() {
132 let v = Version::parse("0.1.0.dev4");
134 assert_eq!(v.triple(), (0, 1, 0));
135 assert_eq!(v.extra.as_deref(), Some("dev4"));
136
137 let v = Version::parse("1.2.3-rc1");
138 assert_eq!(v.triple(), (1, 2, 3));
139 assert_eq!(v.extra.as_deref(), Some("rc1"));
140 }
141
142 #[test]
143 fn other_suffixes_are_kept_verbatim() {
144 let v = Version::parse("0.1.0rc2");
146 assert_eq!(v.triple(), (0, 1, 0));
147 assert_eq!(v.extra.as_deref(), Some("rc2"));
148 }
149
150 #[test]
151 fn separator_before_patch_is_kept() {
152 let v = Version::parse("1.2-foo");
155 assert_eq!(v.triple(), (1, 2, 0));
156 assert_eq!(v.extra.as_deref(), Some("-foo"));
157 }
158
159 #[test]
160 fn truncated_versions_zero_the_rest() {
161 let v = Version::parse("1");
162 assert_eq!(v.triple(), (1, 0, 0));
163 assert_eq!(v.extra, None);
164
165 let v = Version::parse("1.2");
166 assert_eq!(v.triple(), (1, 2, 0));
167 assert_eq!(v.extra, None);
168 }
169
170 #[test]
171 fn non_numeric_is_preserved_verbatim() {
172 let v = Version::parse("not-a-version");
173 assert_eq!(v.triple(), (0, 0, 0));
174 assert_eq!(v.version, "not-a-version");
175 assert_eq!(v.extra, None);
176 }
177
178 #[test]
179 fn missing_component_becomes_extra() {
180 let v = Version::parse("1.x");
181 assert_eq!(v.triple(), (1, 0, 0));
182 assert_eq!(v.extra.as_deref(), Some("x"));
183 }
184
185 #[test]
186 fn empty_string() {
187 let v = Version::parse("");
188 assert_eq!(v.triple(), (0, 0, 0));
189 assert_eq!(v.version, "");
190 }
191
192 #[test]
193 fn display_round_trips_the_original() {
194 for s in ["1.6.0", "0.1.0rc2", "not-a-version", "1.2.3-rc1"] {
195 assert_eq!(Version::parse(s).to_string(), s);
196 }
197 }
198}