Skip to main content

qubit_codec/value/
value_codec_registry.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Immutable local and process-wide value-codec registries.
10
11use std::collections::BTreeMap;
12use std::sync::OnceLock;
13
14use inventory::iter;
15
16use crate::ValueBytesCodecDescriptor;
17use crate::ValueBytesCodecRegistrationFactory;
18use crate::ValueCodecId;
19use crate::ValueCodecRegistration;
20use crate::ValueCodecRegistryError;
21use crate::ValueStringCodecDescriptor;
22use crate::ValueStringCodecRegistrationFactory;
23
24/// An immutable string-wire value-codec registry sorted by stable ID.
25pub type ValueStringCodecRegistry = ValueCodecRegistry<ValueStringCodecDescriptor>;
26
27/// An immutable bytes-wire value-codec registry sorted by stable ID.
28pub type ValueBytesCodecRegistry = ValueCodecRegistry<ValueBytesCodecDescriptor>;
29
30/// An immutable value-codec registry sorted by stable ID.
31///
32/// # Type Parameters
33///
34/// - `D`: Copyable, static descriptor identifying one wire representation.
35///
36/// # Examples
37///
38/// ```
39/// use qubit_codec::ValueStringCodecRegistry;
40///
41/// let registry = ValueStringCodecRegistry::empty();
42/// assert!(registry.get("missing.example").is_none());
43/// ```
44#[derive(Debug)]
45pub struct ValueCodecRegistry<D: 'static> {
46    /// Owned registrations sorted once by stable ID during construction.
47    registrations: Box<[ValueCodecRegistration<D>]>,
48    /// Stable ID to position mapping into the immutable registration slice.
49    indices: BTreeMap<ValueCodecId, usize>,
50}
51
52impl<D: Copy + 'static> ValueCodecRegistry<D> {
53    /// Builds a local registry from static registrations.
54    ///
55    /// # Parameters
56    ///
57    /// - `registrations`: Static entries copied into this registry; input order
58    ///   is ignored.
59    ///
60    /// # Returns
61    ///
62    /// An owned, immutable registry sorted by ID, independent of the iterator.
63    ///
64    /// # Errors
65    ///
66    /// Returns a duplicate-ID error when multiple entries claim one ID.
67    pub fn from_registrations(
68        registrations: impl IntoIterator<Item = &'static ValueCodecRegistration<D>>,
69    ) -> Result<Self, ValueCodecRegistryError> {
70        Self::build(registrations.into_iter().copied().collect())
71    }
72
73    /// Returns an empty local registry.
74    ///
75    /// # Returns
76    ///
77    /// A local registry with no registrations or indexed IDs.
78    #[must_use]
79    #[inline]
80    pub fn empty() -> Self {
81        Self {
82            registrations: Box::new([]),
83            indices: BTreeMap::new(),
84        }
85    }
86
87    /// Freezes registrations and rejects duplicate IDs.
88    ///
89    /// # Parameters
90    ///
91    /// - `registrations`: Owned entries sorted in place before indexing.
92    ///
93    /// # Returns
94    ///
95    /// An immutable registry owning the sorted entries and their lookup index.
96    ///
97    /// # Errors
98    ///
99    /// Returns `DuplicateId` with the first conflicting ID and its two sorted
100    /// sources.
101    fn build(mut registrations: Vec<ValueCodecRegistration<D>>) -> Result<Self, ValueCodecRegistryError> {
102        registrations.sort_by_key(ValueCodecRegistration::id);
103        for pair in registrations.windows(2) {
104            if pair[0].id() == pair[1].id() {
105                let mut sources = pair.iter().map(ValueCodecRegistration::source).collect::<Vec<_>>();
106                sources.sort_unstable();
107                return Err(ValueCodecRegistryError::DuplicateId {
108                    id: pair[0].id().as_str(),
109                    sources,
110                });
111            }
112        }
113        let indices = registrations
114            .iter()
115            .enumerate()
116            .map(|(index, registration)| (registration.id(), index))
117            .collect();
118        Ok(Self {
119            registrations: registrations.into_boxed_slice(),
120            indices,
121        })
122    }
123
124    /// Finds a registration by stable ID.
125    ///
126    /// # Parameters
127    ///
128    /// - `id`: Exact stable ID to look up, without normalization.
129    ///
130    /// # Returns
131    ///
132    /// `Some` borrows the matching registration from this registry; `None`
133    /// means the ID is absent. Lookup uses a logarithmic tree search.
134    #[must_use]
135    #[inline]
136    pub fn get(&self, id: &str) -> Option<&ValueCodecRegistration<D>> {
137        let index = *self.indices.get(id)?;
138        self.registrations.get(index)
139    }
140
141    /// Returns registrations in deterministic ID order.
142    ///
143    /// # Returns
144    ///
145    /// The immutable registration slice borrowed from this registry without
146    /// allocation.
147    #[must_use]
148    #[inline]
149    pub fn registrations(&self) -> &[ValueCodecRegistration<D>] {
150        &self.registrations
151    }
152}
153
154impl ValueStringCodecRegistry {
155    /// Initializes and returns the process-wide linked string registry.
156    ///
157    /// Initialization is synchronized and cached, including construction
158    /// failures.
159    ///
160    /// # Returns
161    ///
162    /// A shared static registry containing all linked string registrations.
163    ///
164    /// # Errors
165    ///
166    /// Returns the cached construction error when linked registrations
167    /// conflict.
168    pub fn try_global() -> Result<&'static Self, ValueCodecRegistryError> {
169        // Caches both successful construction and duplicate-ID failures.
170        static REGISTRY: OnceLock<Result<ValueStringCodecRegistry, ValueCodecRegistryError>> = OnceLock::new();
171        match REGISTRY.get_or_init(|| {
172            let registrations = iter::<ValueStringCodecRegistrationFactory>
173                .into_iter()
174                .map(|factory| (factory.0)())
175                .collect();
176            Self::build(registrations)
177        }) {
178            Ok(registry) => Ok(registry),
179            Err(error) => Err(error.clone()),
180        }
181    }
182
183    /// Returns the process-wide string registry or panics with a stable
184    /// diagnostic.
185    ///
186    /// # Returns
187    ///
188    /// A shared static registry, initialized at most once by `try_global`.
189    ///
190    /// # Panics
191    ///
192    /// Panics when linked registrations conflict.
193    #[must_use]
194    pub fn global() -> &'static Self {
195        Self::try_global().unwrap_or_else(|error| panic!("invalid global value string codec registry: {error}"))
196    }
197}
198
199impl ValueBytesCodecRegistry {
200    /// Initializes and returns the process-wide linked bytes registry.
201    ///
202    /// Initialization is synchronized and cached, including construction
203    /// failures.
204    ///
205    /// # Returns
206    ///
207    /// A shared static registry containing all linked bytes registrations.
208    ///
209    /// # Errors
210    ///
211    /// Returns the cached construction error when linked registrations
212    /// conflict.
213    pub fn try_global() -> Result<&'static Self, ValueCodecRegistryError> {
214        // Caches both successful construction and duplicate-ID failures.
215        static REGISTRY: OnceLock<Result<ValueBytesCodecRegistry, ValueCodecRegistryError>> = OnceLock::new();
216        match REGISTRY.get_or_init(|| {
217            let registrations = iter::<ValueBytesCodecRegistrationFactory>
218                .into_iter()
219                .map(|factory| (factory.0)())
220                .collect();
221            Self::build(registrations)
222        }) {
223            Ok(registry) => Ok(registry),
224            Err(error) => Err(error.clone()),
225        }
226    }
227
228    /// Returns the process-wide bytes registry or panics with a stable
229    /// diagnostic.
230    ///
231    /// # Returns
232    ///
233    /// A shared static registry, initialized at most once by `try_global`.
234    ///
235    /// # Panics
236    ///
237    /// Panics when linked registrations conflict.
238    #[must_use]
239    pub fn global() -> &'static Self {
240        Self::try_global().unwrap_or_else(|error| panic!("invalid global value bytes codec registry: {error}"))
241    }
242}