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}