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"
}
}