1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum GoVersionParseError {
10 Empty,
11 InvalidVersion,
12 MissingMinor,
13 TooManyComponents,
14}
15
16impl fmt::Display for GoVersionParseError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Empty => formatter.write_str("Go version cannot be empty"),
20 Self::InvalidVersion => formatter.write_str("invalid Go version"),
21 Self::MissingMinor => formatter.write_str("Go patch version requires a minor version"),
22 Self::TooManyComponents => formatter.write_str("Go version has too many components"),
23 }
24 }
25}
26
27impl Error for GoVersionParseError {}
28
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct GoMajorVersion(u16);
32
33impl GoMajorVersion {
34 pub const fn new(value: u16) -> Result<Self, GoVersionParseError> {
40 if value == 0 {
41 Err(GoVersionParseError::InvalidVersion)
42 } else {
43 Ok(Self(value))
44 }
45 }
46
47 #[must_use]
49 pub const fn value(self) -> u16 {
50 self.0
51 }
52}
53
54impl fmt::Display for GoMajorVersion {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(formatter, "{}", self.0)
57 }
58}
59
60#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub struct GoMinorVersion(u16);
63
64impl GoMinorVersion {
65 #[must_use]
67 pub const fn new(value: u16) -> Self {
68 Self(value)
69 }
70
71 #[must_use]
73 pub const fn value(self) -> u16 {
74 self.0
75 }
76}
77
78impl fmt::Display for GoMinorVersion {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(formatter, "{}", self.0)
81 }
82}
83
84#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct GoPatchVersion(u16);
87
88impl GoPatchVersion {
89 #[must_use]
91 pub const fn new(value: u16) -> Self {
92 Self(value)
93 }
94
95 #[must_use]
97 pub const fn value(self) -> u16 {
98 self.0
99 }
100}
101
102impl fmt::Display for GoPatchVersion {
103 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write!(formatter, "{}", self.0)
105 }
106}
107
108#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum GoVersionFamily {
111 Go1,
112 Go2,
113}
114
115impl GoVersionFamily {
116 #[must_use]
118 pub const fn as_str(self) -> &'static str {
119 match self {
120 Self::Go1 => "go1",
121 Self::Go2 => "go2",
122 }
123 }
124}
125
126impl fmt::Display for GoVersionFamily {
127 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128 formatter.write_str(self.as_str())
129 }
130}
131
132impl FromStr for GoVersionFamily {
133 type Err = GoVersionParseError;
134
135 fn from_str(input: &str) -> Result<Self, Self::Err> {
136 match normalize_go_prefix(input)?.as_str() {
137 "1" => Ok(Self::Go1),
138 "2" => Ok(Self::Go2),
139 _ => Err(GoVersionParseError::InvalidVersion),
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
146pub struct GoVersion {
147 major: GoMajorVersion,
148 minor: Option<GoMinorVersion>,
149 patch: Option<GoPatchVersion>,
150}
151
152impl GoVersion {
153 pub const fn new(
160 major: u16,
161 minor: Option<u16>,
162 patch: Option<u16>,
163 ) -> Result<Self, GoVersionParseError> {
164 if minor.is_none() && patch.is_some() {
165 return Err(GoVersionParseError::MissingMinor);
166 }
167
168 let Ok(major) = GoMajorVersion::new(major) else {
169 return Err(GoVersionParseError::InvalidVersion);
170 };
171
172 Ok(Self {
173 major,
174 minor: match minor {
175 Some(value) => Some(GoMinorVersion::new(value)),
176 None => None,
177 },
178 patch: match patch {
179 Some(value) => Some(GoPatchVersion::new(value)),
180 None => None,
181 },
182 })
183 }
184
185 #[must_use]
187 pub const fn major(self) -> u16 {
188 self.major.value()
189 }
190
191 #[must_use]
193 pub const fn minor(self) -> Option<u16> {
194 match self.minor {
195 Some(value) => Some(value.value()),
196 None => None,
197 }
198 }
199
200 #[must_use]
202 pub const fn patch(self) -> Option<u16> {
203 match self.patch {
204 Some(value) => Some(value.value()),
205 None => None,
206 }
207 }
208
209 #[must_use]
211 pub const fn family(self) -> Option<GoVersionFamily> {
212 match self.major() {
213 1 => Some(GoVersionFamily::Go1),
214 2 => Some(GoVersionFamily::Go2),
215 _ => None,
216 }
217 }
218
219 #[must_use]
221 pub const fn is_go1(self) -> bool {
222 matches!(self.family(), Some(GoVersionFamily::Go1))
223 }
224}
225
226impl fmt::Display for GoVersion {
227 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228 write!(formatter, "{}", self.major)?;
229 if let Some(minor) = self.minor {
230 write!(formatter, ".{minor}")?;
231 }
232 if let Some(patch) = self.patch {
233 write!(formatter, ".{patch}")?;
234 }
235 Ok(())
236 }
237}
238
239impl FromStr for GoVersion {
240 type Err = GoVersionParseError;
241
242 fn from_str(input: &str) -> Result<Self, Self::Err> {
243 parse_go_version(input)
244 }
245}
246
247impl TryFrom<&str> for GoVersion {
248 type Error = GoVersionParseError;
249
250 fn try_from(value: &str) -> Result<Self, Self::Error> {
251 Self::from_str(value)
252 }
253}
254
255#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
257pub struct GoToolchainVersion(GoVersion);
258
259impl GoToolchainVersion {
260 #[must_use]
262 pub const fn new(version: GoVersion) -> Self {
263 Self(version)
264 }
265
266 #[must_use]
268 pub const fn version(self) -> GoVersion {
269 self.0
270 }
271}
272
273impl fmt::Display for GoToolchainVersion {
274 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275 write!(formatter, "go{}", self.0)
276 }
277}
278
279impl FromStr for GoToolchainVersion {
280 type Err = GoVersionParseError;
281
282 fn from_str(input: &str) -> Result<Self, Self::Err> {
283 input.parse::<GoVersion>().map(Self)
284 }
285}
286
287impl TryFrom<&str> for GoToolchainVersion {
288 type Error = GoVersionParseError;
289
290 fn try_from(value: &str) -> Result<Self, Self::Error> {
291 Self::from_str(value)
292 }
293}
294
295#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
297pub struct GoCompatibilityVersion(GoVersion);
298
299impl GoCompatibilityVersion {
300 #[must_use]
302 pub const fn new(version: GoVersion) -> Self {
303 Self(version)
304 }
305
306 #[must_use]
308 pub const fn version(self) -> GoVersion {
309 self.0
310 }
311}
312
313impl fmt::Display for GoCompatibilityVersion {
314 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315 self.0.fmt(formatter)
316 }
317}
318
319impl FromStr for GoCompatibilityVersion {
320 type Err = GoVersionParseError;
321
322 fn from_str(input: &str) -> Result<Self, Self::Err> {
323 input.parse::<GoVersion>().map(Self)
324 }
325}
326
327impl TryFrom<&str> for GoCompatibilityVersion {
328 type Error = GoVersionParseError;
329
330 fn try_from(value: &str) -> Result<Self, Self::Error> {
331 Self::from_str(value)
332 }
333}
334
335fn parse_go_version(input: &str) -> Result<GoVersion, GoVersionParseError> {
336 let normalized = normalize_go_prefix(input)?;
337 let components = normalized.split('.').collect::<Vec<_>>();
338
339 if components.len() > 3 {
340 return Err(GoVersionParseError::TooManyComponents);
341 }
342
343 if components.iter().any(|component| component.is_empty()) {
344 return Err(GoVersionParseError::InvalidVersion);
345 }
346
347 let major = parse_component(components[0])?;
348 let minor = match components.get(1) {
349 Some(component) => Some(parse_component(component)?),
350 None => None,
351 };
352 let patch = match components.get(2) {
353 Some(component) => Some(parse_component(component)?),
354 None => None,
355 };
356
357 GoVersion::new(major, minor, patch)
358}
359
360fn normalize_go_prefix(input: &str) -> Result<String, GoVersionParseError> {
361 let trimmed = input.trim();
362 if trimmed.is_empty() {
363 return Err(GoVersionParseError::Empty);
364 }
365
366 let mut characters = trimmed.char_indices();
367 let first = characters.next();
368 let second = characters.next();
369 let without_prefix =
370 if matches!(first, Some((_, 'g' | 'G'))) && matches!(second, Some((_, 'o' | 'O'))) {
371 let Some((index, character)) = second else {
372 return Err(GoVersionParseError::InvalidVersion);
373 };
374 trimmed[index + character.len_utf8()..].trim_start()
375 } else {
376 trimmed
377 };
378
379 if without_prefix.is_empty() {
380 Err(GoVersionParseError::InvalidVersion)
381 } else {
382 Ok(without_prefix.to_string())
383 }
384}
385
386fn parse_component(component: &str) -> Result<u16, GoVersionParseError> {
387 if !component
388 .chars()
389 .all(|character| character.is_ascii_digit())
390 {
391 return Err(GoVersionParseError::InvalidVersion);
392 }
393 component
394 .parse::<u16>()
395 .map_err(|_| GoVersionParseError::InvalidVersion)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::{
401 GoCompatibilityVersion, GoToolchainVersion, GoVersion, GoVersionFamily, GoVersionParseError,
402 };
403
404 #[test]
405 fn parses_go_versions() -> Result<(), GoVersionParseError> {
406 assert_eq!("1".parse::<GoVersion>()?.to_string(), "1");
407 assert_eq!("1.21".parse::<GoVersion>()?.to_string(), "1.21");
408 assert_eq!("1.21.6".parse::<GoVersion>()?.to_string(), "1.21.6");
409 assert_eq!("go1.22.0".parse::<GoVersion>()?.to_string(), "1.22.0");
410 assert_eq!("Go 1.23.1".parse::<GoVersion>()?.to_string(), "1.23.1");
411 Ok(())
412 }
413
414 #[test]
415 fn exposes_version_helpers() -> Result<(), GoVersionParseError> {
416 let version: GoVersion = "1.22.0".parse()?;
417 assert_eq!(version.major(), 1);
418 assert_eq!(version.minor(), Some(22));
419 assert_eq!(version.patch(), Some(0));
420 assert_eq!(version.family(), Some(GoVersionFamily::Go1));
421 assert!(version.is_go1());
422 Ok(())
423 }
424
425 #[test]
426 fn rejects_invalid_versions() {
427 assert_eq!("".parse::<GoVersion>(), Err(GoVersionParseError::Empty));
428 assert_eq!(
429 "0".parse::<GoVersion>(),
430 Err(GoVersionParseError::InvalidVersion)
431 );
432 assert_eq!(
433 "1.2.3.4".parse::<GoVersion>(),
434 Err(GoVersionParseError::TooManyComponents)
435 );
436 assert_eq!(
437 "1.x".parse::<GoVersion>(),
438 Err(GoVersionParseError::InvalidVersion)
439 );
440 assert_eq!(
441 "π".parse::<GoVersion>(),
442 Err(GoVersionParseError::InvalidVersion)
443 );
444 }
445
446 #[test]
447 fn models_toolchain_and_compatibility_versions() -> Result<(), GoVersionParseError> {
448 let version: GoVersion = "1.21".parse()?;
449 let toolchain = GoToolchainVersion::new(version);
450 let compatibility = GoCompatibilityVersion::new(version);
451
452 assert_eq!(toolchain.to_string(), "go1.21");
453 assert_eq!(compatibility.to_string(), "1.21");
454 assert_eq!("go2".parse::<GoVersionFamily>()?, GoVersionFamily::Go2);
455 Ok(())
456 }
457}