qubit-argument 0.4.7

Structured, ownership-preserving argument validation for Rust applications
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Ownership-preserving validation for collection arguments.

use crate::argument::ArgumentError;
use crate::argument::ArgumentErrorKind;
use crate::argument::ArgumentResult;
use crate::argument::LengthConstraint;
use crate::argument::LengthMetric;
use crate::argument::internal::Sealed;

/// Validates collection lengths while preserving the original collection.
///
/// Every successful method consumes and returns the same collection value
/// without cloning its elements. Implementations are provided for owned
/// vectors, borrowed slices, and arrays. Length failures carry
/// [`LengthMetric::Elements`].
///
/// The trait is sealed to those library-supported collection forms.
///
/// # Examples
///
/// ```
/// use qubit_argument::CollectionArgument;
///
/// let values = vec![1, 2, 3].require_len("items", 3).unwrap();
/// assert_eq!(values, [1, 2, 3]);
/// ```
pub trait CollectionArgument: Sealed + Sized {
    /// Requires this collection to contain at least one element.
    ///
    /// # Parameters
    ///
    /// - `path`: identifies the collection in a validation error.
    ///
    /// # Returns
    ///
    /// The original collection on success, without cloning its elements.
    ///
    /// # Errors
    ///
    /// Returns [`ArgumentErrorKind::Empty`] at `path` when the collection is
    /// empty.
    fn require_non_empty(self, path: &str) -> ArgumentResult<Self>;

    /// Requires this collection to contain exactly `expected` elements.
    ///
    /// # Parameters
    ///
    /// - `path`: identifies the collection in a validation error.
    /// - `expected`: the required number of elements.
    ///
    /// # Returns
    ///
    /// The original collection on success, without cloning its elements.
    ///
    /// # Errors
    ///
    /// Returns [`ArgumentErrorKind::Length`] at `path` when the length differs
    /// from `expected`.
    fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self>;

    /// Requires this collection to contain at least `min` elements.
    ///
    /// # Parameters
    ///
    /// - `path`: identifies the collection in a validation error.
    /// - `min`: the minimum number of elements.
    ///
    /// # Returns
    ///
    /// The original collection on success, without cloning its elements.
    ///
    /// # Errors
    ///
    /// Returns [`ArgumentErrorKind::Length`] at `path` when the length is
    /// below `min`.
    fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self>;

    /// Requires this collection to contain at most `max` elements.
    ///
    /// # Parameters
    ///
    /// - `path`: identifies the collection in a validation error.
    /// - `max`: the maximum number of elements.
    ///
    /// # Returns
    ///
    /// The original collection on success, without cloning its elements.
    ///
    /// # Errors
    ///
    /// Returns [`ArgumentErrorKind::Length`] at `path` when the length is
    /// above `max`.
    fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self>;

    /// Requires this collection's length to lie in `min..=max`.
    ///
    /// # Parameters
    ///
    /// - `path`: identifies the collection in a validation error.
    /// - `min`: the inclusive lower bound.
    /// - `max`: the inclusive upper bound.
    ///
    /// # Returns
    ///
    /// The original collection on success, without cloning its elements.
    ///
    /// # Errors
    ///
    /// The range is validated before the observed length. If `min > max`,
    /// returns [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`;
    /// otherwise, an out-of-range length returns [`ArgumentErrorKind::Length`].
    fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self>;
}

impl<T> CollectionArgument for Vec<T> {
    /// Validates non-emptiness and returns the original vector.
    ///
    /// An empty vector returns [`ArgumentErrorKind::Empty`] at `path`.
    #[inline]
    fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
        if self.is_empty() {
            return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
        }
        Ok(self)
    }

    /// Validates the exact length and returns the original vector.
    ///
    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::Exact(expected))?;
        Ok(self)
    }

    /// Validates the minimum length and returns the original vector.
    ///
    /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::AtLeast(min))?;
        Ok(self)
    }

    /// Validates the maximum length and returns the original vector.
    ///
    /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::AtMost(max))?;
        Ok(self)
    }

    /// Validates an inclusive length range and returns the original vector.
    ///
    /// A reversed range returns
    /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
    /// out-of-range length returns [`ArgumentErrorKind::Length`].
    #[inline]
    fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::InRange { min, max })?;
        Ok(self)
    }
}

impl<T> CollectionArgument for &[T] {
    /// Validates non-emptiness and returns the original borrowed slice.
    ///
    /// An empty slice returns [`ArgumentErrorKind::Empty`] at `path`.
    #[inline]
    fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
        if self.is_empty() {
            return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
        }
        Ok(self)
    }

    /// Validates the exact length and returns the original borrowed slice.
    ///
    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::Exact(expected))?;
        Ok(self)
    }

    /// Validates the minimum length and returns the original borrowed slice.
    ///
    /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::AtLeast(min))?;
        Ok(self)
    }

    /// Validates the maximum length and returns the original borrowed slice.
    ///
    /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::AtMost(max))?;
        Ok(self)
    }

    /// Validates an inclusive length range and returns the borrowed slice.
    ///
    /// A reversed range returns
    /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
    /// out-of-range length returns [`ArgumentErrorKind::Length`].
    #[inline]
    fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
        validate_length(path, self.len(), LengthConstraint::InRange { min, max })?;
        Ok(self)
    }
}

impl<T, const N: usize> CollectionArgument for [T; N] {
    /// Validates non-emptiness and returns the original array.
    ///
    /// A zero-length array returns [`ArgumentErrorKind::Empty`] at `path`.
    #[inline]
    fn require_non_empty(self, path: &str) -> ArgumentResult<Self> {
        if N == 0 {
            return Err(ArgumentError::new(path, ArgumentErrorKind::Empty));
        }
        Ok(self)
    }

    /// Validates the exact length and returns the original array.
    ///
    /// A mismatch returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len(self, path: &str, expected: usize) -> ArgumentResult<Self> {
        validate_length(path, N, LengthConstraint::Exact(expected))?;
        Ok(self)
    }

    /// Validates the minimum length and returns the original array.
    ///
    /// A length below `min` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_least(self, path: &str, min: usize) -> ArgumentResult<Self> {
        validate_length(path, N, LengthConstraint::AtLeast(min))?;
        Ok(self)
    }

    /// Validates the maximum length and returns the original array.
    ///
    /// A length above `max` returns [`ArgumentErrorKind::Length`] at `path`.
    #[inline]
    fn require_len_at_most(self, path: &str, max: usize) -> ArgumentResult<Self> {
        validate_length(path, N, LengthConstraint::AtMost(max))?;
        Ok(self)
    }

    /// Validates an inclusive length range and returns the original array.
    ///
    /// A reversed range returns
    /// [`ArgumentErrorKind::InvalidLengthConstraint`] at `path`; an
    /// out-of-range length returns [`ArgumentErrorKind::Length`].
    #[inline]
    fn require_len_in(self, path: &str, min: usize, max: usize) -> ArgumentResult<Self> {
        validate_length(path, N, LengthConstraint::InRange { min, max })?;
        Ok(self)
    }
}

/// Validates an observed collection length against a structured constraint.
///
/// # Parameters
///
/// - `path`: identifies the collection in a validation error.
/// - `actual`: the observed number of elements.
/// - `constraint`: the required relationship between length and element count.
///
/// # Returns
///
/// Returns `Ok(())` when the constraint is satisfied.
///
/// # Errors
///
/// A reversed inclusive range returns
/// [`ArgumentErrorKind::InvalidLengthConstraint`] before `actual` is checked.
/// Any other unsatisfied constraint returns [`ArgumentErrorKind::Length`].
fn validate_length(path: &str, actual: usize, constraint: LengthConstraint) -> ArgumentResult<()> {
    if let LengthConstraint::InRange { min, max } = &constraint
        && min > max
    {
        return Err(ArgumentError::new(
            path,
            ArgumentErrorKind::InvalidLengthConstraint {
                constraint,
                metric: LengthMetric::Elements,
            },
        ));
    }

    let is_valid = match &constraint {
        LengthConstraint::Exact(expected) => actual == *expected,
        LengthConstraint::AtLeast(min) => actual >= *min,
        LengthConstraint::AtMost(max) => actual <= *max,
        LengthConstraint::InRange { min, max } => actual >= *min && actual <= *max,
    };
    if is_valid {
        Ok(())
    } else {
        Err(ArgumentError::new(
            path,
            ArgumentErrorKind::Length {
                actual,
                constraint,
                metric: LengthMetric::Elements,
            },
        ))
    }
}