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 lexically first conflicting ID and all
103 /// of its sources sorted by source fields. Repeated identical sources are
104 /// retained because each registration is reported.
105 fn build(mut registrations: Vec<ValueCodecRegistration<D>>) -> Result<Self, ValueCodecRegistryError> {
106 registrations.sort_by_key(ValueCodecRegistration::id);
107 for (index, pair) in registrations.windows(2).enumerate() {
108 if pair[0].id() == pair[1].id() {
109 let id = pair[0].id();
110 let mut sources = registrations[index..]
111 .iter()
112 .take_while(|registration| registration.id() == id)
113 .map(ValueCodecRegistration::source)
114 .collect::<Vec<_>>();
115 sources.sort_unstable();
116 return Err(ValueCodecRegistryError::DuplicateId {
117 id: id.as_str(),
118 sources,
119 });
120 }
121 }
122 let indices = registrations
123 .iter()
124 .enumerate()
125 .map(|(index, registration)| (registration.id(), index))
126 .collect();
127 Ok(Self {
128 registrations: registrations.into_boxed_slice(),
129 indices,
130 })
131 }
132
133 /// Finds a registration by stable ID.
134 ///
135 /// # Parameters
136 ///
137 /// - `id`: Exact stable ID to look up, without normalization.
138 ///
139 /// # Returns
140 ///
141 /// `Some` borrows the matching registration from this registry; `None`
142 /// means the ID is absent. Lookup uses a logarithmic tree search.
143 #[must_use]
144 #[inline]
145 pub fn get(&self, id: &str) -> Option<&ValueCodecRegistration<D>> {
146 let index = *self.indices.get(id)?;
147 self.registrations.get(index)
148 }
149
150 /// Returns registrations in deterministic ID order.
151 ///
152 /// # Returns
153 ///
154 /// The immutable registration slice borrowed from this registry without
155 /// allocation.
156 #[must_use]
157 #[inline]
158 pub fn registrations(&self) -> &[ValueCodecRegistration<D>] {
159 &self.registrations
160 }
161}
162
163impl ValueStringCodecRegistry {
164 /// Initializes and returns the process-wide linked string registry.
165 ///
166 /// Initialization is synchronized and cached, including construction
167 /// failures.
168 ///
169 /// # Returns
170 ///
171 /// A shared static registry containing all linked string registrations.
172 ///
173 /// # Errors
174 ///
175 /// Returns the cached construction error when linked registrations
176 /// conflict. The error identifies the lexically first duplicate ID and
177 /// all declarations for that ID in source-field order.
178 pub fn try_global() -> Result<&'static Self, ValueCodecRegistryError> {
179 // Caches both successful construction and duplicate-ID failures.
180 static REGISTRY: OnceLock<Result<ValueStringCodecRegistry, ValueCodecRegistryError>> = OnceLock::new();
181 match REGISTRY.get_or_init(|| {
182 let registrations = iter::<ValueStringCodecRegistrationFactory>
183 .into_iter()
184 .map(|factory| (factory.0)())
185 .collect();
186 Self::build(registrations)
187 }) {
188 Ok(registry) => Ok(registry),
189 Err(error) => Err(error.clone()),
190 }
191 }
192
193 /// Returns the process-wide string registry or panics with a stable
194 /// diagnostic.
195 ///
196 /// # Returns
197 ///
198 /// A shared static registry, initialized at most once by `try_global`.
199 ///
200 /// # Panics
201 ///
202 /// Panics when linked registrations conflict.
203 #[must_use]
204 pub fn global() -> &'static Self {
205 Self::try_global().unwrap_or_else(|error| panic!("invalid global value string codec registry: {error}"))
206 }
207}
208
209impl ValueBytesCodecRegistry {
210 /// Initializes and returns the process-wide linked bytes registry.
211 ///
212 /// Initialization is synchronized and cached, including construction
213 /// failures.
214 ///
215 /// # Returns
216 ///
217 /// A shared static registry containing all linked bytes registrations.
218 ///
219 /// # Errors
220 ///
221 /// Returns the cached construction error when linked registrations
222 /// conflict. The error identifies the lexically first duplicate ID and
223 /// all declarations for that ID in source-field order.
224 pub fn try_global() -> Result<&'static Self, ValueCodecRegistryError> {
225 // Caches both successful construction and duplicate-ID failures.
226 static REGISTRY: OnceLock<Result<ValueBytesCodecRegistry, ValueCodecRegistryError>> = OnceLock::new();
227 match REGISTRY.get_or_init(|| {
228 let registrations = iter::<ValueBytesCodecRegistrationFactory>
229 .into_iter()
230 .map(|factory| (factory.0)())
231 .collect();
232 Self::build(registrations)
233 }) {
234 Ok(registry) => Ok(registry),
235 Err(error) => Err(error.clone()),
236 }
237 }
238
239 /// Returns the process-wide bytes registry or panics with a stable
240 /// diagnostic.
241 ///
242 /// # Returns
243 ///
244 /// A shared static registry, initialized at most once by `try_global`.
245 ///
246 /// # Panics
247 ///
248 /// Panics when linked registrations conflict.
249 #[must_use]
250 pub fn global() -> &'static Self {
251 Self::try_global().unwrap_or_else(|error| panic!("invalid global value bytes codec registry: {error}"))
252 }
253}