qubit_validator/binding/bound_validator.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//! Configured validator occurrences.
10
11use std::fmt;
12use std::sync::Arc;
13
14use super::BindError;
15use super::BindErrorKind;
16use super::BoundValidationContext;
17use super::DependencySpec;
18use super::ExecutionError;
19use super::ExecutionErrorKind;
20use super::InputType;
21use super::PreparedValidator;
22use super::ValidationOutcome;
23use super::ValidationValue;
24use super::ValidatorSignature;
25use crate::NamedValidationDependency;
26use crate::ValidationPath;
27use crate::ValidatorId;
28
29/// A bound occurrence that owns a reusable prepared validator instance.
30///
31/// A prepared validator reports only whether its execution was valid or
32/// invalid. Callers decide whether an occurrence should be skipped and build
33/// the corresponding [`ValidationOutcome`] themselves.
34///
35/// # Examples
36///
37/// ```
38/// use std::sync::Arc;
39///
40/// use qubit_validator::ArgumentReader;
41/// use qubit_validator::BindError;
42/// use qubit_validator::BoundValidationContext;
43/// use qubit_validator::ExecutionError;
44/// use qubit_validator::InputType;
45/// use qubit_validator::PreparedOutcome;
46/// use qubit_validator::PreparedValidator;
47/// use qubit_validator::ValidationOutcome;
48/// use qubit_validator::ValidationValue;
49/// use qubit_validator::ValidatorDescriptor;
50/// use qubit_validator::ValidatorId;
51/// use qubit_validator::ValidatorSignature;
52///
53/// struct AcceptAll;
54///
55/// impl PreparedValidator for AcceptAll {
56/// fn input_type(&self) -> InputType { InputType::Text }
57/// fn dependency_specs(&self) -> &'static [qubit_validator::DependencySpec] { &[] }
58/// fn validate(
59/// &self,
60/// _: ValidationValue<'_>,
61/// _: &BoundValidationContext<'_>,
62/// ) -> Result<PreparedOutcome, ExecutionError> {
63/// Ok(PreparedOutcome::Valid)
64/// }
65/// }
66///
67/// fn prepare(
68/// _reader: &mut ArgumentReader<'_>,
69/// ) -> Result<Arc<dyn PreparedValidator>, BindError> {
70/// Ok(Arc::new(AcceptAll))
71/// }
72///
73/// static SIGNATURES: &[ValidatorSignature] = &[
74/// ValidatorSignature::new(InputType::Text, &[], prepare),
75/// ];
76/// let descriptor = ValidatorDescriptor::new(SIGNATURES);
77/// let bound = descriptor.bind(ValidatorId::new("example.accept_all"), 0, &[])?;
78/// let outcome = bound.validate(
79/// ValidationValue::Text("accepted"),
80/// &BoundValidationContext::new(&[]),
81/// )?;
82/// assert_eq!(outcome, ValidationOutcome::Valid);
83/// # Ok::<(), Box<dyn std::error::Error>>(())
84/// ```
85#[derive(Clone)]
86pub struct BoundValidator {
87 /// Immutable prepared implementation shared by cloned bound occurrences.
88 prepared: Arc<dyn PreparedValidator>,
89 /// Erased input shape accepted by the prepared implementation.
90 input: InputType,
91 /// Ordered dependency slots required by the prepared implementation.
92 dependencies: &'static [DependencySpec],
93 /// Stable identifier attached to outcomes and errors.
94 rule_id: ValidatorId,
95}
96
97impl BoundValidator {
98 /// Creates a zero-dependency binding for an already prepared typed rule.
99 ///
100 /// The binding checks that calls supply exactly `T` and no dependency
101 /// slots before invoking the prepared validator. It does not perform
102 /// registry lookup or parameter decoding.
103 ///
104 /// # Type Parameters
105 ///
106 /// - `T`: Exact concrete value type accepted by the prepared validator.
107 ///
108 /// # Parameters
109 ///
110 /// - `rule_id`: Stable identity attached to violations and execution
111 /// errors.
112 /// - `prepared`: Immutable prepared implementation shared by clones.
113 ///
114 /// # Returns
115 ///
116 /// A reusable bound validator accepting values of type `T` without
117 /// dependencies.
118 ///
119 /// # Errors
120 ///
121 /// Returns `PreparedSignatureMismatch` with `rule_id` attached when the
122 /// prepared validator does not accept exactly `T` or declares dependencies.
123 pub fn try_from_prepared<T: 'static>(
124 rule_id: ValidatorId,
125 prepared: Arc<dyn PreparedValidator>,
126 ) -> Result<Self, BindError> {
127 if prepared.input_type() != InputType::of::<T>() || !prepared.dependency_specs().is_empty() {
128 return Err(BindError::new(BindErrorKind::PreparedSignatureMismatch).with_rule(rule_id));
129 }
130 Ok(Self {
131 prepared,
132 input: InputType::of::<T>(),
133 dependencies: &[],
134 rule_id,
135 })
136 }
137
138 /// Creates a bound occurrence from a prepared implementation and signature.
139 ///
140 /// # Parameters
141 ///
142 /// - `prepared`: Immutable implementation to invoke for this occurrence.
143 /// - `signature`: Input and dependency contract selected for the
144 /// occurrence.
145 /// - `rule_id`: Stable identifier attached to validation results and
146 /// errors.
147 #[must_use]
148 pub(crate) fn new(
149 prepared: Arc<dyn PreparedValidator>,
150 signature: ValidatorSignature,
151 rule_id: ValidatorId,
152 ) -> Self {
153 Self {
154 prepared,
155 input: signature.input(),
156 dependencies: signature.dependencies(),
157 rule_id,
158 }
159 }
160
161 /// Validates one value after checking its erased input and dependencies.
162 ///
163 /// Dependency values and paths must already follow
164 /// [`Self::dependency_specs`] order. Use [`Self::validate_named`] for
165 /// directly assembled dependency sets or whenever same-typed dependencies
166 /// could be supplied in the wrong order.
167 ///
168 /// # Parameters
169 /// - `value`: Borrowed target value.
170 /// - `context`: Dependency values and paths in signature order.
171 ///
172 /// # Returns
173 /// The bound validation outcome.
174 ///
175 /// # Errors
176 ///
177 /// Returns a shape, dependency, adapter, or rule execution error.
178 pub fn validate(
179 &self,
180 value: ValidationValue<'_>,
181 context: &BoundValidationContext<'_>,
182 ) -> Result<ValidationOutcome, ExecutionError> {
183 self.check_input(value)?;
184 self.check_dependencies(context)?;
185 let outcome = self
186 .prepared
187 .validate(value, context)
188 .map_err(|error| error.with_rule(self.rule_id))?;
189 outcome
190 .into_bound(self.rule_id)
191 .map_err(|_| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation).with_rule(self.rule_id))
192 }
193
194 /// Validates a value after resolving dependency values by signature name.
195 ///
196 /// This entry point protects direct callers from swapping dependencies
197 /// that have the same input type. Values are reordered into signature slot
198 /// order for this call. Use [`Self::validate`] when a caller has already
199 /// checked and constructed values in that order. Zero or one dependency
200 /// needs no reordering container; two to eight use fixed-size stack
201 /// buffers, while more than eight allocate vectors as needed. Cloning a
202 /// supplied non-root path may still allocate.
203 ///
204 /// # Parameters
205 /// - `value`: Borrowed target value.
206 /// - `dependencies`: Borrowed dependency values keyed by signature name.
207 /// Every declared slot must be present; optional values can use
208 /// [`ValidationValue::Missing`].
209 ///
210 /// # Returns
211 /// The bound validation outcome.
212 ///
213 /// # Errors
214 ///
215 /// Returns a named-binding, input-shape, dependency-shape, adapter, or rule
216 /// execution error.
217 pub fn validate_named(
218 &self,
219 value: ValidationValue<'_>,
220 dependencies: &[NamedValidationDependency<'_>],
221 ) -> Result<ValidationOutcome, ExecutionError> {
222 if self.dependencies.len() <= 1 {
223 for (index, dependency) in dependencies.iter().enumerate() {
224 if dependencies[..index]
225 .iter()
226 .any(|previous| previous.name == dependency.name)
227 {
228 return Err(ExecutionError::new(ExecutionErrorKind::DuplicateDependencyBinding)
229 .with_rule(self.rule_id)
230 .with_dependency(dependency.name));
231 }
232 if !self.dependencies.iter().any(|spec| spec.name() == dependency.name) {
233 return Err(ExecutionError::new(ExecutionErrorKind::UnknownDependencyBinding)
234 .with_rule(self.rule_id)
235 .with_dependency(dependency.name));
236 }
237 }
238 }
239
240 if self.dependencies.is_empty() {
241 return self.validate(value, &BoundValidationContext::new(&[]));
242 }
243 if self.dependencies.len() == 1 {
244 let spec = self.dependencies[0];
245 let dependency = dependencies
246 .iter()
247 .find(|dependency| dependency.name == spec.name())
248 .ok_or_else(|| {
249 ExecutionError::new(ExecutionErrorKind::MissingDependencyBinding)
250 .with_rule(self.rule_id)
251 .with_dependency(spec.name())
252 })?;
253 let values = [dependency.value];
254 return match dependency.path {
255 Some(path) => {
256 let paths = [path.clone()];
257 let context = BoundValidationContext::new_with_paths(&values, &paths)
258 .map_err(|error| error.with_rule(self.rule_id))?;
259 self.validate(value, &context)
260 }
261 None => self.validate(value, &BoundValidationContext::new(&values)),
262 };
263 }
264
265 self.validate_named_many(value, dependencies)
266 }
267
268 /// Returns the selected input shape.
269 #[must_use]
270 #[inline]
271 pub const fn input_type(&self) -> InputType {
272 self.input
273 }
274
275 /// Returns dependencies in their execution slot order.
276 #[must_use]
277 #[inline]
278 pub const fn dependency_specs(&self) -> &'static [super::DependencySpec] {
279 self.dependencies
280 }
281
282 /// Returns the stable rule identifier.
283 #[must_use]
284 #[inline]
285 pub const fn rule_id(&self) -> ValidatorId {
286 self.rule_id
287 }
288
289 /// Resolves multiple named dependencies into signature order before
290 /// validation.
291 ///
292 /// Small dependency sets use fixed-size stack buffers; larger sets allocate
293 /// vectors. Missing, duplicate, and unknown bindings are reported before
294 /// the prepared validator runs.
295 ///
296 /// # Parameters
297 ///
298 /// - `value`: Borrowed target value to validate.
299 /// - `dependencies`: Borrowed dependency values keyed by signature name.
300 fn validate_named_many(
301 &self,
302 value: ValidationValue<'_>,
303 dependencies: &[NamedValidationDependency<'_>],
304 ) -> Result<ValidationOutcome, ExecutionError> {
305 const INLINE_SLOTS: usize = 8;
306 let mut inline_slots: [Option<NamedValidationDependency<'_>>; INLINE_SLOTS] = [None; INLINE_SLOTS];
307 let mut heap_slots = Vec::new();
308 let slots: &mut [Option<NamedValidationDependency<'_>>] = if self.dependencies.len() <= INLINE_SLOTS {
309 &mut inline_slots[..self.dependencies.len()]
310 } else {
311 heap_slots.resize(self.dependencies.len(), None);
312 heap_slots.as_mut_slice()
313 };
314 let mut has_path = false;
315 for dependency in dependencies.iter().copied() {
316 let Some(index) = self.dependencies.iter().position(|spec| spec.name() == dependency.name) else {
317 return Err(ExecutionError::new(ExecutionErrorKind::UnknownDependencyBinding)
318 .with_rule(self.rule_id)
319 .with_dependency(dependency.name));
320 };
321 if slots[index].replace(dependency).is_some() {
322 return Err(ExecutionError::new(ExecutionErrorKind::DuplicateDependencyBinding)
323 .with_rule(self.rule_id)
324 .with_dependency(dependency.name));
325 }
326 has_path |= dependency.path.is_some();
327 }
328 for (index, slot) in slots.iter().enumerate() {
329 if slot.is_none() {
330 return Err(ExecutionError::new(ExecutionErrorKind::MissingDependencyBinding)
331 .with_rule(self.rule_id)
332 .with_dependency(self.dependencies[index].name()));
333 }
334 }
335 if self.dependencies.len() <= INLINE_SLOTS {
336 let mut values = [ValidationValue::Missing; INLINE_SLOTS];
337 for (index, slot) in slots.iter().enumerate() {
338 values[index] = slot.as_ref().expect("checked slot").value;
339 }
340 let values = &values[..self.dependencies.len()];
341 if !has_path {
342 return self.validate(value, &BoundValidationContext::new(values));
343 }
344 let mut paths: [ValidationPath; INLINE_SLOTS] = std::array::from_fn(|_| ValidationPath::root());
345 for (index, slot) in slots.iter().enumerate() {
346 if let Some(path) = slot.as_ref().expect("checked slot").path {
347 paths[index] = path.clone();
348 }
349 }
350 let context = BoundValidationContext::new_with_paths(values, &paths[..self.dependencies.len()])
351 .map_err(|error| error.with_rule(self.rule_id))?;
352 return self.validate(value, &context);
353 }
354 let values: Vec<_> = slots
355 .iter()
356 .map(|slot| slot.as_ref().expect("checked slot").value)
357 .collect();
358 if !has_path {
359 return self.validate(value, &BoundValidationContext::new(&values));
360 }
361 let paths: Vec<_> = slots
362 .iter()
363 .map(|slot| {
364 slot.as_ref()
365 .expect("checked slot")
366 .path
367 .cloned()
368 .unwrap_or_else(ValidationPath::root)
369 })
370 .collect();
371 let context =
372 BoundValidationContext::new_with_paths(&values, &paths).map_err(|error| error.with_rule(self.rule_id))?;
373 self.validate(value, &context)
374 }
375
376 /// Checks the erased input against the selected signature.
377 ///
378 /// # Parameters
379 ///
380 /// - `value`: Borrowed value whose presence and input shape are checked.
381 fn check_input(&self, value: ValidationValue<'_>) -> Result<(), ExecutionError> {
382 if value.is_missing() || !self.input.accepts(value) {
383 return Err(ExecutionError::new(ExecutionErrorKind::InputTypeMismatch).with_rule(self.rule_id));
384 }
385 Ok(())
386 }
387
388 /// Checks dependency values against the selected signature.
389 ///
390 /// # Parameters
391 ///
392 /// - `context`: Borrowed dependency context checked against the signature.
393 fn check_dependencies(&self, context: &BoundValidationContext<'_>) -> Result<(), ExecutionError> {
394 context
395 .check_specs(self.dependencies)
396 .map_err(|error| error.with_rule(self.rule_id))
397 }
398}
399
400impl fmt::Debug for BoundValidator {
401 /// Formats structural metadata without exposing the prepared
402 /// implementation.
403 ///
404 /// # Parameters
405 ///
406 /// - `formatter`: Destination that receives the debug representation.
407 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
408 formatter
409 .debug_struct("BoundValidator")
410 .field("rule_id", &self.rule_id)
411 .field("input", &self.input)
412 .finish_non_exhaustive()
413 }
414}