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}