eidetica/store/registry.rs
1//! Registry for managing typed entries with metadata
2//!
3//! This module provides a high-level interface for managing registry subtrees,
4//! which store entries with type identifiers and configuration data.
5//! Used for the `_index` system subtree (store metadata) and other subtrees
6//! that need the same typed configuration pattern.
7
8use serde::{Deserialize, Serialize};
9
10use crate::height::HeightStrategy;
11
12/// Trait for types that can be registered in a Registry.
13///
14/// Provides a unique type identifier for runtime type checking. This trait
15/// is implemented by both [`Store`](super::Store) types and
16/// [`TransportConfig`](crate::sync::transports::TransportConfig) types.
17///
18/// # Example
19///
20/// ```
21/// use eidetica::Registered;
22///
23/// struct MyType;
24///
25/// impl Registered for MyType {
26/// fn type_id() -> &'static str {
27/// "mytype:v0"
28/// }
29/// }
30///
31/// assert_eq!(MyType::type_id(), "mytype:v0");
32/// assert!(MyType::supports_type_id("mytype:v0"));
33/// assert!(!MyType::supports_type_id("other:v0"));
34/// ```
35pub trait Registered {
36 /// Returns a unique identifier for this type.
37 ///
38 /// The format is typically `"name:version"` (e.g., `"docstore:v0"`, `"iroh:v0"`).
39 fn type_id() -> &'static str;
40
41 /// Check if this type supports loading from a stored type_id.
42 ///
43 /// Override this method to support version migration, allowing newer
44 /// implementations to read data stored by older versions.
45 ///
46 /// # Example
47 ///
48 /// ```
49 /// use eidetica::Registered;
50 ///
51 /// struct MyTypeV1;
52 ///
53 /// impl Registered for MyTypeV1 {
54 /// fn type_id() -> &'static str {
55 /// "mytype:v1"
56 /// }
57 ///
58 /// fn supports_type_id(type_id: &str) -> bool {
59 /// // v1 can read both v0 and v1 data
60 /// type_id == "mytype:v0" || type_id == "mytype:v1"
61 /// }
62 /// }
63 ///
64 /// assert_eq!(MyTypeV1::type_id(), "mytype:v1");
65 /// assert!(MyTypeV1::supports_type_id("mytype:v0")); // Can read v0
66 /// assert!(MyTypeV1::supports_type_id("mytype:v1")); // Can read v1
67 /// assert!(!MyTypeV1::supports_type_id("other:v0")); // Cannot read other types
68 /// ```
69 fn supports_type_id(type_id: &str) -> bool {
70 type_id == Self::type_id()
71 }
72}
73
74use crate::{
75 Result, Transaction,
76 crdt::{Doc, doc},
77 store::{DocStore, Store, StoreError},
78};
79
80/// Common settings for any subtree type.
81///
82/// These settings can be configured per-subtree via the `_index` registry.
83/// Settings not specified here inherit from database-level defaults.
84#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SubtreeSettings {
86 /// Height strategy override for this subtree.
87 ///
88 /// - `None`: Inherit from database-level strategy (subtree height omitted in entries)
89 /// - `Some(strategy)`: Use independent height calculation for this subtree
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub height_strategy: Option<HeightStrategy>,
92}
93
94impl SubtreeSettings {
95 /// Check if all settings are at their default values.
96 ///
97 /// Used for serde `skip_serializing_if` to avoid storing empty settings.
98 pub fn is_default(&self) -> bool {
99 self.height_strategy.is_none()
100 }
101}
102
103/// Metadata for a registry entry
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct RegistryEntry {
106 /// The type identifier (e.g., "docstore:v0", "iroh:v0")
107 pub type_id: String,
108
109 /// Type-specific configuration data as a [`Doc`]
110 pub config: Doc,
111
112 /// Common subtree settings
113 pub settings: SubtreeSettings,
114}
115
116/// A registry that wraps a DocStore and provides specialized methods
117/// for managing typed entries with metadata.
118///
119/// Registry provides a clean abstraction for storing entries with
120/// `{ type, config }` structure. Used for:
121/// - `_index` subtree: Store type metadata in databases
122/// - `transports` subtree: Transport configuration in sync
123pub struct Registry {
124 /// The underlying DocStore
125 inner: DocStore,
126}
127
128impl Registry {
129 /// Create a new Registry for a specific subtree
130 ///
131 /// # Arguments
132 /// * `transaction` - The transaction to operate within
133 /// * `subtree_name` - The name of the subtree this registry manages
134 ///
135 /// # Returns
136 /// A Result containing the Registry or an error if creation fails
137 pub(crate) async fn new(
138 transaction: &Transaction,
139 subtree_name: impl Into<String>,
140 ) -> Result<Self> {
141 let name = subtree_name.into();
142 // Initialize subtree parents before creating the store
143 // This mirrors what get_store() does but avoids the recursive
144 // get_store -> get_index -> Registry::new -> get_store cycle
145 transaction.init_subtree_parents(&name).await?;
146 // Create DocStore directly instead of going through get_store
147 let inner = DocStore::new(transaction, name).await?;
148 Ok(Self { inner })
149 }
150
151 /// Get metadata for a named entry
152 ///
153 /// # Arguments
154 /// * `name` - The name of the entry to query
155 ///
156 /// # Returns
157 /// The entry metadata if found, or an error if not registered
158 pub async fn get_entry(&self, name: impl AsRef<str>) -> Result<RegistryEntry> {
159 let name = name.as_ref();
160 let value = self.inner.get(name).await?;
161
162 // The value should be a Doc (nested map) with "type" and "config" keys
163 let doc = value
164 .as_doc()
165 .ok_or_else(|| StoreError::DeserializationFailed {
166 store: self.inner.name().to_string(),
167 reason: format!("Entry '{name}' metadata is not a Doc"),
168 })?;
169
170 let type_id = doc
171 .get("type")
172 .and_then(|v: &doc::Value| v.as_text())
173 .ok_or_else(|| StoreError::DeserializationFailed {
174 store: self.inner.name().to_string(),
175 reason: format!("Entry '{name}' missing 'type' field"),
176 })?
177 .to_string();
178
179 let config = match doc.get("config") {
180 Some(doc::Value::Doc(d)) => d.clone(),
181 Some(doc::Value::Text(s)) if s == "{}" => Doc::new(),
182 Some(doc::Value::Text(_)) => {
183 return Err(StoreError::DeserializationFailed {
184 store: self.inner.name().to_string(),
185 reason: format!("Entry '{name}' config is a non-empty Text, expected Doc"),
186 }
187 .into());
188 }
189 _ => Doc::new(),
190 };
191
192 // Parse settings if present, default to empty settings
193 let settings = match doc.get("settings") {
194 Some(settings_value) => {
195 let settings_doc =
196 settings_value
197 .as_doc()
198 .ok_or_else(|| StoreError::DeserializationFailed {
199 store: self.inner.name().to_string(),
200 reason: format!("Entry '{name}' settings is not a Doc"),
201 })?;
202
203 // Parse height_strategy if present
204 let height_strategy = match settings_doc.get("height_strategy") {
205 Some(strategy_value) => {
206 let json = strategy_value.as_text().ok_or_else(|| {
207 StoreError::DeserializationFailed {
208 store: self.inner.name().to_string(),
209 reason: format!(
210 "Entry '{name}' height_strategy is not a text value"
211 ),
212 }
213 })?;
214 Some(serde_json::from_str(json).map_err(|e| {
215 StoreError::DeserializationFailed {
216 store: self.inner.name().to_string(),
217 reason: format!(
218 "Failed to parse height_strategy for '{name}': {e}"
219 ),
220 }
221 })?)
222 }
223 None => None,
224 };
225
226 SubtreeSettings { height_strategy }
227 }
228 None => SubtreeSettings::default(),
229 };
230
231 Ok(RegistryEntry {
232 type_id,
233 config,
234 settings,
235 })
236 }
237
238 /// Check if an entry is registered
239 ///
240 /// # Arguments
241 /// * `name` - The name of the entry to check
242 ///
243 /// # Returns
244 /// true if the entry is registered, false otherwise
245 pub async fn contains(&self, name: impl AsRef<str>) -> bool {
246 self.get_entry(name).await.is_ok()
247 }
248
249 /// Register or update an entry
250 ///
251 /// # Arguments
252 /// * `name` - The name of the entry to register/update
253 /// * `type_id` - The type identifier (e.g., "docstore:v0", "iroh:v0")
254 /// * `config` - Type-specific configuration as a [`Doc`]
255 ///
256 /// # Returns
257 /// Result indicating success or failure
258 pub async fn set_entry(
259 &self,
260 name: impl AsRef<str>,
261 type_id: impl Into<String>,
262 config: Doc,
263 ) -> Result<()> {
264 let name = name.as_ref();
265 let type_id = type_id.into();
266
267 // Create the nested structure for this entry's metadata
268 let mut metadata_doc = Doc::new();
269 metadata_doc.set("type", doc::Value::Text(type_id));
270 metadata_doc.set("config", doc::Value::Doc(config));
271
272 // Set the metadata in the registry subtree
273 self.inner.set(name, doc::Value::Doc(metadata_doc)).await?;
274
275 Ok(())
276 }
277
278 /// List all registered entries
279 ///
280 /// # Returns
281 /// A vector of entry names that are registered
282 pub async fn list(&self) -> Result<Vec<String>> {
283 let full_state: Doc = self
284 .inner
285 .transaction()
286 .get_full_state(self.inner.name())
287 .await?;
288
289 // Get all top-level keys from the Doc and clone them to owned Strings
290 let keys: Vec<String> = full_state.keys().cloned().collect();
291
292 Ok(keys)
293 }
294
295 /// Get the settings for a subtree.
296 ///
297 /// Returns default settings if the subtree is not registered or has no settings.
298 ///
299 /// # Arguments
300 /// * `name` - The name of the subtree
301 ///
302 /// # Returns
303 /// The subtree settings (default if not found or not registered)
304 pub async fn get_subtree_settings(&self, name: impl AsRef<str>) -> Result<SubtreeSettings> {
305 match self.get_entry(name).await {
306 Ok(entry) => Ok(entry.settings),
307 Err(e) if e.is_not_found() => Ok(SubtreeSettings::default()),
308 Err(e) => Err(e),
309 }
310 }
311
312 /// Update the settings for a registered subtree.
313 ///
314 /// The subtree must already be registered (via `set_entry`). This method
315 /// only updates the settings portion, preserving the type_id and config.
316 ///
317 /// # Arguments
318 /// * `name` - The name of the subtree
319 /// * `settings` - The new settings to apply
320 ///
321 /// # Returns
322 /// Result indicating success or failure. Returns an error if the subtree
323 /// is not registered.
324 pub async fn set_subtree_settings(
325 &self,
326 name: impl AsRef<str>,
327 settings: SubtreeSettings,
328 ) -> Result<()> {
329 let name = name.as_ref();
330
331 // Get existing entry to preserve type_id and config
332 let entry = self.get_entry(name).await?;
333
334 // Create the nested structure with updated settings
335 let mut metadata_doc = Doc::new();
336 metadata_doc.set("type", doc::Value::Text(entry.type_id));
337 metadata_doc.set("config", doc::Value::Doc(entry.config));
338
339 // Only add settings if non-default
340 if !settings.is_default() {
341 let mut settings_doc = Doc::new();
342 if let Some(strategy) = settings.height_strategy {
343 // Serialize the strategy as a JSON string for storage
344 let strategy_json = serde_json::to_string(&strategy).map_err(|e| {
345 StoreError::SerializationFailed {
346 store: self.inner.name().to_string(),
347 reason: format!("Failed to serialize height_strategy: {e}"),
348 }
349 })?;
350 settings_doc.set("height_strategy", doc::Value::Text(strategy_json));
351 }
352 metadata_doc.set("settings", doc::Value::Doc(settings_doc));
353 }
354
355 // Set the updated metadata
356 self.inner.set(name, doc::Value::Doc(metadata_doc)).await?;
357
358 Ok(())
359 }
360}