1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::config::Project;
15use crate::model::names::TagName;
16
17const MIN_SEGMENTS: usize = 2;
18const MAX_SEGMENTS: usize = 6;
19
20#[derive(Debug, Clone, PartialEq, Eq, Error)]
21pub enum ReleaseVersionError {
22 #[error("release version is empty")]
23 Empty,
24
25 #[error(
26 "release version {value:?} has {count} segments: needs {MIN_SEGMENTS} to {MAX_SEGMENTS}"
27 )]
28 SegmentCount { value: String, count: usize },
29
30 #[error("release version {value:?} has a non-numeric segment {segment:?}")]
31 NonNumeric { value: String, segment: String },
32
33 #[error("Erlang major {value} is out of range")]
34 MajorOutOfRange { value: u32 },
35
36 #[error("tag {tag:?} does not start with the project's tag prefix {prefix:?}")]
37 MissingPrefix { tag: String, prefix: String },
38
39 #[error("series {value:?} is not of the form MAJOR.MINOR")]
40 Series { value: String },
41
42 #[error(
43 "Erlang version range {value:?} is not of the form [FROM,BELOW), with FROM below BELOW"
44 )]
45 Range { value: String },
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct ReleaseVersion {
50 segments: Vec<u32>,
51}
52
53impl ReleaseVersion {
54 pub fn parse(value: &str) -> Result<Self, ReleaseVersionError> {
55 if value.is_empty() {
56 return Err(ReleaseVersionError::Empty);
57 }
58 let segments = value
59 .split('.')
60 .map(|segment| {
61 segment
62 .parse::<u32>()
63 .map_err(|_| ReleaseVersionError::NonNumeric {
64 value: value.to_owned(),
65 segment: segment.to_owned(),
66 })
67 })
68 .collect::<Result<Vec<_>, _>>()?;
69 if !(MIN_SEGMENTS..=MAX_SEGMENTS).contains(&segments.len()) {
70 return Err(ReleaseVersionError::SegmentCount {
71 value: value.to_owned(),
72 count: segments.len(),
73 });
74 }
75 Ok(Self { segments })
76 }
77
78 fn major(&self) -> u32 {
79 self.segments[0]
80 }
81
82 fn minor(&self) -> u32 {
83 self.segments[1]
84 }
85}
86
87impl fmt::Display for ReleaseVersion {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 let mut segments = self.segments.iter();
90 if let Some(first) = segments.next() {
91 write!(f, "{first}")?;
92 }
93 for segment in segments {
94 write!(f, ".{segment}")?;
95 }
96 Ok(())
97 }
98}
99
100impl FromStr for ReleaseVersion {
101 type Err = ReleaseVersionError;
102
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 Self::parse(s)
105 }
106}
107
108fn strip_tag_prefix<'t>(prefix: &str, tag: &'t TagName) -> Result<&'t str, ReleaseVersionError> {
109 tag.as_str()
110 .strip_prefix(prefix)
111 .ok_or_else(|| ReleaseVersionError::MissingPrefix {
112 tag: tag.to_string(),
113 prefix: prefix.to_owned(),
114 })
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
118#[serde(transparent)]
119pub struct ErlangMajor(u16);
120
121impl ErlangMajor {
122 #[must_use]
123 pub fn get(self) -> u16 {
124 self.0
125 }
126}
127
128impl TryFrom<u32> for ErlangMajor {
129 type Error = ReleaseVersionError;
130
131 fn try_from(value: u32) -> Result<Self, Self::Error> {
132 u16::try_from(value)
133 .map(Self)
134 .map_err(|_| ReleaseVersionError::MajorOutOfRange { value })
135 }
136}
137
138impl fmt::Display for ErlangMajor {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "{}", self.0)
141 }
142}
143
144impl FromStr for ErlangMajor {
145 type Err = ReleaseVersionError;
146
147 fn from_str(s: &str) -> Result<Self, Self::Err> {
148 let value = s
149 .parse::<u32>()
150 .map_err(|_| ReleaseVersionError::NonNumeric {
151 value: s.to_owned(),
152 segment: s.to_owned(),
153 })?;
154 Self::try_from(value)
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
159#[serde(try_from = "String", into = "String")]
160pub struct HostVersion(ReleaseVersion);
161
162impl HostVersion {
163 pub fn parse(value: &str) -> Result<Self, ReleaseVersionError> {
164 ReleaseVersion::parse(value).map(Self)
165 }
166
167 #[must_use]
168 pub fn series(&self) -> HostSeries {
169 HostSeries {
170 major: self.0.major(),
171 minor: self.0.minor(),
172 }
173 }
174}
175
176impl fmt::Display for HostVersion {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 self.0.fmt(f)
179 }
180}
181
182impl FromStr for HostVersion {
183 type Err = ReleaseVersionError;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 Self::parse(s)
187 }
188}
189
190impl TryFrom<String> for HostVersion {
191 type Error = ReleaseVersionError;
192
193 fn try_from(value: String) -> Result<Self, Self::Error> {
194 Self::parse(&value)
195 }
196}
197
198impl From<HostVersion> for String {
199 fn from(version: HostVersion) -> Self {
200 version.to_string()
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
205#[serde(try_from = "String", into = "String")]
206pub struct OtpVersion {
207 version: ReleaseVersion,
208 major: ErlangMajor,
209}
210
211impl OtpVersion {
212 pub fn parse(value: &str) -> Result<Self, ReleaseVersionError> {
213 let version = ReleaseVersion::parse(value)?;
214 let major = ErlangMajor::try_from(version.major())?;
215 Ok(Self { version, major })
216 }
217
218 #[must_use]
219 pub fn major(&self) -> ErlangMajor {
220 self.major
221 }
222
223 #[must_use]
224 pub fn line(&self) -> OtpLineVersion {
225 OtpLineVersion {
226 major: self.major,
227 minor: self.version.minor(),
228 }
229 }
230}
231
232impl fmt::Display for OtpVersion {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 self.version.fmt(f)
235 }
236}
237
238impl FromStr for OtpVersion {
239 type Err = ReleaseVersionError;
240
241 fn from_str(s: &str) -> Result<Self, Self::Err> {
242 Self::parse(s)
243 }
244}
245
246impl TryFrom<String> for OtpVersion {
247 type Error = ReleaseVersionError;
248
249 fn try_from(value: String) -> Result<Self, Self::Error> {
250 Self::parse(&value)
251 }
252}
253
254impl From<OtpVersion> for String {
255 fn from(version: OtpVersion) -> Self {
256 version.to_string()
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
261#[serde(try_from = "String", into = "String")]
262pub struct HostSeries {
263 major: u32,
264 minor: u32,
265}
266
267impl fmt::Display for HostSeries {
268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269 write!(f, "{}.{}", self.major, self.minor)
270 }
271}
272
273fn parse_major_minor(value: &str) -> Option<(u32, u32)> {
274 let (major, minor) = value.split_once('.')?;
275 Some((major.parse().ok()?, minor.parse().ok()?))
276}
277
278impl FromStr for HostSeries {
279 type Err = ReleaseVersionError;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 parse_major_minor(s)
283 .map(|(major, minor)| Self { major, minor })
284 .ok_or_else(|| ReleaseVersionError::Series {
285 value: s.to_owned(),
286 })
287 }
288}
289
290impl TryFrom<String> for HostSeries {
291 type Error = ReleaseVersionError;
292
293 fn try_from(value: String) -> Result<Self, Self::Error> {
294 value.parse()
295 }
296}
297
298impl From<HostSeries> for String {
299 fn from(series: HostSeries) -> Self {
300 series.to_string()
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
305#[serde(try_from = "String", into = "String")]
306pub struct OtpLineVersion {
307 major: ErlangMajor,
308 minor: u32,
309}
310
311impl OtpLineVersion {
312 #[must_use]
313 pub fn major(self) -> ErlangMajor {
314 self.major
315 }
316
317 fn as_release(self) -> ReleaseVersion {
318 ReleaseVersion {
319 segments: vec![u32::from(self.major.get()), self.minor],
320 }
321 }
322}
323
324impl fmt::Display for OtpLineVersion {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "{}.{}", self.major, self.minor)
327 }
328}
329
330impl FromStr for OtpLineVersion {
331 type Err = ReleaseVersionError;
332
333 fn from_str(s: &str) -> Result<Self, Self::Err> {
334 let invalid = || ReleaseVersionError::Series {
335 value: s.to_owned(),
336 };
337 let (major, minor) = parse_major_minor(s).ok_or_else(invalid)?;
338 Ok(Self {
339 major: ErlangMajor::try_from(major)?,
340 minor,
341 })
342 }
343}
344
345impl TryFrom<String> for OtpLineVersion {
346 type Error = ReleaseVersionError;
347
348 fn try_from(value: String) -> Result<Self, Self::Error> {
349 value.parse()
350 }
351}
352
353impl From<OtpLineVersion> for String {
354 fn from(line: OtpLineVersion) -> Self {
355 line.to_string()
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
360pub struct HostTag {
361 version: HostVersion,
362 tag: TagName,
363}
364
365impl HostTag {
366 pub fn parse(project: &Project, tag: TagName) -> Result<Self, ReleaseVersionError> {
367 let version = HostVersion::parse(strip_tag_prefix(&project.tag_prefix, &tag)?)?;
368 Ok(Self { version, tag })
369 }
370
371 #[must_use]
372 pub fn tag(&self) -> &TagName {
373 &self.tag
374 }
375
376 #[must_use]
377 pub fn version(&self) -> &HostVersion {
378 &self.version
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
383pub struct OtpTag {
384 version: OtpVersion,
385 tag: TagName,
386}
387
388impl OtpTag {
389 pub fn parse(project: &Project, tag: TagName) -> Result<Self, ReleaseVersionError> {
390 let version = OtpVersion::parse(strip_tag_prefix(&project.tag_prefix, &tag)?)?;
391 Ok(Self { version, tag })
392 }
393
394 #[must_use]
395 pub fn tag(&self) -> &TagName {
396 &self.tag
397 }
398
399 #[must_use]
400 pub fn version(&self) -> &OtpVersion {
401 &self.version
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
406#[serde(try_from = "String", into = "String")]
407pub struct ErlangVersionRange {
408 from: OtpVersion,
409 below: OtpVersion,
410}
411
412impl ErlangVersionRange {
413 #[must_use]
414 pub fn contains(&self, line: OtpLineVersion) -> bool {
415 let line = line.as_release();
416 self.from.version <= line && line < self.below.version
417 }
418}
419
420impl fmt::Display for ErlangVersionRange {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 write!(f, "[{},{})", self.from, self.below)
423 }
424}
425
426impl FromStr for ErlangVersionRange {
427 type Err = ReleaseVersionError;
428
429 fn from_str(s: &str) -> Result<Self, Self::Err> {
430 let invalid = || ReleaseVersionError::Range {
431 value: s.to_owned(),
432 };
433 let (from, below) = s
434 .strip_prefix('[')
435 .and_then(|rest| rest.strip_suffix(')'))
436 .and_then(|inner| inner.split_once(','))
437 .ok_or_else(invalid)?;
438 let from = OtpVersion::parse(from.trim()).map_err(|_| invalid())?;
439 let below = OtpVersion::parse(below.trim()).map_err(|_| invalid())?;
440 if from >= below {
441 return Err(invalid());
442 }
443 Ok(Self { from, below })
444 }
445}
446
447impl TryFrom<String> for ErlangVersionRange {
448 type Error = ReleaseVersionError;
449
450 fn try_from(value: String) -> Result<Self, Self::Error> {
451 value.parse()
452 }
453}
454
455impl From<ErlangVersionRange> for String {
456 fn from(range: ErlangVersionRange) -> Self {
457 range.to_string()
458 }
459}