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    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}