Skip to main content

qubit_validator/binding/
bound_validation_context.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
9//! Ordered, shape-checked dependency values for one validation call.
10
11use std::fmt::Debug;
12use std::fmt::Formatter;
13use std::fmt::Result as FmtResult;
14
15use super::DependencySpec;
16use super::ExecutionError;
17use super::ExecutionErrorKind;
18use super::ValidationPath;
19use super::ValidationValue;
20
21/// Borrowed dependency slots used during a synchronous validation call.
22///
23/// Optional paths are stored in slot order exactly as supplied by the caller.
24/// This type does not infer a coordinate system or add the current target's
25/// path. When paths are omitted, [`Self::dependency_path`] returns the root
26/// path as a default location; that does not identify the target or report
27/// root.
28///
29/// # Examples
30///
31/// ```
32/// use qubit_validator::{BoundValidationContext, ValidationValue};
33///
34/// let owner = String::from("member@example.test");
35/// let values = [ValidationValue::Text(&owner)];
36/// let context = BoundValidationContext::new(&values);
37/// assert_eq!(context.text(0)?, "member@example.test");
38/// # Ok::<(), qubit_validator::ExecutionError>(())
39/// ```
40pub struct BoundValidationContext<'a> {
41    /// Values stored in signature slot order.
42    values: &'a [ValidationValue<'a>],
43    /// Optional model paths stored in the same slot order as `values`.
44    paths: Option<&'a [ValidationPath]>,
45}
46
47impl<'a> BoundValidationContext<'a> {
48    /// Creates a context with root paths for each dependency slot.
49    ///
50    /// # Parameters
51    ///
52    /// - `values`: Dependency values in signature slot order.
53    #[must_use]
54    #[inline]
55    pub fn new(values: &'a [ValidationValue<'a>]) -> Self {
56        Self { values, paths: None }
57    }
58
59    /// Creates a context with caller-supplied dependency paths.
60    ///
61    /// Paths are associated with values by slot index and kept unchanged. The
62    /// caller chooses their coordinate system; this method does not add a
63    /// current-target prefix.
64    ///
65    /// # Parameters
66    ///
67    /// - `values`: Dependency values in signature slot order.
68    /// - `paths`: Paths aligned with `values` by slot index.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use qubit_validator::{BoundValidationContext, ValidationPath, ValidationValue};
74    ///
75    /// let values = [ValidationValue::Text("ada@example.test")];
76    /// let paths = [ValidationPath::root().with_field("profile").with_field("email")];
77    /// let context = BoundValidationContext::new_with_paths(&values, &paths)?;
78    /// assert_eq!(context.dependency_path(0)?.render(), "profile.email");
79    /// # Ok::<(), qubit_validator::ExecutionError>(())
80    /// ```
81    ///
82    /// # Errors
83    ///
84    /// Returns an adapter contract error when the two slices differ in length.
85    ///
86    /// # Returns
87    ///
88    /// Returns a context whose values and paths occupy matching slots.
89    pub fn new_with_paths(
90        values: &'a [ValidationValue<'a>],
91        paths: &'a [ValidationPath],
92    ) -> Result<Self, ExecutionError> {
93        if values.len() != paths.len() {
94            return Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation));
95        }
96        Ok(Self {
97            values,
98            paths: Some(paths),
99        })
100    }
101
102    /// Returns the value at an ordered dependency slot.
103    ///
104    /// # Errors
105    ///
106    /// Returns an adapter contract error for an invalid slot index.
107    ///
108    /// # Parameters
109    ///
110    /// - `index`: Zero-based dependency slot to read.
111    ///
112    /// # Returns
113    ///
114    /// Returns a copy of the value stored at `index`.
115    #[inline]
116    pub fn value(&self, index: usize) -> Result<ValidationValue<'a>, ExecutionError> {
117        self.values
118            .get(index)
119            .copied()
120            .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation))
121    }
122
123    /// Returns the path at an ordered dependency slot.
124    ///
125    /// # Errors
126    ///
127    /// Returns an adapter contract error for an invalid slot index.
128    ///
129    /// # Returns
130    ///
131    /// Returns the stored path unchanged, or the root path when no paths were
132    /// supplied. The default root is only a fallback location and does not
133    /// establish that the dependency is the current target.
134    ///
135    /// # Parameters
136    ///
137    /// - `index`: Zero-based dependency slot whose path is requested.
138    #[inline]
139    pub fn dependency_path(&self, index: usize) -> Result<&ValidationPath, ExecutionError> {
140        match self.paths {
141            Some(paths) => paths
142                .get(index)
143                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation)),
144            None if index < self.values.len() => Ok(&ROOT_PATH),
145            None => Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation)),
146        }
147    }
148
149    /// Downcasts a required dependency to its exact type.
150    ///
151    /// # Type Parameters
152    ///
153    /// - `T`: Concrete dependency type to borrow; it must be `'static` for
154    ///   `Any` downcasting.
155    ///
156    /// # Parameters
157    ///
158    /// - `index`: Zero-based dependency slot to downcast.
159    ///
160    /// # Errors
161    ///
162    /// Returns a shape, missing-value, or slot error.
163    ///
164    /// # Returns
165    ///
166    /// Returns a borrow of the value when it has concrete type `T`.
167    #[inline]
168    pub fn typed<T: 'static>(&self, index: usize) -> Result<&'a T, ExecutionError> {
169        match self.value(index)? {
170            ValidationValue::Typed(value) => value
171                .downcast_ref::<T>()
172                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
173            ValidationValue::Missing => Err(ExecutionError::new(ExecutionErrorKind::MissingRequiredDependencyValue)),
174            ValidationValue::Text(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
175        }
176    }
177
178    /// Downcasts an optional dependency, treating only `Missing` as `None`.
179    ///
180    /// # Type Parameters
181    ///
182    /// - `T`: Concrete dependency type to borrow; it must be `'static` for
183    ///   `Any` downcasting.
184    ///
185    /// # Returns
186    ///
187    /// Returns `Some` with a correctly typed present value, or `None` for the
188    /// explicit missing marker.
189    ///
190    /// # Errors
191    ///
192    /// Returns a shape or slot error. A wrong concrete type is never treated
193    /// as an absent optional value.
194    ///
195    /// # Parameters
196    ///
197    /// - `index`: Zero-based optional dependency slot to downcast.
198    #[inline]
199    pub fn optional_typed<T: 'static>(&self, index: usize) -> Result<Option<&'a T>, ExecutionError> {
200        match self.value(index)? {
201            ValidationValue::Missing => Ok(None),
202            ValidationValue::Typed(value) => value
203                .downcast_ref::<T>()
204                .map(Some)
205                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
206            ValidationValue::Text(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
207        }
208    }
209
210    /// Borrows a required text dependency.
211    ///
212    /// # Errors
213    ///
214    /// Returns a shape, missing-value, or slot error.
215    ///
216    /// # Returns
217    ///
218    /// Returns the borrowed text dependency.
219    ///
220    /// # Parameters
221    ///
222    /// - `index`: Zero-based dependency slot to borrow as text.
223    #[inline]
224    pub fn text(&self, index: usize) -> Result<&'a str, ExecutionError> {
225        match self.value(index)? {
226            ValidationValue::Text(value) => Ok(value),
227            ValidationValue::Missing => Err(ExecutionError::new(ExecutionErrorKind::MissingRequiredDependencyValue)),
228            ValidationValue::Typed(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
229        }
230    }
231
232    /// Checks all bound values against their declared dependency slots.
233    ///
234    /// # Errors
235    ///
236    /// Returns the first shape, missing-value, or adapter contract error in
237    /// signature order. Shape errors retain the supplied slot path, or its
238    /// default root path. Error metadata does not include raw dependency
239    /// values.
240    ///
241    /// # Returns
242    ///
243    /// Returns `()` when every slot satisfies its declared dependency.
244    ///
245    /// # Parameters
246    ///
247    /// - `specs`: Dependency declarations in the same order as the bound slots.
248    pub(crate) fn check_specs(&self, specs: &[DependencySpec]) -> Result<(), ExecutionError> {
249        if self.values.len() != specs.len() || self.paths.is_some_and(|paths| paths.len() != specs.len()) {
250            return Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation));
251        }
252        for (index, spec) in specs.iter().copied().enumerate() {
253            let value = self.value(index)?;
254            let error_kind = if value.is_missing() {
255                if !spec.optional() {
256                    ExecutionErrorKind::MissingRequiredDependencyValue
257                } else {
258                    continue;
259                }
260            } else if !spec.input().accepts(value) {
261                ExecutionErrorKind::DependencyTypeMismatch
262            } else {
263                continue;
264            };
265            let path = self.dependency_path(index)?.clone();
266            return Err(ExecutionError::new(error_kind)
267                .with_dependency(spec.name())
268                .with_path(path));
269        }
270        Ok(())
271    }
272}
273
274/// Shared root path used when an adapter does not supply dependency paths.
275static ROOT_PATH: ValidationPath = ValidationPath::root();
276
277impl Debug for BoundValidationContext<'_> {
278    /// Formats only the slot count so dependency values remain private.
279    ///
280    /// # Parameters
281    ///
282    /// - `formatter`: Destination formatter receiving the redacted
283    ///   representation.
284    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
285        formatter
286            .debug_struct("BoundValidationContext")
287            .field("slot_count", &self.values.len())
288            .finish()
289    }
290}