qubit_metadata/metadata.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Provides the [`Metadata`] type — a structured, ordered, type-safe key-value
10//! store backed by [`serde_json::Value`].
11
12use std::collections::BTreeMap;
13
14use serde::{
15 de::DeserializeOwned,
16 Serialize,
17};
18use serde_json::Value;
19
20use crate::{
21 MetadataError,
22 MetadataResult,
23 MetadataValueKind,
24};
25
26/// A structured, ordered, type-safe key-value store for attaching arbitrary
27/// annotations to domain objects.
28///
29/// `Metadata` is backed by a [`BTreeMap<String, Value>`] (ordered by key) and
30/// provides two layers of typed access:
31///
32/// - Convenience accessors like [`Metadata::get`] and [`Metadata::set`] keep the
33/// API terse and ergonomic.
34/// - Explicit accessors like [`Metadata::try_get`] and [`Metadata::try_set`]
35/// preserve failure reasons, which is useful for debugging and validation.
36///
37/// The type model intentionally stays JSON-shaped rather than closed over a
38/// fixed enum of Rust scalar types. This keeps the crate interoperable with
39/// `serde_json`, nested objects, and external JSON-based APIs.
40///
41/// # Examples
42///
43/// ```rust
44/// use qubit_metadata::Metadata;
45///
46/// let mut meta = Metadata::new();
47/// meta.set("author", "alice");
48/// meta.set("priority", 3_i64);
49/// meta.set("reviewed", true);
50///
51/// // Convenience API
52/// let author: Option<String> = meta.get("author");
53/// assert_eq!(author.as_deref(), Some("alice"));
54///
55/// // Explicit API
56/// let priority = meta.try_get::<i64>("priority").unwrap();
57/// assert_eq!(priority, 3);
58/// ```
59#[derive(Debug, Clone, PartialEq, Default, Serialize, serde::Deserialize)]
60pub struct Metadata(BTreeMap<String, Value>);
61
62impl Metadata {
63 /// Creates an empty `Metadata` instance.
64 #[inline]
65 pub fn new() -> Self {
66 Self(BTreeMap::new())
67 }
68
69 /// Returns `true` if there are no entries.
70 #[inline]
71 pub fn is_empty(&self) -> bool {
72 self.0.is_empty()
73 }
74
75 /// Returns the number of key-value pairs.
76 #[inline]
77 pub fn len(&self) -> usize {
78 self.0.len()
79 }
80
81 /// Returns `true` if the given key exists.
82 #[inline]
83 pub fn contains_key(&self, key: &str) -> bool {
84 self.0.contains_key(key)
85 }
86
87 /// Retrieves and deserializes the value associated with `key`.
88 ///
89 /// This is the convenience version of [`Metadata::try_get`]. It returns
90 /// `None` when the key is absent or when deserialization into `T` fails.
91 ///
92 /// Use this when a concise, best-effort lookup is preferred over detailed
93 /// diagnostics.
94 pub fn get<T>(&self, key: &str) -> Option<T>
95 where
96 T: DeserializeOwned,
97 {
98 self.try_get(key).ok()
99 }
100
101 /// Retrieves and deserializes the value associated with `key`, preserving
102 /// the reason when retrieval fails.
103 ///
104 /// # Errors
105 ///
106 /// - [`MetadataError::MissingKey`] if `key` does not exist
107 /// - [`MetadataError::DeserializationError`] if the stored JSON value cannot
108 /// be deserialized into `T`
109 pub fn try_get<T>(&self, key: &str) -> MetadataResult<T>
110 where
111 T: DeserializeOwned,
112 {
113 let value = self
114 .0
115 .get(key)
116 .ok_or_else(|| MetadataError::MissingKey(key.to_string()))?;
117 serde_json::from_value(value.clone())
118 .map_err(|error| MetadataError::deserialization_error::<T>(key, value, error))
119 }
120
121 /// Returns a reference to the raw [`Value`] for `key`, or `None` if absent.
122 #[inline]
123 pub fn get_raw(&self, key: &str) -> Option<&Value> {
124 self.0.get(key)
125 }
126
127 /// Returns the coarse JSON kind of the value stored under `key`.
128 ///
129 /// This is a lightweight inspection API inspired by the stricter type
130 /// introspection facilities in `qubit-value`, adapted to `Metadata`'s
131 /// open-ended JSON storage model.
132 #[inline]
133 pub fn value_kind(&self, key: &str) -> Option<MetadataValueKind> {
134 self.0.get(key).map(MetadataValueKind::of)
135 }
136
137 /// Retrieves and deserializes the value associated with `key`, or returns
138 /// `default` if lookup fails for any reason.
139 ///
140 /// This mirrors the forgiving default-value style used by `qubit-config`.
141 /// It is intentionally convenience-oriented: both missing keys and type
142 /// mismatches fall back to the supplied default.
143 #[must_use]
144 pub fn get_or<T>(&self, key: &str, default: T) -> T
145 where
146 T: DeserializeOwned,
147 {
148 self.try_get(key).unwrap_or(default)
149 }
150
151 /// Serializes `value` and inserts it under `key`.
152 ///
153 /// This is the convenience version of [`Metadata::try_set`]. It preserves
154 /// the current ergonomic API and panics if serialization fails.
155 pub fn set<T>(&mut self, key: impl Into<String>, value: T) -> Option<Value>
156 where
157 T: Serialize,
158 {
159 self.try_set(key, value)
160 .expect("Metadata::set: value must be serializable to serde_json::Value")
161 }
162
163 /// Serializes `value` and inserts it under `key`, preserving serialization
164 /// failures instead of panicking.
165 ///
166 /// # Errors
167 ///
168 /// Returns [`MetadataError::SerializationError`] when `value` fails to
169 /// serialize into [`serde_json::Value`].
170 pub fn try_set<T>(&mut self, key: impl Into<String>, value: T) -> MetadataResult<Option<Value>>
171 where
172 T: Serialize,
173 {
174 let key = key.into();
175 let json = serde_json::to_value(value)
176 .map_err(|error| MetadataError::serialization_error(key.clone(), error))?;
177 Ok(self.0.insert(key, json))
178 }
179
180 /// Inserts a raw [`Value`] directly, bypassing serialization.
181 ///
182 /// Returns the previous value if present.
183 #[inline]
184 pub fn set_raw(&mut self, key: impl Into<String>, value: Value) -> Option<Value> {
185 self.0.insert(key.into(), value)
186 }
187
188 /// Removes the entry for `key` and returns the raw [`Value`] if it existed.
189 #[inline]
190 pub fn remove(&mut self, key: &str) -> Option<Value> {
191 self.0.remove(key)
192 }
193
194 /// Removes all entries.
195 #[inline]
196 pub fn clear(&mut self) {
197 self.0.clear();
198 }
199
200 /// Returns an iterator over `(&str, &Value)` pairs in key-sorted order.
201 #[inline]
202 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
203 self.0.iter().map(|(k, v)| (k.as_str(), v))
204 }
205
206 /// Returns an iterator over the keys in sorted order.
207 #[inline]
208 pub fn keys(&self) -> impl Iterator<Item = &str> {
209 self.0.keys().map(String::as_str)
210 }
211
212 /// Returns an iterator over the raw values in key-sorted order.
213 #[inline]
214 pub fn values(&self) -> impl Iterator<Item = &Value> {
215 self.0.values()
216 }
217
218 /// Merges all entries from `other` into `self`, overwriting existing keys.
219 pub fn merge(&mut self, other: Metadata) {
220 for (k, v) in other.0 {
221 self.0.insert(k, v);
222 }
223 }
224
225 /// Returns a new `Metadata` that contains all entries from both `self` and
226 /// `other`. Entries in `other` take precedence on key conflicts.
227 #[must_use]
228 pub fn merged(&self, other: &Metadata) -> Metadata {
229 let mut result = self.clone();
230 for (k, v) in &other.0 {
231 result.0.insert(k.clone(), v.clone());
232 }
233 result
234 }
235
236 /// Retains only the entries for which `predicate` returns `true`.
237 pub fn retain<F>(&mut self, mut predicate: F)
238 where
239 F: FnMut(&str, &Value) -> bool,
240 {
241 self.0.retain(|k, v| predicate(k.as_str(), v));
242 }
243
244 /// Converts this `Metadata` into its underlying [`BTreeMap`].
245 #[inline]
246 pub fn into_inner(self) -> BTreeMap<String, Value> {
247 self.0
248 }
249}
250
251impl From<BTreeMap<String, Value>> for Metadata {
252 #[inline]
253 fn from(map: BTreeMap<String, Value>) -> Self {
254 Self(map)
255 }
256}
257
258impl From<Metadata> for BTreeMap<String, Value> {
259 #[inline]
260 fn from(meta: Metadata) -> Self {
261 meta.0
262 }
263}
264
265impl FromIterator<(String, Value)> for Metadata {
266 fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
267 Self(iter.into_iter().collect())
268 }
269}
270
271impl IntoIterator for Metadata {
272 type Item = (String, Value);
273 type IntoIter = std::collections::btree_map::IntoIter<String, Value>;
274
275 #[inline]
276 fn into_iter(self) -> Self::IntoIter {
277 self.0.into_iter()
278 }
279}
280
281impl<'a> IntoIterator for &'a Metadata {
282 type Item = (&'a String, &'a Value);
283 type IntoIter = std::collections::btree_map::Iter<'a, String, Value>;
284
285 #[inline]
286 fn into_iter(self) -> Self::IntoIter {
287 self.0.iter()
288 }
289}
290
291impl Extend<(String, Value)> for Metadata {
292 fn extend<I: IntoIterator<Item = (String, Value)>>(&mut self, iter: I) {
293 self.0.extend(iter);
294 }
295}