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}