Skip to main content

storage_kit/core/
locator.rs

1use serde::{Deserialize, Serialize};
2
3use crate::StorageError;
4
5/// Logical address for a collection/table across backends.
6///
7/// # Examples
8///
9/// ```
10/// let locator = storage_kit::database::StorageLocator::new("documents")
11///     .with_namespace(["tenant"])
12///     .with_database("app");
13/// assert_eq!(locator.collection, "documents");
14/// ```
15#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
16pub struct StorageLocator {
17    /// Optional namespace path that scopes the collection.
18    ///
19    /// Backends with one native namespace value may reject multi-segment paths.
20    pub namespace: Vec<String>,
21    /// Optional logical database name inserted between the namespace and collection.
22    pub database: Option<String>,
23    /// Logical collection or table name targeted by the request.
24    pub collection: String,
25}
26
27pub(crate) trait ApplyDefaultLocator: Sized {
28    fn apply_default_locator(self, default: &StorageLocator) -> Self;
29}
30
31pub(crate) trait HasLocator {
32    fn locator_mut(&mut self) -> &mut StorageLocator;
33}
34
35impl<T> ApplyDefaultLocator for T
36where
37    T: HasLocator,
38{
39    fn apply_default_locator(mut self, default: &StorageLocator) -> Self {
40        let locator = self.locator_mut().clone().with_defaults_from(default);
41        *self.locator_mut() = locator;
42        self
43    }
44}
45
46impl StorageLocator {
47    pub(crate) fn validate_for_config(&self, field: &str) -> Result<(), StorageError> {
48        if self.collection.trim().is_empty() {
49            return Err(StorageError::InvalidConfig(format!(
50                "{field}.collection must not be empty"
51            )));
52        }
53        if self
54            .database
55            .as_deref()
56            .is_some_and(|database| database.trim().is_empty())
57        {
58            return Err(StorageError::InvalidConfig(format!(
59                "{field}.database must not be empty"
60            )));
61        }
62        if let Some(index) = self
63            .namespace
64            .iter()
65            .position(|segment| segment.trim().is_empty())
66        {
67            return Err(StorageError::InvalidConfig(format!(
68                "{field}.namespace[{index}] must not be empty"
69            )));
70        }
71        Ok(())
72    }
73
74    /// Creates a locator for a collection without a namespace or database override.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// let locator = storage_kit::database::StorageLocator::new("documents");
80    /// assert_eq!(locator.collection, "documents");
81    /// ```
82    pub fn new(collection: impl Into<String>) -> Self {
83        Self {
84            namespace: Vec::new(),
85            database: None,
86            collection: collection.into(),
87        }
88    }
89
90    /// Replaces the namespace path on the locator.
91    ///
92    /// Some backends accept only one native namespace segment.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// let locator = storage_kit::database::StorageLocator::new("documents").with_namespace(["tenant"]);
98    /// assert_eq!(locator.namespace, vec!["tenant"]);
99    /// ```
100    pub fn with_namespace(
101        mut self,
102        namespace: impl IntoIterator<Item = impl Into<String>>,
103    ) -> Self {
104        self.namespace = namespace.into_iter().map(Into::into).collect();
105        self
106    }
107
108    /// Sets the logical database name on the locator.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// let locator = storage_kit::database::StorageLocator::new("documents").with_database("app");
114    /// assert_eq!(locator.database.as_deref(), Some("app"));
115    /// ```
116    pub fn with_database(mut self, database: impl Into<String>) -> Self {
117        self.database = Some(database.into());
118        self
119    }
120
121    #[must_use]
122    pub(crate) fn with_defaults_from(mut self, default: &Self) -> Self {
123        if self.namespace.is_empty() {
124            self.namespace = default.namespace.clone();
125        }
126        if self.database.is_none() {
127            self.database = default.database.clone();
128        }
129        if self.collection.is_empty() {
130            self.collection = default.collection.clone();
131        }
132        self
133    }
134}
135
136impl ApplyDefaultLocator for StorageLocator {
137    fn apply_default_locator(self, default: &StorageLocator) -> Self {
138        self.with_defaults_from(default)
139    }
140}