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 MetadataValueType,
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 #[inline]
95 pub fn get<T>(&self, key: &str) -> Option<T>
96 where
97 T: DeserializeOwned,
98 {
99 self.try_get(key).ok()
100 }
101
102 /// Retrieves and deserializes the value associated with `key`, preserving
103 /// the reason when retrieval fails.
104 ///
105 /// # Errors
106 ///
107 /// - [`MetadataError::MissingKey`] if `key` does not exist
108 /// - [`MetadataError::DeserializationError`] if the stored JSON value cannot
109 /// be deserialized into `T`
110 pub fn try_get<T>(&self, key: &str) -> MetadataResult<T>
111 where
112 T: DeserializeOwned,
113 {
114 let value = self
115 .0
116 .get(key)
117 .ok_or_else(|| MetadataError::MissingKey(key.to_string()))?;
118 serde_json::from_value(value.clone())
119 .map_err(|error| MetadataError::deserialization_error::<T>(key, value, error))
120 }
121
122 /// Returns a reference to the raw [`Value`] for `key`, or `None` if absent.
123 #[inline]
124 pub fn get_raw(&self, key: &str) -> Option<&Value> {
125 self.0.get(key)
126 }
127
128 /// Returns the coarse JSON value type of the value stored under `key`.
129 ///
130 /// This is a lightweight inspection API inspired by the stricter type
131 /// introspection facilities in `qubit-value`, adapted to `Metadata`'s
132 /// open-ended JSON storage model.
133 #[inline]
134 pub fn value_type(&self, key: &str) -> Option<MetadataValueType> {
135 self.0.get(key).map(MetadataValueType::of)
136 }
137
138 /// Retrieves and deserializes the value associated with `key`, or returns
139 /// `default` if lookup fails for any reason.
140 ///
141 /// This mirrors the forgiving default-value style used by `qubit-config`.
142 /// It is intentionally convenience-oriented: both missing keys and type
143 /// mismatches fall back to the supplied default.
144 #[inline]
145 #[must_use]
146 pub fn get_or<T>(&self, key: &str, default: T) -> T
147 where
148 T: DeserializeOwned,
149 {
150 self.try_get(key).unwrap_or(default)
151 }
152
153 /// Serializes `value` and inserts it under `key`.
154 ///
155 /// This is the convenience version of [`Metadata::try_set`]. It preserves
156 /// the current ergonomic API and panics if serialization fails.
157 #[inline]
158 pub fn set<T>(&mut self, key: impl Into<String>, value: T) -> Option<Value>
159 where
160 T: Serialize,
161 {
162 self.try_set(key, value)
163 .expect("Metadata::set: value must be serializable to serde_json::Value")
164 }
165
166 /// Serializes `value` and inserts it under `key`, preserving serialization
167 /// failures instead of panicking.
168 ///
169 /// # Errors
170 ///
171 /// Returns [`MetadataError::SerializationError`] when `value` fails to
172 /// serialize into [`serde_json::Value`].
173 pub fn try_set<T>(&mut self, key: impl Into<String>, value: T) -> MetadataResult<Option<Value>>
174 where
175 T: Serialize,
176 {
177 let key = key.into();
178 let json = serde_json::to_value(value)
179 .map_err(|error| MetadataError::serialization_error(key.clone(), error))?;
180 Ok(self.0.insert(key, json))
181 }
182
183 /// Inserts a raw [`Value`] directly, bypassing serialization.
184 ///
185 /// Returns the previous value if present.
186 #[inline]
187 pub fn set_raw(&mut self, key: impl Into<String>, value: Value) -> Option<Value> {
188 self.0.insert(key.into(), value)
189 }
190
191 /// Removes the entry for `key` and returns the raw [`Value`] if it existed.
192 #[inline]
193 pub fn remove(&mut self, key: &str) -> Option<Value> {
194 self.0.remove(key)
195 }
196
197 /// Removes all entries.
198 #[inline]
199 pub fn clear(&mut self) {
200 self.0.clear();
201 }
202
203 /// Returns an iterator over `(&str, &Value)` pairs in key-sorted order.
204 #[inline]
205 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
206 self.0.iter().map(|(k, v)| (k.as_str(), v))
207 }
208
209 /// Returns an iterator over the keys in sorted order.
210 #[inline]
211 pub fn keys(&self) -> impl Iterator<Item = &str> {
212 self.0.keys().map(String::as_str)
213 }
214
215 /// Returns an iterator over the raw values in key-sorted order.
216 #[inline]
217 pub fn values(&self) -> impl Iterator<Item = &Value> {
218 self.0.values()
219 }
220
221 /// Merges all entries from `other` into `self`, overwriting existing keys.
222 pub fn merge(&mut self, other: Metadata) {
223 for (k, v) in other.0 {
224 self.0.insert(k, v);
225 }
226 }
227
228 /// Returns a new `Metadata` that contains all entries from both `self` and
229 /// `other`. Entries in `other` take precedence on key conflicts.
230 #[must_use]
231 pub fn merged(&self, other: &Metadata) -> Metadata {
232 let mut result = self.clone();
233 for (k, v) in &other.0 {
234 result.0.insert(k.clone(), v.clone());
235 }
236 result
237 }
238
239 /// Retains only the entries for which `predicate` returns `true`.
240 #[inline]
241 pub fn retain<F>(&mut self, mut predicate: F)
242 where
243 F: FnMut(&str, &Value) -> bool,
244 {
245 self.0.retain(|k, v| predicate(k.as_str(), v));
246 }
247
248 /// Converts this `Metadata` into its underlying [`BTreeMap`].
249 #[inline]
250 pub fn into_inner(self) -> BTreeMap<String, Value> {
251 self.0
252 }
253}
254
255impl From<BTreeMap<String, Value>> for Metadata {
256 #[inline]
257 fn from(map: BTreeMap<String, Value>) -> Self {
258 Self(map)
259 }
260}
261
262impl From<Metadata> for BTreeMap<String, Value> {
263 #[inline]
264 fn from(meta: Metadata) -> Self {
265 meta.0
266 }
267}
268
269impl FromIterator<(String, Value)> for Metadata {
270 #[inline]
271 fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
272 Self(iter.into_iter().collect())
273 }
274}
275
276impl IntoIterator for Metadata {
277 type Item = (String, Value);
278 type IntoIter = std::collections::btree_map::IntoIter<String, Value>;
279
280 #[inline]
281 fn into_iter(self) -> Self::IntoIter {
282 self.0.into_iter()
283 }
284}
285
286impl<'a> IntoIterator for &'a Metadata {
287 type Item = (&'a String, &'a Value);
288 type IntoIter = std::collections::btree_map::Iter<'a, String, Value>;
289
290 #[inline]
291 fn into_iter(self) -> Self::IntoIter {
292 self.0.iter()
293 }
294}
295
296impl Extend<(String, Value)> for Metadata {
297 #[inline]
298 fn extend<I: IntoIterator<Item = (String, Value)>>(&mut self, iter: I) {
299 self.0.extend(iter);
300 }
301}