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