Skip to main content

qubit_validator/registry/validator_registry/
validator_registry.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//! Registry type and implementation for multi-signature validator definitions.
10
11#[cfg(feature = "inventory")]
12use std::sync::OnceLock;
13
14#[cfg(feature = "inventory")]
15use inventory::iter;
16
17use super::super::ValidatorRegistration;
18use crate::NamedValidationArgument;
19use crate::ValidatorRegistryError;
20use crate::binding::BindError;
21use crate::binding::BindErrorKind;
22use crate::binding::BoundValidator;
23use crate::binding::InputType;
24
25/// A deterministic local registry containing one definition per stable ID.
26///
27/// # Examples
28///
29/// ```
30/// use qubit_validator::ValidatorRegistry;
31///
32/// let registry = ValidatorRegistry::empty();
33/// assert!(registry.registrations().is_empty());
34/// assert!(registry.get("example.unknown").is_none());
35/// ```
36#[derive(Debug)]
37pub struct ValidatorRegistry {
38    /// Registrations sorted by stable identifier.
39    registrations: Box<[ValidatorRegistration]>,
40}
41
42impl ValidatorRegistry {
43    /// Builds a registry by taking ownership of registration values.
44    ///
45    /// # Type Parameters
46    ///
47    /// * `I` - An iterable whose items can be converted into registrations.
48    ///
49    /// # Parameters
50    ///
51    /// * `registrations` - Values to validate and store in the registry.
52    ///
53    /// # Errors
54    ///
55    /// Returns every source which declared a duplicated stable ID, or the
56    /// identifier and cause of the first invalid descriptor.
57    ///
58    /// # Returns
59    ///
60    /// Returns a registry containing the validated registrations sorted by ID.
61    pub fn from_registrations<I>(registrations: I) -> Result<Self, ValidatorRegistryError>
62    where
63        I: IntoIterator,
64        I::Item: Into<ValidatorRegistration>,
65    {
66        Self::build(registrations.into_iter().map(Into::into).collect())
67    }
68
69    /// Returns an empty registry.
70    ///
71    /// # Returns
72    ///
73    /// Returns a registry with no registrations.
74    #[must_use]
75    #[inline]
76    pub fn empty() -> Self {
77        Self {
78            registrations: Box::new([]),
79        }
80    }
81
82    /// Initializes and returns the process-wide inventory registry.
83    ///
84    /// # Errors
85    ///
86    /// Returns the cached error when linked registrations contain a duplicate
87    /// ID or an invalid descriptor. This method is available only with the
88    /// `inventory` feature.
89    ///
90    /// # Returns
91    ///
92    /// Returns the process-wide registry when initialization succeeds.
93    #[cfg(feature = "inventory")]
94    pub fn try_global() -> Result<&'static Self, ValidatorRegistryError> {
95        static REGISTRY: OnceLock<Result<ValidatorRegistry, ValidatorRegistryError>> = OnceLock::new();
96        match REGISTRY.get_or_init(|| {
97            let registrations = iter::<crate::ValidatorRegistrationFactory>
98                .into_iter()
99                .map(|factory| (factory.0)())
100                .collect();
101            Self::build(registrations)
102        }) {
103            Ok(registry) => Ok(registry),
104            Err(error) => Err(error.clone()),
105        }
106    }
107
108    /// Returns the process-wide inventory registry.
109    ///
110    /// # Panics
111    ///
112    /// Panics when linked registrations contain duplicate IDs or an invalid
113    /// descriptor. This method is available only with the `inventory` feature.
114    ///
115    /// # Returns
116    ///
117    /// Returns the initialized process-wide registry.
118    #[cfg(feature = "inventory")]
119    #[must_use]
120    pub fn global() -> &'static Self {
121        Self::try_global().unwrap_or_else(|error| panic!("invalid global validator registry: {error}"))
122    }
123
124    /// Finds a registration by stable ID.
125    ///
126    /// # Parameters
127    ///
128    /// * `id` - Stable identifier to look up.
129    ///
130    /// # Returns
131    ///
132    /// Returns `Some` for a registered ID and `None` when the ID is unknown.
133    #[inline]
134    pub fn get(&self, id: &str) -> Option<&ValidatorRegistration> {
135        self.registrations
136            .binary_search_by(|registration| registration.id().as_str().cmp(id))
137            .ok()
138            .and_then(|index| self.registrations.get(index))
139    }
140
141    /// Returns registrations sorted by stable ID.
142    ///
143    /// # Returns
144    ///
145    /// Returns a borrowed slice of all registrations in stable-ID order.
146    #[must_use]
147    #[inline]
148    pub fn registrations(&self) -> &[ValidatorRegistration] {
149        &self.registrations
150    }
151
152    /// Binds a rule by ID and input shape.
153    ///
154    /// # Parameters
155    ///
156    /// * `id` - Stable identifier of the registered rule to bind.
157    /// * `input` - Input shape that the bound rule must accept.
158    /// * `params` - Named arguments supplied to the rule.
159    ///
160    /// # Errors
161    ///
162    /// Returns `MissingRule` when `id` has no registration. This error has
163    /// no resolved rule ID; callers that need the requested text retain `id`.
164    /// Signature and preparation errors for a found rule carry its ID.
165    ///
166    /// # Returns
167    ///
168    /// Returns the bound validator when the registration exists and accepts
169    /// the supplied input and parameters.
170    pub fn bind(
171        &self,
172        id: &str,
173        input: InputType,
174        params: &[NamedValidationArgument<'_>],
175    ) -> Result<BoundValidator, BindError> {
176        let registration = self.get(id).ok_or_else(|| BindError::new(BindErrorKind::MissingRule))?;
177        registration
178            .descriptor()
179            .bind_for_validated(registration.id(), input, params)
180            .map_err(|error| error.with_rule(registration.id()))
181    }
182
183    /// Sorts and validates owned registrations before freezing the registry.
184    ///
185    /// # Parameters
186    ///
187    /// * `registrations` - Owned registrations to sort and validate.
188    ///
189    /// # Errors
190    ///
191    /// Returns duplicate-ID metadata or the first invalid descriptor.
192    ///
193    /// # Returns
194    ///
195    /// Returns a registry containing the validated registrations sorted by ID.
196    fn build(mut registrations: Vec<ValidatorRegistration>) -> Result<Self, ValidatorRegistryError> {
197        registrations.sort_by_key(|registration| (registration.id(), registration.source()));
198        let mut index = 0;
199        while index < registrations.len() {
200            let id = registrations[index].id();
201            let end = index
202                + registrations[index..]
203                    .iter()
204                    .take_while(|registration| registration.id() == id)
205                    .count();
206            if end - index > 1 {
207                let sources = registrations[index..end]
208                    .iter()
209                    .map(ValidatorRegistration::source)
210                    .collect();
211                return Err(ValidatorRegistryError::DuplicateId {
212                    id: id.as_str(),
213                    sources,
214                });
215            }
216            index = end;
217        }
218        for registration in &registrations {
219            if let Err(error) = registration.descriptor().validate_definition() {
220                return Err(ValidatorRegistryError::InvalidDescriptor {
221                    id: registration.id(),
222                    registration_source: registration.source(),
223                    kind: error.kind(),
224                    dependency: error.dependency().map(str::to_owned),
225                });
226            }
227        }
228        Ok(Self {
229            registrations: registrations.into_boxed_slice(),
230        })
231    }
232}