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    #[must_use]
50    #[inline]
51    pub fn new(values: &'a [ValidationValue<'a>]) -> Self {
52        Self { values, paths: None }
53    }
54
55    /// Creates a context with caller-supplied dependency paths.
56    ///
57    /// Paths are associated with values by slot index and kept unchanged. The
58    /// caller chooses their coordinate system; this method does not add a
59    /// current-target prefix.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use qubit_validator::{BoundValidationContext, ValidationPath, ValidationValue};
65    ///
66    /// let values = [ValidationValue::Text("ada@example.test")];
67    /// let paths = [ValidationPath::root().with_field("profile").with_field("email")];
68    /// let context = BoundValidationContext::new_with_paths(&values, &paths)?;
69    /// assert_eq!(context.dependency_path(0)?.render(), "profile.email");
70    /// # Ok::<(), qubit_validator::ExecutionError>(())
71    /// ```
72    ///
73    /// # Errors
74    ///
75    /// Returns an adapter contract error when the two slices differ in length.
76    ///
77    /// # Returns
78    ///
79    /// Returns a context whose values and paths occupy matching slots.
80    pub fn new_with_paths(
81        values: &'a [ValidationValue<'a>],
82        paths: &'a [ValidationPath],
83    ) -> Result<Self, ExecutionError> {
84        if values.len() != paths.len() {
85            return Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation));
86        }
87        Ok(Self {
88            values,
89            paths: Some(paths),
90        })
91    }
92
93    /// Returns the value at an ordered dependency slot.
94    ///
95    /// # Errors
96    ///
97    /// Returns an adapter contract error for an invalid slot index.
98    ///
99    /// # Returns
100    ///
101    /// Returns a copy of the value stored at `index`.
102    #[inline]
103    pub fn value(&self, index: usize) -> Result<ValidationValue<'a>, ExecutionError> {
104        self.values
105            .get(index)
106            .copied()
107            .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation))
108    }
109
110    /// Returns the path at an ordered dependency slot.
111    ///
112    /// # Errors
113    ///
114    /// Returns an adapter contract error for an invalid slot index.
115    ///
116    /// # Returns
117    ///
118    /// Returns the stored path unchanged, or the root path when no paths were
119    /// supplied. The default root is only a fallback location and does not
120    /// establish that the dependency is the current target.
121    #[inline]
122    pub fn dependency_path(&self, index: usize) -> Result<&ValidationPath, ExecutionError> {
123        match self.paths {
124            Some(paths) => paths
125                .get(index)
126                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation)),
127            None if index < self.values.len() => Ok(&ROOT_PATH),
128            None => Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation)),
129        }
130    }
131
132    /// Downcasts a required dependency to its exact type.
133    ///
134    /// # Type Parameters
135    ///
136    /// - `T`: Concrete dependency type to borrow; it must be `'static` for
137    ///   `Any` downcasting.
138    ///
139    /// # Errors
140    ///
141    /// Returns a shape, missing-value, or slot error.
142    ///
143    /// # Returns
144    ///
145    /// Returns a borrow of the value when it has concrete type `T`.
146    #[inline]
147    pub fn typed<T: 'static>(&self, index: usize) -> Result<&'a T, ExecutionError> {
148        match self.value(index)? {
149            ValidationValue::Typed(value) => value
150                .downcast_ref::<T>()
151                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
152            ValidationValue::Missing => Err(ExecutionError::new(ExecutionErrorKind::MissingRequiredDependencyValue)),
153            ValidationValue::Text(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
154        }
155    }
156
157    /// Downcasts an optional dependency, treating only `Missing` as `None`.
158    ///
159    /// # Type Parameters
160    ///
161    /// - `T`: Concrete dependency type to borrow; it must be `'static` for
162    ///   `Any` downcasting.
163    ///
164    /// # Returns
165    ///
166    /// Returns `Some` with a correctly typed present value, or `None` for the
167    /// explicit missing marker.
168    ///
169    /// # Errors
170    ///
171    /// Returns a shape or slot error. A wrong concrete type is never treated
172    /// as an absent optional value.
173    #[inline]
174    pub fn optional_typed<T: 'static>(&self, index: usize) -> Result<Option<&'a T>, ExecutionError> {
175        match self.value(index)? {
176            ValidationValue::Missing => Ok(None),
177            ValidationValue::Typed(value) => value
178                .downcast_ref::<T>()
179                .map(Some)
180                .ok_or_else(|| ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
181            ValidationValue::Text(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
182        }
183    }
184
185    /// Borrows a required text dependency.
186    ///
187    /// # Errors
188    ///
189    /// Returns a shape, missing-value, or slot error.
190    ///
191    /// # Returns
192    ///
193    /// Returns the borrowed text dependency.
194    #[inline]
195    pub fn text(&self, index: usize) -> Result<&'a str, ExecutionError> {
196        match self.value(index)? {
197            ValidationValue::Text(value) => Ok(value),
198            ValidationValue::Missing => Err(ExecutionError::new(ExecutionErrorKind::MissingRequiredDependencyValue)),
199            ValidationValue::Typed(_) => Err(ExecutionError::new(ExecutionErrorKind::DependencyTypeMismatch)),
200        }
201    }
202
203    /// Checks all bound values against their declared dependency slots.
204    ///
205    /// # Errors
206    ///
207    /// Returns the first shape, missing-value, or adapter contract error in
208    /// signature order. Shape errors retain the supplied slot path, or its
209    /// default root path. Error metadata does not include raw dependency
210    /// values.
211    ///
212    /// # Returns
213    ///
214    /// Returns `()` when every slot satisfies its declared dependency.
215    pub(crate) fn check_specs(&self, specs: &[DependencySpec]) -> Result<(), ExecutionError> {
216        if self.values.len() != specs.len() || self.paths.is_some_and(|paths| paths.len() != specs.len()) {
217            return Err(ExecutionError::new(ExecutionErrorKind::AdapterContractViolation));
218        }
219        for (index, spec) in specs.iter().copied().enumerate() {
220            let value = self.value(index)?;
221            let error_kind = if value.is_missing() {
222                if !spec.optional() {
223                    ExecutionErrorKind::MissingRequiredDependencyValue
224                } else {
225                    continue;
226                }
227            } else if !spec.input().accepts(value) {
228                ExecutionErrorKind::DependencyTypeMismatch
229            } else {
230                continue;
231            };
232            let path = self.dependency_path(index)?.clone();
233            return Err(ExecutionError::new(error_kind)
234                .with_dependency(spec.name())
235                .with_path(path));
236        }
237        Ok(())
238    }
239}
240
241/// Shared root path used when an adapter does not supply dependency paths.
242static ROOT_PATH: ValidationPath = ValidationPath::root();
243
244impl Debug for BoundValidationContext<'_> {
245    /// Formats only the slot count so dependency values remain private.
246    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
247        formatter
248            .debug_struct("BoundValidationContext")
249            .field("slot_count", &self.values.len())
250            .finish()
251    }
252}