jayver 1.0.0

A calendar versioning scheme for binaries developed by Emmett Jayhart
Documentation
//! Error types for JayVer parsing and comparison operations.
//!
//! This module provides a comprehensive set of errors that can occur
//! during version parsing, comparison, and requirement handling.

use std::result::Result as StdResult;

use nom::error::Error as NomError;
use thiserror::Error as ThisError;

/// Convenience alias for results using JayVer errors.
pub type Result<T> = StdResult<T, Error>;

/// Errors that can occur when working with JayVer versions.
#[derive(Debug, ThisError)]
pub enum Error {
    /// A parsing error originating from `nom`.
    #[error("failed to parse `{input}`: {reason}")]
    Parse {
        /// The input that could not be parsed
        input: String,
        /// The reason parsing failed
        reason: String,
        /// The original nom error
        #[source]
        source: Option<Box<NomError<String>>>,
    },

    /// An invalid week number was specified.
    #[error("invalid week number: {week}, expected 1-53")]
    InvalidWeek {
        /// The invalid week number
        week: u8,
    },

    /// An invalid year/week combination was specified.
    #[error("invalid year/week combination: {year}.{week}")]
    InvalidYearWeek {
        /// The year component
        year: i32,
        /// The week component
        week: u8,
    },

    /// An invalid year was specified.
    #[error("invalid year: {year}, should be ISO week-numbering year minus 2000")]
    InvalidYear {
        /// The invalid year
        year: i32,
    },

    /// An invalid version string was provided.
    #[error("invalid version format: {version}")]
    InvalidVersion {
        /// The invalid version string
        version: String,
    },

    /// A version was expected in the format YY.WW.PATCH.
    #[error(
        "expected version to match format YY.WW.PATCH (where YY is ISO year minus 2000), got: \
         {version}"
    )]
    FormatError {
        /// The invalid version string
        version: String,
    },

    /// An invalid operator was provided in a version requirement.
    #[error("invalid operator: {operator}")]
    InvalidOperator {
        /// The invalid operator
        operator: String,
    },

    /// An invalid version requirement string was provided.
    #[error("invalid version requirement: {input}")]
    InvalidRequirement {
        /// The invalid requirement string
        input: String,
    },

    /// An empty version requirement was provided.
    #[error("empty version requirement")]
    EmptyRequirement,

    /// The requirement contained an unexpected character or was malformed.
    #[error("unexpected character in requirement at position {position}: {found}")]
    UnexpectedRequirementChar {
        /// The position of the unexpected character
        position: usize,
        /// The unexpected character
        found: char,
    },

    /// A generic error with a message.
    #[error("{0}")]
    Message(String),
}

impl Error {
    /// Create a new parse error with a message.
    pub(crate) fn parse<S>(input: S, reason: S) -> Self
    where
        S: Into<String>,
    {
        Error::Parse {
            input: input.into(),
            reason: reason.into(),
            source: None,
        }
    }

    /// Create a new parse error from a `nom` error.
    pub(crate) fn from_nom<S>(input: S, source: NomError<&str>) -> Self
    where
        S: Into<String>,
    {
        Error::Parse {
            input: input.into(),
            reason: format!("parser error: {:?}", source.code),
            source: Some(Box::new(NomError::new(
                source.input.to_string(),
                source.code,
            ))),
        }
    }

    /// Create a new format error.
    pub(crate) fn format<S>(version: S) -> Self
    where
        S: Into<String>,
    {
        Error::FormatError {
            version: version.into(),
        }
    }

    /// Create a new invalid version error.
    pub(crate) fn invalid_version<S>(version: S) -> Self
    where
        S: Into<String>,
    {
        Error::InvalidVersion {
            version: version.into(),
        }
    }

    /// Create a new invalid requirement error.
    pub(crate) fn invalid_requirement<S>(input: S) -> Self
    where
        S: Into<String>,
    {
        Error::InvalidRequirement {
            input: input.into(),
        }
    }
}

/// Custom implementation of `From<NomError>` that preserves the input.
impl From<NomError<&str>> for Error {
    fn from(e: NomError<&str>) -> Self {
        Error::from_nom(e.input, e)
    }
}

#[cfg(test)]
mod tests {
    use nom::error::ErrorKind;

    use super::*;

    #[test]
    fn test_parse_error_display() {
        let err = Error::parse("25.abc.0", "expected digit");
        assert_eq!(
            format!("{err}"),
            "failed to parse `25.abc.0`: expected digit"
        );
    }

    #[test]
    fn test_nom_error_conversion() {
        let nom_err = NomError::new("invalid", ErrorKind::Digit);
        let err = Error::from_nom("25.invalid.0", nom_err);
        assert_eq!(
            format!("{err}"),
            "failed to parse `25.invalid.0`: parser error: Digit"
        );
    }

    #[test]
    fn test_invalid_week_display() {
        let err = Error::InvalidWeek {
            week: 54,
        };
        assert_eq!(format!("{err}"), "invalid week number: 54, expected 1-53");
    }

    #[test]
    fn test_invalid_year_week_display() {
        let err = Error::InvalidYearWeek {
            year: 25,
            week: 54,
        };
        assert_eq!(format!("{err}"), "invalid year/week combination: 25.54");
    }

    #[test]
    fn test_invalid_requirement_display() {
        let err = Error::invalid_requirement("><25.10.0");
        assert_eq!(format!("{err}"), "invalid version requirement: ><25.10.0");
    }

    #[test]
    fn test_format_error_display() {
        let err = Error::format("25-10-0");
        assert_eq!(
            format!("{err}"),
            "expected version to match format YY.WW.PATCH (where YY is ISO year minus 2000), got: \
             25-10-0"
        );
    }

    #[test]
    fn test_unexpected_char_display() {
        let err = Error::UnexpectedRequirementChar {
            position: 2,
            found: '@',
        };
        assert_eq!(
            format!("{err}"),
            "unexpected character in requirement at position 2: @"
        );
    }

    #[test]
    fn test_empty_requirement_display() {
        let err = Error::EmptyRequirement;
        assert_eq!(format!("{err}"), "empty version requirement");
    }
}