1
1
//! Chronologic version parsing.
2
//!
3
//! Chronologic versioning (see <https://chronver.org>) is a set of rules for assigning version
4
//! numbers.
5
//!
6
//! ## ChronVer overview
7
//!
8
//! Given a version number YEAR.MONTH.DAY.CHANGESET_IDENTIFIER, increment the:
9
//!
10
//! 1. YEAR version when the year changes,
11
//! 2. MONTH version when the month changes,
12
//! 3. DAY version when the day changes, and the
13
//! 4. CHANGESET_IDENTIFIER everytime you commit a change to your package/project.
14
//!
15
//! ## Versions
16
//!
17
//! A simple `Version` can be constructed by using the `parse` method:
18
//!
19
//! ```
20
//! use chronver::Version;
21
//! use time::macros::date;
22
//!
23
//! assert!(Version::parse("2020.01.06") == Ok(Version {
24
//!     date: date!(2020-01-06),
25
//!     changeset: 0,
26
//!     label: None,
27
//! }));
28
//! ```
29
//!
30
//! Versions can also be compared with each other:
31
//!
32
//! ```
33
//! use chronver::Version;
34
//!
35
//! assert_ne!(
36
//!     Version::parse("2020.01.06-alpha").unwrap(),
37
//!     Version::parse("2020.01.06-beta").unwrap()
38
//! );
39
//! ```
40
//!
41

            
42
#![doc(html_root_url = "https://docs.rs/chronver/0.2.0")]
43
#![forbid(unsafe_code)]
44
#![deny(clippy::all, clippy::pedantic)]
45
#![warn(clippy::nursery)]
46
#![warn(
47
    missing_docs,
48
    rustdoc::missing_doc_code_examples,
49
    clippy::missing_docs_in_private_items
50
)]
51

            
52
use std::{
53
    convert::TryFrom,
54
    fmt::{self, Display},
55
    str::FromStr,
56
};
57

            
58
use thiserror::Error;
59
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
60
pub use time::{Date, Month};
61

            
62
/// An error type for this crate.
63
3
#[derive(Error, Debug, Clone, Eq, PartialEq)]
64
pub enum ChronVerError {
65
    /// The version string was too short.
66
    #[error("Version string is too short")]
67
    TooShort,
68
    /// An error occurred while parsing the version component.
69
    #[error("Invalid version string")]
70
    InvalidVersion(#[from] time::error::Parse),
71
    /// An error occurred while constructing an version from date components.
72
    #[error("Invalid date components")]
73
    InvalidComponents(#[from] time::error::ComponentRange),
74
    /// An error occurred while parsing the changeset component.
75
    #[error("Invalid changeset")]
76
    InvalidChangeset(#[from] std::num::ParseIntError),
77
    /// An error occurred while parsing the label component.
78
    #[error("Invalid label")]
79
    InvalidLabel,
80
}
81

            
82
/// Represents a version number conforming to the chronologic versioning scheme.
83
8
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
84
#[cfg_attr(
85
    feature = "serde",
86
2
    derive(serde::Serialize, serde::Deserialize),
87
    serde(try_from = "&str"),
88
    serde(into = "String")
89
)]
90
pub struct Version {
91
    /// The date of release, to be updated whenever a new release is made on a different date than
92
    /// the last release.
93
    pub date: Date,
94
    /// The changeset number, to be incremented when a change was released on the same day.
95
    pub changeset: u32,
96
    /// The optional label, which can have any format or follow a branch formatting (see [`Label`]
97
    /// for more information).
98
    ///
99
    /// The special label `break` is reserved to signal a release with breaking changes.
100
    ///
101
    /// [`Label`]: enum.Label.html
102
    pub label: Option<Label>,
103
}
104

            
105
/// Minimum length that a version must have to be further processed.
106
const DATE_LENGTH: usize = 10;
107
/// Format for the date part of a version.
108
const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year].[month].[day]");
109
/// The special label to decide whether the version introduces breaking changes.
110
const BREAK_LABEL: &str = "break";
111

            
112
/// Shorthand to return an error when a condition is invalid.
113
macro_rules! ensure {
114
    ($cond:expr, $err:expr $(,)?) => {
115
        if !$cond {
116
            return Err($err);
117
        }
118
    };
119
}
120

            
121
impl Version {
122
    /// Parse a string into a chronver object.
123
    ///
124
    /// # Examples
125
    ///
126
    /// ```
127
    /// use chronver::{Version, Label};
128
    /// use time::macros::date;
129
    ///
130
    /// // Basic version with just a date
131
    /// assert_eq!(Version::parse("2020.03.05"), Ok(Version {
132
    ///     date: date!(2020-03-05),
133
    ///     changeset: 0,
134
    ///     label: None,
135
    /// }));
136
    ///
137
    /// // Version with a changeset
138
    /// assert_eq!(Version::parse("2020.03.05.2"), Ok(Version {
139
    ///     date: date!(2020-03-05),
140
    ///     changeset: 2,
141
    ///     label: None,
142
    /// }));
143
    ///
144
    /// // And with label
145
    /// assert_eq!(Version::parse("2020.03.05.2-new"), Ok(Version {
146
    ///     date: date!(2020-03-05),
147
    ///     changeset: 2,
148
    ///     label: Some(Label::Text("new".to_owned())),
149
    /// }));
150
    /// ```
151
    ///
152
    /// # Errors
153
    ///
154
    /// An error can occur in two cases. First, when the very first part of the version is not a
155
    /// valid date in the format `YYYY.MM.DD`. Second, when a **changeset** follows the date but
156
    /// it is not a valid `u32` number.
157
17
    pub fn parse(version: &str) -> Result<Self, ChronVerError> {
158
17
        ensure!(version.len() >= DATE_LENGTH, ChronVerError::TooShort);
159

            
160
15
        let date =
161
16
            Date::parse(&version[..DATE_LENGTH], &DATE_FORMAT).map_err(ChronVerError::from)?;
162

            
163
15
        let rem = &version[DATE_LENGTH..];
164

            
165
15
        let (changeset, label_pos) = if let Some(rem) = rem.strip_prefix('.') {
166
12
            let end = rem
167
21
                .find(|c: char| !c.is_ascii_digit())
168
12
                .unwrap_or_else(|| rem.len());
169
12
            (rem[..end].parse().map_err(ChronVerError::from)?, end + 1)
170
        } else {
171
3
            ensure!(
172
3
                rem.is_empty() || rem.starts_with('-'),
173
1
                ChronVerError::InvalidLabel
174
            );
175
2
            (0, 0)
176
        };
177

            
178
13
        let rem = &rem[label_pos..];
179

            
180
13
        let label = if let Some(rem) = rem.strip_prefix('-') {
181
9
            Some(rem.into())
182
        } else {
183
4
            ensure!(rem.is_empty(), ChronVerError::InvalidLabel);
184
3
            None
185
        };
186

            
187
12
        Ok(Self {
188
12
            date,
189
12
            changeset,
190
12
            label,
191
12
        })
192
17
    }
193

            
194
    /// Update the version to the current date or increment the changeset in case the date
195
    /// is the same. If a label exists, it will be removed.
196
    pub fn update(&mut self) {
197
        let new_date = OffsetDateTime::now_utc().date();
198
        if self.date == new_date {
199
            self.changeset += 1;
200
        } else {
201
            self.date = new_date;
202
            self.changeset = 0;
203
        }
204
        self.label = None;
205
    }
206

            
207
    /// Check whether the current version introduces breaking changes.
208
    ///
209
    /// # Examples
210
    ///
211
    /// ```
212
    /// use chronver::Version;
213
    ///
214
    /// assert!(Version::parse("2020.03.05-break").unwrap().is_breaking());
215
    /// assert!(!Version::parse("2020.03.05").unwrap().is_breaking());
216
    /// ```
217
    #[must_use]
218
    pub fn is_breaking(&self) -> bool {
219
        if let Some(Label::Text(label)) = &self.label {
220
            return label == BREAK_LABEL;
221
        }
222
        false
223
    }
224
}
225

            
226
impl Default for Version {
227
    #[must_use]
228
    fn default() -> Self {
229
        Self {
230
            date: OffsetDateTime::now_utc().date(),
231
            changeset: 0,
232
            label: None,
233
        }
234
    }
235
}
236

            
237
impl FromStr for Version {
238
    type Err = ChronVerError;
239

            
240
2
    fn from_str(s: &str) -> Result<Self, Self::Err> {
241
2
        Self::parse(s)
242
2
    }
243
}
244

            
245
impl Display for Version {
246
2
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
247
2
        f.write_str(&self.date.format(&DATE_FORMAT).map_err(|_| fmt::Error)?)?;
248
2
        if self.changeset > 0 {
249
2
            write!(f, ".{}", self.changeset)?;
250
        }
251
2
        if let Some(label) = &self.label {
252
2
            write!(f, "-{}", label)?;
253
        }
254
2
        Ok(())
255
2
    }
256
}
257

            
258
impl From<Date> for Version {
259
    #[must_use]
260
2
    fn from(date: Date) -> Self {
261
2
        Self {
262
2
            date,
263
2
            changeset: 0,
264
2
            label: None,
265
2
        }
266
2
    }
267
}
268

            
269
impl TryFrom<(i32, Month, u8)> for Version {
270
    type Error = ChronVerError;
271

            
272
    fn try_from(tuple: (i32, Month, u8)) -> Result<Self, Self::Error> {
273
        Date::from_calendar_date(tuple.0, tuple.1, tuple.2)
274
            .map(Self::from)
275
            .map_err(Into::into)
276
    }
277
}
278

            
279
impl TryFrom<&str> for Version {
280
    type Error = ChronVerError;
281

            
282
    #[inline]
283
2
    fn try_from(s: &str) -> Result<Self, Self::Error> {
284
2
        s.parse()
285
2
    }
286
}
287

            
288
impl From<Version> for String {
289
    #[inline]
290
    #[must_use]
291
2
    fn from(version: Version) -> Self {
292
2
        format!("{}", version)
293
2
    }
294
}
295

            
296
/// A label in the version metadata.
297
5
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
298
#[cfg_attr(
299
    feature = "serde",
300
    derive(serde::Serialize, serde::Deserialize),
301
    serde(from = "&str"),
302
    serde(into = "String")
303
)]
304
pub enum Label {
305
    /// A simple text label without a specific format.
306
    Text(String),
307
    /// A feature label in the format `BRANCH.CHANGESET`, where the changeset can be
308
    /// omitted when it is 0.
309
    Feature {
310
        /// Name of the feature branch.
311
        branch: String,
312
        /// Changeset number, omitted if 0.
313
        changeset: u32,
314
    },
315
}
316

            
317
impl Label {
318
    ///
319
    ///
320
    /// # Examples
321
    ///
322
    /// ```
323
    /// use chronver::Label;
324
    ///
325
    /// assert_eq!(Label::parse("test"), Label::Text("test".to_owned()));
326
    /// assert_eq!(Label::parse("feature.1"), Label::Feature {
327
    ///     branch: "feature".to_owned(),
328
    ///     changeset: 1,
329
    /// });
330
    /// ```
331
    #[must_use]
332
    pub fn parse(label: &str) -> Self {
333
9
        if let Some(i) = label.rfind('.') {
334
3
            if let Ok(changeset) = label[i + 1..].parse() {
335
3
                return Self::Feature {
336
3
                    branch: label[..i].to_owned(),
337
3
                    changeset,
338
3
                };
339
            }
340
6
        }
341

            
342
6
        Self::Text(label.to_owned())
343
9
    }
344
}
345

            
346
impl Display for Label {
347
2
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
348
2
        match self {
349
1
            Self::Text(s) => f.write_str(s),
350
1
            Self::Feature { branch, changeset } => write!(f, "{}.{}", branch, changeset),
351
        }
352
2
    }
353
}
354

            
355
impl From<&str> for Label {
356
    #[inline]
357
    #[must_use]
358
9
    fn from(s: &str) -> Self {
359
9
        Self::parse(s)
360
9
    }
361
}
362

            
363
impl From<Label> for String {
364
    #[inline]
365
    #[must_use]
366
    fn from(label: Label) -> Self {
367
        format!("{}", label)
368
    }
369
}
370

            
371
#[cfg(test)]
372
mod tests {
373
    use time::macros::date;
374

            
375
    use super::*;
376

            
377
1
    #[test]
378
1
    fn simple_version() {
379
1
        let version = Version::parse("2019.01.06");
380
1
        assert_eq!(Version::from(date!(2019 - 01 - 06)), version.unwrap());
381
1
    }
382

            
383
1
    #[test]
384
1
    fn with_changeset() {
385
1
        let version = Version::parse("2019.01.06.12");
386
1
        assert_eq!(
387
1
            Version {
388
1
                date: date!(2019 - 01 - 06),
389
1
                changeset: 12,
390
1
                label: None
391
1
            },
392
1
            version.unwrap()
393
1
        );
394
1
    }
395

            
396
1
    #[test]
397
1
    fn with_default_changeset() {
398
1
        let version = Version::parse("2019.01.06.0");
399
1
        assert_eq!(Version::from(date!(2019 - 01 - 06)), version.unwrap());
400
1
    }
401

            
402
1
    #[test]
403
1
    fn with_label() {
404
1
        let version = Version::parse("2019.01.06-test");
405
1
        assert_eq!(
406
1
            Version {
407
1
                date: date!(2019 - 01 - 06),
408
1
                changeset: 0,
409
1
                label: Some(Label::Text("test".to_owned()))
410
1
            },
411
1
            version.unwrap()
412
1
        );
413
1
    }
414

            
415
1
    #[test]
416
1
    fn with_changeset_and_label() {
417
1
        let version = Version::parse("2019.01.06.1-test");
418
1
        assert_eq!(
419
1
            Version {
420
1
                date: date!(2019 - 01 - 06),
421
1
                changeset: 1,
422
1
                label: Some(Label::Text("test".to_owned()))
423
1
            },
424
1
            version.unwrap()
425
1
        );
426
1
    }
427

            
428
1
    #[test]
429
1
    fn with_default_changeset_and_label() {
430
1
        let version = Version::parse("2019.01.06.0-test");
431
1
        assert_eq!(
432
1
            Version {
433
1
                date: date!(2019 - 01 - 06),
434
1
                changeset: 0,
435
1
                label: Some(Label::Text("test".to_owned()))
436
1
            },
437
1
            version.unwrap()
438
1
        );
439
1
    }
440

            
441
1
    #[test]
442
1
    fn too_short() {
443
1
        let version = Version::parse("2019");
444
1
        assert_eq!(ChronVerError::TooShort, version.unwrap_err());
445
1
    }
446

            
447
1
    #[test]
448
1
    fn invalid_date() {
449
1
        let version = Version::parse("2019.30.01");
450
1
        assert!(matches!(
451
1
            version.unwrap_err(),
452
            ChronVerError::InvalidVersion(_)
453
        ));
454
1
    }
455

            
456
1
    #[test]
457
1
    fn invalid_changeset() {
458
1
        let version = Version::parse("2019.01.06+111");
459
1
        assert_eq!(ChronVerError::InvalidLabel, version.unwrap_err());
460
1
    }
461

            
462
1
    #[test]
463
1
    fn invalid_changeset_number() {
464
1
        let version = Version::parse("2019.01.06.a");
465
1
        assert!(matches!(
466
1
            version.unwrap_err(),
467
            ChronVerError::InvalidChangeset(_)
468
        ));
469
1
    }
470

            
471
1
    #[test]
472
1
    fn invalid_label() {
473
1
        let version = Version::parse("2019.01.06.1+test");
474
1
        assert_eq!(ChronVerError::InvalidLabel, version.unwrap_err());
475
1
    }
476

            
477
    #[cfg(feature = "serde")]
478
1
    #[test]
479
1
    fn serialize() {
480
1
        let version = Version::parse("2019.01.06.1-test.2");
481
1
        assert_eq!(
482
1
            "\"2019.01.06.1-test.2\"",
483
1
            serde_json::to_string(&version.unwrap()).unwrap()
484
1
        );
485

            
486
1
        let version = Version::parse("2019.01.06.1-test");
487
1
        assert_eq!(
488
1
            "\"2019.01.06.1-test\"",
489
1
            serde_json::to_string(&version.unwrap()).unwrap()
490
1
        );
491
1
    }
492

            
493
    #[cfg(feature = "serde")]
494
1
    #[test]
495
1
    fn deserialize() {
496
1
        let version = Version::parse("2019.01.06.1-test.2");
497
1
        assert_eq!(
498
1
            serde_json::from_str::<Version>("\"2019.01.06.1-test.2\"").unwrap(),
499
1
            version.unwrap()
500
1
        );
501

            
502
1
        let version = Version::parse("2019.01.06.1-test");
503
1
        assert_eq!(
504
1
            serde_json::from_str::<Version>("\"2019.01.06.1-test\"").unwrap(),
505
1
            version.unwrap()
506
1
        );
507
1
    }
508
}