Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

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}