version_spec 0.11.3

A specification for working with partial, full, or aliased versions. Supports semver and calver.
Documentation
use crate::syntax::{Clause, Op, Range, Requirement, Version, VersionKind};

/// Trait for matching a version against the implementing type.
pub trait MatchesVersion {
    /// Returns true if the provided version satisfies this shape,
    /// following the same rules as the [`semver`] crate.
    fn matches(&self, version: &Version) -> bool;
}

impl MatchesVersion for Version {
    fn matches(&self, version: &Version) -> bool {
        self == version
    }
}

impl MatchesVersion for Requirement {
    fn matches(&self, version: &Version) -> bool {
        self.matches_op(version) && (version.prerelease.is_none() || self.matches_pre(version))
    }
}

impl MatchesVersion for Clause {
    fn matches(&self, version: &Version) -> bool {
        match self {
            Clause::All(reqs) => {
                reqs.iter().all(|req| req.matches_op(version))
                    && (version.prerelease.is_none()
                        || reqs.iter().any(|req| req.matches_pre(version)))
            }

            // Bounded ranges are inclusive on both ends
            Clause::Between(lower, upper) => {
                let lower = lower.to_requirement(Op::GreaterEq);
                let upper = upper.to_requirement(Op::LessEq);

                lower.matches_op(version)
                    && upper.matches_op(version)
                    && (version.prerelease.is_none()
                        || lower.matches_pre(version)
                        || upper.matches_pre(version))
            }

            Clause::Only(req) => req.matches(version),
        }
    }
}

impl MatchesVersion for Range {
    fn matches(&self, version: &Version) -> bool {
        if self.clauses.is_empty() {
            return version.prerelease.is_none();
        }

        self.clauses.iter().any(|clause| clause.matches(version))
    }
}

/// Options for formatting a version into a string.
#[derive(Debug, Clone)]
pub struct FormatOptions {
    /// Whether to include the comparison operator, and wildcard placeholders,
    /// for requirements.
    pub include_op: bool,
    /// Whether to include the minor version.
    pub include_minor: bool,
    /// Whether to include the scope.
    pub include_scope: bool,
    /// Whether to include the patch version.
    pub include_patch: bool,
    /// Whether to include the pre-release information.
    pub include_prerelease: bool,
    /// Whether to include the build metadata.
    pub include_build: bool,
    /// Whether to pad the major version with leading zeros, and if so, how many digits to pad to.
    pub pad_major: Option<u8>,
    /// Whether to pad the minor version with leading zeros, and if so, how many digits to pad to.
    pub pad_minor: Option<u8>,
    /// Whether to pad the patch version with leading zeros, and if so, how many digits to pad to.
    pub pad_patch: Option<u8>,
    /// The separator character to use between version components.
    pub separator: char,
}

impl FormatOptions {
    /// Create a new `FormatOptions` instance with the specified version kind.
    pub fn new(kind: VersionKind) -> Self {
        match kind {
            VersionKind::Calendar => Self::calendar(),
            VersionKind::Semantic => Self::semantic(),
        }
    }

    /// Returns a new `FormatOptions` instance with default settings for calendar versioning.
    pub fn calendar() -> Self {
        Self {
            pad_major: Some(4),
            pad_minor: Some(2),
            pad_patch: Some(2),
            separator: '-',
            ..Default::default()
        }
    }

    /// Returns a new `FormatOptions` instance with default settings for semantic versioning.
    pub fn semantic() -> Self {
        Self::default()
    }
}

impl Default for FormatOptions {
    fn default() -> Self {
        Self {
            include_op: true,
            include_minor: true,
            include_scope: true,
            include_patch: true,
            include_prerelease: true,
            include_build: true,
            pad_major: None,
            pad_minor: None,
            pad_patch: None,
            separator: '.',
        }
    }
}

/// Trait for formatting a version into a string with custom options.
pub trait FormatsVersion {
    /// Returns a formatted string representation of the version according to the provided options.
    fn to_formatted_string(&self, options: &FormatOptions) -> String;
}

impl FormatsVersion for Version {
    fn to_formatted_string(&self, options: &FormatOptions) -> String {
        let mut out = String::new();

        if options.include_scope
            && let Some(scope) = &self.scope
        {
            out.push_str(scope);
            out.push('-');
        }

        let pad = |out: &mut String, value: u32, width: Option<u8>| {
            if let Some(width) = width {
                let width = width as usize;
                out.push_str(&format!("{value:0>width$}"));
            } else {
                out.push_str(&value.to_string());
            }
        };

        pad(&mut out, self.major, options.pad_major);

        if options.include_minor && !(self.kind == VersionKind::Calendar && self.minor == 0) {
            out.push(options.separator);
            pad(&mut out, self.minor, options.pad_minor);

            if options.include_patch && !(self.kind == VersionKind::Calendar && self.patch == 0) {
                out.push(options.separator);
                pad(&mut out, self.patch, options.pad_patch);
            }
        }

        if options.include_prerelease
            && let Some(pre) = &self.prerelease
        {
            out.push('-');
            out.push_str(pre);
        }

        if options.include_build
            && let Some(build) = &self.build
        {
            out.push('+');
            out.push_str(build);
        }

        out
    }
}

impl FormatsVersion for Requirement {
    fn to_formatted_string(&self, options: &FormatOptions) -> String {
        let mut out = if options.include_op {
            self.op.to_string()
        } else {
            String::new()
        };

        if options.include_scope
            && let Some(scope) = &self.scope
        {
            out.push_str(scope);
            out.push('-');
        }

        let pad = |out: &mut String, value: &u32, width: Option<u8>| {
            if let Some(width) = width {
                let width = width as usize;
                out.push_str(&format!("{value:0>width$}"));
            } else {
                out.push_str(&value.to_string());
            }
        };

        if let Some(major) = &self.major {
            pad(&mut out, major, options.pad_major);

            if !options.include_minor {
                // Nothing
            } else if let Some(minor) = &self.minor {
                out.push(options.separator);
                pad(&mut out, minor, options.pad_minor);

                if !options.include_patch {
                    // Nothing
                } else if let Some(patch) = &self.patch {
                    out.push(options.separator);
                    pad(&mut out, patch, options.pad_patch);
                } else if options.include_op && self.op == Op::Wildcard {
                    out.push(options.separator);
                    out.push('*');
                }
            } else if options.include_op && self.op == Op::Wildcard {
                out.push(options.separator);
                out.push('*');
            }
        } else if options.include_op && self.op == Op::Wildcard {
            out.push('*');
        }

        if options.include_prerelease
            && let Some(pre) = &self.prerelease
        {
            out.push('-');
            out.push_str(pre);
        }

        out
    }
}