1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use crate::{
    format::{Format, FormatError},
    specifier::{CalSemLevel, CalSemSpecifier, CalSpecifier, SemSpecifier, Specifier},
    version::{Date, NextError, Version, VersionError},
    SemLevel,
};

/// An error that occurred in a function that composes calls for other crate functions with other
/// error types.
#[non_exhaustive]
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum CompositeError {
    /// An error from parsing a format string. See [`FormatError`] for more details.
    #[error(transparent)]
    Format(#[from] FormatError),

    /// An error from parsing a version string. See [`VersionError`] for more details.
    #[error(transparent)]
    Version(#[from] VersionError),

    /// An error incrementing a [`Version`](crate::Version). See [`NextError`] for more details.
    #[error(transparent)]
    Next(#[from] NextError),
}

pub(crate) mod priv_trait {
    use super::Specifier as SpecifierT;
    use core::fmt::Debug;

    /// A private trait that is implemented by the public [`super::Scheme`] trait. This is used to
    /// define methods that are only meant to be used internally, and not by the user.
    pub(crate) trait Scheme: Sized + Debug + PartialEq + Eq {
        /// The kinds of specifiers this scheme uses
        type Specifier: SpecifierT;

        /// The maximum number of specifiers that can be in a format string. For a given scheme,
        /// this should equal the largest number of specifiers that can be in a valid format.
        ///
        /// See [`Scheme::MAX_TOKENS`].
        const MAX_SPECIFIERS: usize;

        /// The maximum number of tokens that can be in a [`Format`] or [`Version`]. To account for
        /// literals being around the specifiers, this is equal to the maximum number of specifiers
        /// times 2, plus 1. Think fenceposts.
        ///
        /// This is useful for pre-allocating a vector to hold the tokens.
        ///
        /// See [`Scheme::MAX_SPECIFIERS`].
        const MAX_TOKENS: usize = Self::MAX_SPECIFIERS * 2 + 1;

        /// The specifiers that can be used as the first specifier in a format string, comma
        /// separated, for use in error messages.
        fn first_variants_string() -> String {
            arr_to_english_or(Self::Specifier::first_variants())
        }

        /// The specifiers that can be used as the last specifier in a format string, comma
        /// separated, for use in error messages.
        fn last_variants_string() -> String {
            arr_to_english_or(Self::Specifier::last_variants())
        }

        /// Returns a human readable name of the scheme for error messages.
        fn name() -> &'static str;
    }

    fn arr_to_english_or(specs: &'static [&'static impl SpecifierT]) -> String {
        let spec_strings = specs
            .iter()
            .map(|spec| format!("`{spec}`"))
            .collect::<Vec<_>>();
        match spec_strings.as_slice() {
            [] => String::new(),
            [a] => a.to_string(),
            [a, b] => format!("{a} or {b}"),
            [firsts @ .., last] => {
                let mut joined = firsts.join(", ");
                joined.push_str(&format!(", or {last}"));
                joined
            }
        }
    }
}

/// A trait for versioning schemes, which dictate the kinds of specifiers/values allowed in
/// formats/versions and the rules for incrementing them.
#[allow(private_bounds)]
pub trait Scheme: priv_trait::Scheme {
    /// Parse a format string containing specifier and literal tokens into a [`Format`].
    ///
    /// The format string is made up of specifiers and literals. Specifiers indicate numeric values
    /// that can change, while literals are fixed text.
    ///
    /// See the specifier table [here](crate#table) for a list of all specifiers, which
    /// appear as `<...>` in the format string. To escape a literal `<`, use `<<`. `>` must not be
    /// escaped.
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// let format_str = "<YYYY>.<MM>.<PATCH>";
    /// let format = CalSem::new_format(format_str)?;
    /// assert_eq!(format_str, &format.to_string());
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// # Errors
    ///
    /// Returns an `Err` of one of the following [`FormatError`] variants:
    ///
    /// - [`FormatError::UnterminatedSpecifier`] if an open bracket is not closed with a closing
    ///   bracket.
    /// - [`FormatError::UnacceptableSpecifier`] if a specifier is not known to the scheme.
    /// - [`FormatError::SpecifiersMustStepDecrease`] if specifiers are not in order. See each
    ///   scheme's documentation for more details.
    /// - [`FormatError::WrongFirstSpecifier`] if the first specifier is not acceptable for the
    ///   scheme.
    /// - [`FormatError::Incomplete`] if the last specifier is not acceptable for the scheme.
    /// - [`FormatError::NoSpecifiersInFormat`] if there are no specifiers in the format.
    fn new_format(format_str: &str) -> Result<Format<Self>, FormatError> {
        Format::parse(format_str)
    }

    /// Parses a version string against a format string, and returns a [`Version`] object if the
    /// version string matches the format string. Otherwise, returns a
    /// [`NextError`](crate::NextError).
    ///
    /// This is a convenience method that creates a temporary [`Format`] object with
    /// [`Scheme::new_format`] and parses the version string against it with
    /// [`Format::new_version`].
    ///
    /// Note: For calendar schemes, the values in `version_str` are *not* validated to be actual
    /// dates. For example, `2021.02.31` is valid for the format `<YYYY>.<MM>.<DD>`, even though
    /// February 31st does not exist.
    ///
    /// Returns a result of [`Version`] or [`CompositeError`] if either of the format or version
    /// operations fail.
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// let format_str = "<YYYY>.<0M>.<PATCH>";
    /// let version_str = "2021.02.3";
    /// let version = CalSem::new_version(format_str, version_str)?;
    /// assert_eq!(version_str, &version.to_string());
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// # Errors
    ///
    /// Returns a [`CompositeError`] of all error surface area from [`Self::new_format`] and
    /// [`Format::new_version`].
    fn new_version<'vs>(
        format_str: &str,
        version_str: &'vs str,
    ) -> Result<Version<'vs, Self>, CompositeError> {
        let format = Self::new_format(format_str)?;
        let version = format.new_version(version_str)?;
        Ok(version)
    }

    /// Returns Ok(`true`) if the given version string is valid for the given format string, or else
    /// Ok(`false`). Returns an error if the format string could not be parsed.
    ///
    /// Returns a result of [`bool`] or [`FormatError`] if either of the format creation fails.
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// assert!(Sem::is_valid("<MAJOR>.<MINOR>.<PATCH>", "1.2.3").unwrap());
    /// assert!(!Sem::is_valid("<MAJOR>.<MINOR>.<PATCH>", "1.2").unwrap());
    /// ```
    ///
    /// # Errors
    ///
    /// Returns a [`FormatError`] if the format string could not be parsed.
    fn is_valid(format_str: &str, version_str: &str) -> Result<bool, FormatError> {
        let format = Self::new_format(format_str)?;
        let version = format.new_version(version_str);
        Ok(version.is_ok())
    }
}

/// Scheme for formats that have only semantic specifiers, such as `<MAJOR>.<MINOR>.<PATCH>`.
///
/// Sem behaves almost exactly like the [SemVer](https://semver.org/) scheme, but with a few
/// differences.
///
/// See the available specifiers for this scheme in the [table](crate#table).
///
/// # Rules
///
/// - The first specifier must be `MAJOR`.
/// - `MINOR` and `PATCH` are not required. If `MINOR` is present, it must be after `MAJOR`, and if
///   `PATCH` is present, it must be after `MINOR`.
/// - As for all schemes, arbitrary literals can be placed in the format string. For example, dots,
///   hyphens, or any other character(s) can be used, such as `v<MAJOR>#<MINOR>-p<PATCH>`.
///
/// # Example Formats
///
/// - `<MAJOR>.<MINOR>.<PATCH>`: Major, minor, and patch. Dot-separated.
/// - `v<MAJOR>.<MINOR>`: `v` followed by major and minor. Dot-separated.
#[derive(Debug, PartialEq, Eq)]
pub struct Sem;

impl Sem {
    /// Increments the version string (formatted by the format string) by the given semantic
    /// specifier and returns the new version's string.
    ///
    /// This is a convenience method that creates a temporary [`Format`] and [`Version`] with
    /// [`Scheme::new_version`], and increments it with
    /// [`Version::next`](struct.Version.html#method.next).
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// let next_str = Sem::next_version_string(
    ///   "<MAJOR>.<MINOR>.<PATCH>",
    ///   "1.2.3",
    ///    SemLevel::Minor
    /// ).unwrap();
    ///
    /// assert_eq!("1.3.0", next_str);
    /// ```
    ///
    /// # Errors
    ///
    /// Returns a [`CompositeError`] of all error surface area from [`Self::new_version`] and
    /// [`Version::next`](struct.Version.html#method.next).
    pub fn next_version_string(
        format_str: &str,
        version_str: &str,
        level: SemLevel,
    ) -> Result<String, CompositeError> {
        let version = Self::new_version(format_str, version_str)?;
        let next_version = version.next(level)?;
        Ok(next_version.to_string())
    }
}

impl Scheme for Sem {}

impl priv_trait::Scheme for Sem {
    type Specifier = SemSpecifier;

    // longest exemplar is <MAJOR><MINOR><PATCH>
    const MAX_SPECIFIERS: usize = 3;

    fn name() -> &'static str {
        "semantic"
    }
}

/// Scheme for formats that have only calendar specifiers, such as `<YYYY>.<MM>.<DD>`.
///
/// This scheme is less useful than [`CalSem`] because there is no way to increment it twice in the
/// same period of its least significant specifier. For example, a version with format
/// `<YYYY>.<MM>.<DD>` can only be incremented/updated once per day.
///
/// See the available specifiers for this scheme in the [table](crate#table).
///
/// # Rules
///
/// - The first specifier must be a year (`YYYY`, `YY`, or `0Y`).
/// - For adjacent specifiers `a` and `b`, `b` must be relative to `a`:
///   - month specifiers are relative to year ones (e.g., `<YYYY>.<MM>`)
///   - day specifiers are relative to month ones (e.g., `<YYYY>.<MM>.<DD>`)
///   - week specifiers are relative to year ones (and *not month ones*) (e.g., `<YYYY>.<WW>`)
/// - As for all schemes, arbitrary literals can be placed in the format string. For example, dots,
///   hyphens, or any other character(s) can be used, such as `y<YYYY>m<MM>d<DD>`.
///
/// # Example Formats
///
/// - `<YYYY>.<0M>.<0D>`: Full year, zero-padded month, and zero-padded day. Dot-separated.
/// - `<0Y>.<0M>.<0D>`: Zero-padded year, zero-padded month, and zero-padded day. Dot-separated.
/// - `<YYYY>-<0W>`: Full year and zero-padded week. Hyphen-separated.
#[derive(Debug, PartialEq, Eq)]
pub struct Cal;

impl Cal {
    /// Increments the version string (formatted by the format string) by the given date and returns
    /// the new version's string.
    ///
    /// This is a convenience method that creates a temporary [`Format`] and [`Version`] with
    /// [`Scheme::new_version`], and increments it with
    /// [`Version::next`](struct.Version.html#method.next-1).
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// let date = Date::utc_now(); // assume today is 2024-02-23
    /// # let date = Date::explicit(2024, 2, 23).unwrap();
    ///
    /// let next_str = Cal::next_version_string(
    ///   "<YYYY>.<0M>.<0D>",
    ///   "2001.02.03",
    ///   date
    /// ).unwrap();
    ///
    /// assert_eq!("2024.02.23", next_str);
    /// ```
    ///
    /// # Errors
    ///
    /// Returns a [`CompositeError`] of all error surface area from [`Self::new_version`] and
    /// [`Version::next`](struct.Version.html#method.next-1).
    pub fn next_version_string(
        format_str: &str,
        version_str: &str,
        date: Date,
    ) -> Result<String, CompositeError> {
        let format = Self::new_format(format_str)?;
        let version = Version::parse(version_str, &format)?;
        let next_version = version.next(date)?;
        Ok(next_version.to_string())
    }
}

impl Scheme for Cal {}

impl priv_trait::Scheme for Cal {
    type Specifier = CalSpecifier;

    // longest exemplar is <YYYY><MM><DD>
    const MAX_SPECIFIERS: usize = 3;

    fn name() -> &'static str {
        "calendar"
    }
}

/// Scheme for formats that have both calendar and semantic specifiers, such as
/// `<YYYY>.<MM>.<PATCH>`.
///
/// You would have such a format if you want to be able to increase your version multiple times
/// within the period of your smallest calendar specifier, such a second time in the same day.
///
/// See the available specifiers for this scheme in the [table](crate#table).
///
/// # Rules
///
/// - The first specifier must be a year (`YYYY`, `YY`, or `0Y`).
/// - For adjacent *calendar* specifiers `a` and `b`, `b` must be relative to `a`:
///   - month specifiers are relative to year ones (e.g., `<YYYY>.<MM>`)
///   - day specifiers are relative to month ones (e.g., `<YYYY>.<MM>.<DD>`)
///   - week specifiers are relative to year ones (and *not month ones*) (e.g., `<YYYY>.<WW>`)
/// - The format must end with the `PATCH` semantic specifier.
///   - `MINOR` may optionally come before `PATCH` if more granularity is desired.
/// - As for all schemes, arbitrary literals can be placed in the format string. For example, dots,
///   hyphens, or any other character(s) can be used, such as `y<YYYY>m<MM>d<DD>-p<PATCH>`.
///
/// # Example Formats
///
/// - `<YYYY>.<0M>.<0D>.<PATCH>`: Full year, zero-padded month, zero-padded day, and patch.
///   Dot-separated.
/// - `<0Y>.<0M>.<0D>.<PATCH>`: Zero-padded year, zero-padded month, zero-padded day, and patch.
///   Dot-separated.
/// - `<YYYY>.<0W>-<MINOR>.<PATCH>`: Full year, zero-padded week, minor, and patch. Dot- and
///   hyphen-separated.
#[derive(Debug, PartialEq, Eq)]
pub struct CalSem;

impl CalSem {
    /// Increments the version string (formatted by the format string) by the given date and
    /// semantic specifier, and returns the new version's string.
    ///
    /// This is a convenience method that creates a temporary [`Format`] and [`Version`] with
    /// [`Scheme::new_version`], and increments it with
    /// [`Version::next`](struct.Version.html#method.next-2).
    ///
    /// # Example
    ///
    /// ```
    /// use nextver::prelude::*;
    ///
    /// let date = Date::utc_now(); // assume today is 2024-02-23
    /// # let date = Date::explicit(2024, 2, 23).unwrap();
    ///
    /// let next_str = CalSem::next_version_string(
    ///   "<YYYY>.<0M>.<PATCH>",
    ///   "2024.01.42",
    ///   date,
    ///   CalSemLevel::Patch
    /// ).unwrap();
    ///
    /// assert_eq!("2024.02.0", next_str);
    /// ```
    ///
    /// # Errors
    ///
    /// Returns a [`CompositeError`] of all error surface area from [`Self::new_version`] and
    /// [`Version::next`](struct.Version.html#method.next-2).
    pub fn next_version_string(
        format_str: &str,
        version_str: &str,
        date: Date,
        level: CalSemLevel,
    ) -> Result<String, CompositeError> {
        let format = Self::new_format(format_str)?;
        let version = Version::parse(version_str, &format)?;
        let next_version = version.next(date, level)?;
        Ok(next_version.to_string())
    }
}

impl Scheme for CalSem {}

impl priv_trait::Scheme for CalSem {
    type Specifier = CalSemSpecifier;

    // longest exemplar is <YYYY><MM><DD><MINOR><PATCH>
    const MAX_SPECIFIERS: usize = 5;

    fn name() -> &'static str {
        "calendar-semantic"
    }
}