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 pub(crate) fn new(
140 prepared: Arc<dyn PreparedValidator>,
141 signature: ValidatorSignature,
142 rule_id: ValidatorId,
143 ) -> Self {
144 Self {
145 prepared,
146 input: signature.input(),
147 dependencies: signature.dependencies(),
148 rule_id,
149 }
150 }
151
152 /// Validates one value after checking its erased input and dependencies.
153 ///
154 /// Dependency values and paths must already follow
155 /// [`Self::dependency_specs`] order. Use [`Self::validate_named`] for
156 /// directly assembled dependency sets or whenever same-typed dependencies
157 /// could be supplied in the wrong order.
158 ///
159 /// # Parameters
160 /// - `value`: Borrowed target value.
161 /// - `context`: Dependency values and paths in signature order.
162 ///
163 /// # Returns
164 /// The bound validation outcome.
165 ///
166 /// # Errors
167 ///
168 /// Returns a shape, dependency, adapter, or rule execution error.
169 pub fn validate(
170 &self,
171 value: ValidationValue<'_>,
172 context: &BoundValidationContext<'_>,
173 ) -> Result<ValidationOutcome, ExecutionError> {
174 self.check_input(value)?;
175 self.check_dependencies(context)?;
176 let outcome = self
177 .prepared
178 .validate(value, context)
179 .map_err(|error| error.with_rule(self.rule_id))?;
180 outcome
181 .into_bound(self.rule_id)
182 .map_err(|_| ExecutionError::new(ExecutionErrorKind::AdapterContractViolation).with_rule(self.rule_id))
183 }
184
185 /// Validates a value after resolving dependency values by signature name.
186 ///
187 /// This entry point protects direct callers from swapping dependencies
188 /// that have the same input type. Values are reordered into signature slot
189 /// order for this call. Use [`Self::validate`] when a caller has already
190 /// checked and constructed values in that order. Zero or one dependency
191 /// needs no reordering container; two to eight use fixed-size stack
192 /// buffers, while more than eight allocate vectors as needed. Cloning a
193 /// supplied non-root path may still allocate.
194 ///
195 /// # Parameters
196 /// - `value`: Borrowed target value.
197 /// - `dependencies`: Borrowed dependency values keyed by signature name.
198 /// Every declared slot must be present; optional values can use
199 /// [`ValidationValue::Missing`].
200 ///
201 /// # Returns
202 /// The bound validation outcome.
203 ///
204 /// # Errors
205 ///
206 /// Returns a named-binding, input-shape, dependency-shape, adapter, or rule
207 /// execution error.
208 pub fn validate_named(
209 &self,
210 value: ValidationValue<'_>,
211 dependencies: &[NamedValidationDependency<'_>],
212 ) -> Result<ValidationOutcome, ExecutionError> {
213 if self.dependencies.len() <= 1 {
214 for (index, dependency) in dependencies.iter().enumerate() {
215 if dependencies[..index]
216 .iter()
217 .any(|previous| previous.name == dependency.name)
218 {
219 return Err(ExecutionError::new(ExecutionErrorKind::DuplicateDependencyBinding)
220 .with_rule(self.rule_id)
221 .with_dependency(dependency.name));
222 }
223 if !self.dependencies.iter().any(|spec| spec.name() == dependency.name) {
224 return Err(ExecutionError::new(ExecutionErrorKind::UnknownDependencyBinding)
225 .with_rule(self.rule_id)
226 .with_dependency(dependency.name));
227 }
228 }
229 }
230
231 if self.dependencies.is_empty() {
232 return self.validate(value, &BoundValidationContext::new(&[]));
233 }
234 if self.dependencies.len() == 1 {
235 let spec = self.dependencies[0];
236 let dependency = dependencies
237 .iter()
238 .find(|dependency| dependency.name == spec.name())
239 .ok_or_else(|| {
240 ExecutionError::new(ExecutionErrorKind::MissingDependencyBinding)
241 .with_rule(self.rule_id)
242 .with_dependency(spec.name())
243 })?;
244 let values = [dependency.value];
245 return match dependency.path {
246 Some(path) => {
247 let paths = [path.clone()];
248 let context = BoundValidationContext::new_with_paths(&values, &paths)
249 .map_err(|error| error.with_rule(self.rule_id))?;
250 self.validate(value, &context)
251 }
252 None => self.validate(value, &BoundValidationContext::new(&values)),
253 };
254 }
255
256 self.validate_named_many(value, dependencies)
257 }
258
259 /// Returns the selected input shape.
260 #[must_use]
261 #[inline]
262 pub const fn input_type(&self) -> InputType {
263 self.input
264 }
265
266 /// Returns dependencies in their execution slot order.
267 #[must_use]
268 #[inline]
269 pub const fn dependency_specs(&self) -> &'static [super::DependencySpec] {
270 self.dependencies
271 }
272
273 /// Returns the stable rule identifier.
274 #[must_use]
275 #[inline]
276 pub const fn rule_id(&self) -> ValidatorId {
277 self.rule_id
278 }
279
280 /// Resolves multiple named dependencies into signature order before
281 /// validation.
282 ///
283 /// Small dependency sets use fixed-size stack buffers; larger sets allocate
284 /// vectors. Missing, duplicate, and unknown bindings are reported before
285 /// the prepared validator runs.
286 fn validate_named_many(
287 &self,
288 value: ValidationValue<'_>,
289 dependencies: &[NamedValidationDependency<'_>],
290 ) -> Result<ValidationOutcome, ExecutionError> {
291 const INLINE_SLOTS: usize = 8;
292 let mut inline_slots: [Option<NamedValidationDependency<'_>>; INLINE_SLOTS] = [None; INLINE_SLOTS];
293 let mut heap_slots = Vec::new();
294 let slots: &mut [Option<NamedValidationDependency<'_>>] = if self.dependencies.len() <= INLINE_SLOTS {
295 &mut inline_slots[..self.dependencies.len()]
296 } else {
297 heap_slots.resize(self.dependencies.len(), None);
298 heap_slots.as_mut_slice()
299 };
300 let mut has_path = false;
301 for dependency in dependencies.iter().copied() {
302 let Some(index) = self.dependencies.iter().position(|spec| spec.name() == dependency.name) else {
303 return Err(ExecutionError::new(ExecutionErrorKind::UnknownDependencyBinding)
304 .with_rule(self.rule_id)
305 .with_dependency(dependency.name));
306 };
307 if slots[index].replace(dependency).is_some() {
308 return Err(ExecutionError::new(ExecutionErrorKind::DuplicateDependencyBinding)
309 .with_rule(self.rule_id)
310 .with_dependency(dependency.name));
311 }
312 has_path |= dependency.path.is_some();
313 }
314 for (index, slot) in slots.iter().enumerate() {
315 if slot.is_none() {
316 return Err(ExecutionError::new(ExecutionErrorKind::MissingDependencyBinding)
317 .with_rule(self.rule_id)
318 .with_dependency(self.dependencies[index].name()));
319 }
320 }
321 if self.dependencies.len() <= INLINE_SLOTS {
322 let mut values = [ValidationValue::Missing; INLINE_SLOTS];
323 for (index, slot) in slots.iter().enumerate() {
324 values[index] = slot.as_ref().expect("checked slot").value;
325 }
326 let values = &values[..self.dependencies.len()];
327 if !has_path {
328 return self.validate(value, &BoundValidationContext::new(values));
329 }
330 let mut paths: [ValidationPath; INLINE_SLOTS] = std::array::from_fn(|_| ValidationPath::root());
331 for (index, slot) in slots.iter().enumerate() {
332 if let Some(path) = slot.as_ref().expect("checked slot").path {
333 paths[index] = path.clone();
334 }
335 }
336 let context = BoundValidationContext::new_with_paths(values, &paths[..self.dependencies.len()])
337 .map_err(|error| error.with_rule(self.rule_id))?;
338 return self.validate(value, &context);
339 }
340 let values: Vec<_> = slots
341 .iter()
342 .map(|slot| slot.as_ref().expect("checked slot").value)
343 .collect();
344 if !has_path {
345 return self.validate(value, &BoundValidationContext::new(&values));
346 }
347 let paths: Vec<_> = slots
348 .iter()
349 .map(|slot| {
350 slot.as_ref()
351 .expect("checked slot")
352 .path
353 .cloned()
354 .unwrap_or_else(ValidationPath::root)
355 })
356 .collect();
357 let context =
358 BoundValidationContext::new_with_paths(&values, &paths).map_err(|error| error.with_rule(self.rule_id))?;
359 self.validate(value, &context)
360 }
361
362 /// Checks the erased input against the selected signature.
363 fn check_input(&self, value: ValidationValue<'_>) -> Result<(), ExecutionError> {
364 if value.is_missing() || !self.input.accepts(value) {
365 return Err(ExecutionError::new(ExecutionErrorKind::InputTypeMismatch).with_rule(self.rule_id));
366 }
367 Ok(())
368 }
369
370 /// Checks dependency values against the selected signature.
371 fn check_dependencies(&self, context: &BoundValidationContext<'_>) -> Result<(), ExecutionError> {
372 context
373 .check_specs(self.dependencies)
374 .map_err(|error| error.with_rule(self.rule_id))
375 }
376}
377
378impl fmt::Debug for BoundValidator {
379 /// Formats structural metadata without exposing the prepared
380 /// implementation.
381 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
382 formatter
383 .debug_struct("BoundValidator")
384 .field("rule_id", &self.rule_id)
385 .field("input", &self.input)
386 .finish_non_exhaustive()
387 }
388}