qubit_argument/argument/bounds.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Bounds and caller-defined argument validation.
9
10use std::ops::Range;
11
12use crate::argument::{
13 ArgumentError,
14 ArgumentErrorKind,
15 ArgumentResult,
16 IndexRole,
17};
18
19/// Validates a value with a caller-provided predicate.
20///
21/// The predicate receives a shared reference to `value`. If it returns `true`,
22/// this function returns the original value without cloning it or allocating
23/// error strings. If it returns `false`, `path`, `code`, and `message` identify
24/// the failed validation rule.
25///
26/// # Errors
27///
28/// Returns [`ArgumentErrorKind::Custom`] when `predicate` returns `false`.
29/// The error owns copies of `path`, `code`, and `message`.
30#[inline]
31pub fn require_that<T, F>(
32 value: T,
33 path: &str,
34 predicate: F,
35 code: &str,
36 message: &str,
37) -> ArgumentResult<T>
38where
39 F: FnOnce(&T) -> bool,
40{
41 if predicate(&value) {
42 Ok(value)
43 } else {
44 Err(ArgumentError::new(
45 path,
46 ArgumentErrorKind::Custom {
47 code: String::from(code),
48 message: String::from(message),
49 },
50 ))
51 }
52}
53
54/// Validates that an offset and length fit within a total length.
55///
56/// `offset` may equal `total_length` only when `length` is zero. Validation
57/// checks the offset before subtracting it from the total, so it never computes
58/// an unchecked sum of the offset and length.
59///
60/// # Errors
61///
62/// Returns [`ArgumentErrorKind::Bounds`] when `offset` exceeds `total_length`
63/// or when `length` exceeds the number of units remaining after `offset`.
64#[inline]
65pub fn check_bounds(
66 path: &str,
67 offset: usize,
68 length: usize,
69 total_length: usize,
70) -> ArgumentResult<()> {
71 if offset > total_length {
72 return Err(ArgumentError::new(
73 path,
74 ArgumentErrorKind::Bounds {
75 offset,
76 length,
77 total_length,
78 },
79 ));
80 }
81 if length > total_length - offset {
82 return Err(ArgumentError::new(
83 path,
84 ArgumentErrorKind::Bounds {
85 offset,
86 length,
87 total_length,
88 },
89 ));
90 }
91 Ok(())
92}
93
94/// Validates an index that must identify an existing element.
95///
96/// `index` is valid exactly when it is strictly less than `size`. On success,
97/// the validated index is returned unchanged.
98///
99/// # Errors
100///
101/// Returns [`ArgumentErrorKind::Index`] with [`IndexRole::Element`] when
102/// `index` is greater than or equal to `size`.
103#[inline]
104pub fn check_element_index(
105 path: &str,
106 index: usize,
107 size: usize,
108) -> ArgumentResult<usize> {
109 if index >= size {
110 return Err(ArgumentError::new(
111 path,
112 ArgumentErrorKind::Index {
113 index,
114 size,
115 role: IndexRole::Element,
116 },
117 ));
118 }
119 Ok(index)
120}
121
122/// Validates an index that identifies a boundary position.
123///
124/// `index` is valid when it is less than or equal to `size`, including the
125/// position immediately after the final element. On success, the validated
126/// index is returned unchanged.
127///
128/// # Errors
129///
130/// Returns [`ArgumentErrorKind::Index`] with [`IndexRole::Position`] when
131/// `index` exceeds `size`.
132#[inline]
133pub fn check_position_index(
134 path: &str,
135 index: usize,
136 size: usize,
137) -> ArgumentResult<usize> {
138 if index > size {
139 return Err(ArgumentError::new(
140 path,
141 ArgumentErrorKind::Index {
142 index,
143 size,
144 role: IndexRole::Position,
145 },
146 ));
147 }
148 Ok(index)
149}
150
151/// Validates a half-open range of boundary positions.
152///
153/// `start` and `end` are valid when `start <= end <= size`. Equal endpoints
154/// are accepted and produce an empty range. On success, this function returns
155/// the validated `start..end` range.
156///
157/// # Errors
158///
159/// Returns [`ArgumentErrorKind::IndexRange`] when `start` exceeds `end` or
160/// when `end` exceeds `size`.
161#[inline]
162pub fn check_position_range(
163 path: &str,
164 start: usize,
165 end: usize,
166 size: usize,
167) -> ArgumentResult<Range<usize>> {
168 if start > end {
169 return Err(ArgumentError::new(
170 path,
171 ArgumentErrorKind::IndexRange { start, end, size },
172 ));
173 }
174 if end > size {
175 return Err(ArgumentError::new(
176 path,
177 ArgumentErrorKind::IndexRange { start, end, size },
178 ));
179 }
180 Ok(start..end)
181}