Skip to main content

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}