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    /// # Returns
127    ///
128    /// Returns `Some` for a registered ID and `None` when the ID is unknown.
129    #[inline]
130    pub fn get(&self, id: &str) -> Option<&ValidatorRegistration> {
131        self.registrations
132            .binary_search_by(|registration| registration.id().as_str().cmp(id))
133            .ok()
134            .and_then(|index| self.registrations.get(index))
135    }
136
137    /// Returns registrations sorted by stable ID.
138    ///
139    /// # Returns
140    ///
141    /// Returns a borrowed slice of all registrations in stable-ID order.
142    #[must_use]
143    #[inline]
144    pub fn registrations(&self) -> &[ValidatorRegistration] {
145        &self.registrations
146    }
147
148    /// Binds a rule by ID and input shape.
149    ///
150    /// # Parameters
151    ///
152    /// * `id` - Stable identifier of the registered rule to bind.
153    /// * `input` - Input shape that the bound rule must accept.
154    /// * `params` - Named arguments supplied to the rule.
155    ///
156    /// # Errors
157    ///
158    /// Returns `MissingRule` when `id` has no registration. This error has
159    /// no resolved rule ID; callers that need the requested text retain `id`.
160    /// Signature and preparation errors for a found rule carry its ID.
161    ///
162    /// # Returns
163    ///
164    /// Returns the bound validator when the registration exists and accepts
165    /// the supplied input and parameters.
166    pub fn bind(
167        &self,
168        id: &str,
169        input: InputType,
170        params: &[NamedValidationArgument<'_>],
171    ) -> Result<BoundValidator, BindError> {
172        let registration = self.get(id).ok_or_else(|| BindError::new(BindErrorKind::MissingRule))?;
173        registration
174            .descriptor()
175            .bind_for_validated(registration.id(), input, params)
176            .map_err(|error| error.with_rule(registration.id()))
177    }
178
179    /// Sorts and validates owned registrations before freezing the registry.
180    ///
181    /// # Parameters
182    ///
183    /// * `registrations` - Owned registrations to sort and validate.
184    ///
185    /// # Errors
186    ///
187    /// Returns duplicate-ID metadata or the first invalid descriptor.
188    ///
189    /// # Returns
190    ///
191    /// Returns a registry containing the validated registrations sorted by ID.
192    fn build(mut registrations: Vec<ValidatorRegistration>) -> Result<Self, ValidatorRegistryError> {
193        registrations.sort_by_key(|registration| (registration.id(), registration.source()));
194        let mut index = 0;
195        while index < registrations.len() {
196            let id = registrations[index].id();
197            let end = index
198                + registrations[index..]
199                    .iter()
200                    .take_while(|registration| registration.id() == id)
201                    .count();
202            if end - index > 1 {
203                let sources = registrations[index..end]
204                    .iter()
205                    .map(ValidatorRegistration::source)
206                    .collect();
207                return Err(ValidatorRegistryError::DuplicateId {
208                    id: id.as_str(),
209                    sources,
210                });
211            }
212            index = end;
213        }
214        for registration in &registrations {
215            if let Err(error) = registration.descriptor().validate_definition() {
216                return Err(ValidatorRegistryError::InvalidDescriptor {
217                    id: registration.id(),
218                    registration_source: registration.source(),
219                    kind: error.kind(),
220                    dependency: error.dependency().map(str::to_owned),
221                });
222            }
223        }
224        Ok(Self {
225            registrations: registrations.into_boxed_slice(),
226        })
227    }
228}