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}