Skip to main content

common/storage/
config.rs

1//! Storage configuration types.
2//!
3//! This module provides configuration structures for different storage backends,
4//! allowing services to configure storage type (InMemory or SlateDB) via config files
5//! or environment variables.
6
7use serde::{Deserialize, Serialize};
8
9/// Top-level storage configuration.
10///
11/// Defaults to `SlateDb` with a local `/tmp/opendata-storage` directory.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(tag = "type")]
14pub enum StorageConfig {
15    InMemory,
16    SlateDb(SlateDbStorageConfig),
17}
18
19impl Default for StorageConfig {
20    fn default() -> Self {
21        StorageConfig::SlateDb(SlateDbStorageConfig {
22            path: "data".to_string(),
23            object_store: ObjectStoreConfig::Local(LocalObjectStoreConfig {
24                path: ".data".to_string(),
25            }),
26            settings_path: None,
27        })
28    }
29}
30
31/// SlateDB-specific configuration.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct SlateDbStorageConfig {
34    /// Path prefix for SlateDB data in the object store.
35    pub path: String,
36
37    /// Object store provider configuration.
38    pub object_store: ObjectStoreConfig,
39
40    /// Optional path to SlateDB settings file (TOML/YAML/JSON).
41    ///
42    /// If not provided, uses SlateDB's `Settings::load()` which checks for
43    /// `SlateDb.toml`, `SlateDb.json`, `SlateDb.yaml` in the working directory
44    /// and merges any `SLATEDB_` prefixed environment variables.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub settings_path: Option<String>,
47}
48
49impl Default for SlateDbStorageConfig {
50    fn default() -> Self {
51        Self {
52            path: "data".to_string(),
53            object_store: ObjectStoreConfig::default(),
54            settings_path: None,
55        }
56    }
57}
58
59impl StorageConfig {
60    /// Returns a new config with the path modified by appending a suffix.
61    ///
62    /// For SlateDB storage, appends the suffix to the path (e.g., "data" -> "data/0").
63    /// For InMemory storage, returns a clone unchanged.
64    pub fn with_path_suffix(&self, suffix: &str) -> Self {
65        match self {
66            StorageConfig::InMemory => StorageConfig::InMemory,
67            StorageConfig::SlateDb(config) => StorageConfig::SlateDb(SlateDbStorageConfig {
68                path: format!("{}/{}", config.path, suffix),
69                object_store: config.object_store.clone(),
70                settings_path: config.settings_path.clone(),
71            }),
72        }
73    }
74}
75
76/// Object store provider configuration for SlateDB.
77#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
78#[serde(tag = "type")]
79pub enum ObjectStoreConfig {
80    /// In-memory object store (useful for testing and development).
81    #[default]
82    InMemory,
83
84    /// AWS S3 object store.
85    Aws(AwsObjectStoreConfig),
86
87    /// Local filesystem object store.
88    Local(LocalObjectStoreConfig),
89}
90
91/// AWS S3 object store configuration.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93pub struct AwsObjectStoreConfig {
94    /// AWS region (e.g., "us-west-2").
95    pub region: String,
96
97    /// S3 bucket name.
98    pub bucket: String,
99}
100
101/// Local filesystem object store configuration.
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103pub struct LocalObjectStoreConfig {
104    /// Path to the local directory for storage.
105    pub path: String,
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn should_default_to_slatedb_with_local_data_dir() {
114        // given/when
115        let config = StorageConfig::default();
116
117        // then
118        match config {
119            StorageConfig::SlateDb(slate_config) => {
120                assert_eq!(slate_config.path, "data");
121                assert_eq!(
122                    slate_config.object_store,
123                    ObjectStoreConfig::Local(LocalObjectStoreConfig {
124                        path: ".data".to_string()
125                    })
126                );
127            }
128            _ => panic!("Expected SlateDb config as default"),
129        }
130    }
131
132    #[test]
133    fn should_deserialize_in_memory_config() {
134        // given
135        let yaml = r#"type: InMemory"#;
136
137        // when
138        let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
139
140        // then
141        assert_eq!(config, StorageConfig::InMemory);
142    }
143
144    #[test]
145    fn should_deserialize_slatedb_config_with_local_object_store() {
146        // given
147        let yaml = r#"
148type: SlateDb
149path: my-data
150object_store:
151  type: Local
152  path: /tmp/slatedb
153"#;
154
155        // when
156        let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
157
158        // then
159        match config {
160            StorageConfig::SlateDb(slate_config) => {
161                assert_eq!(slate_config.path, "my-data");
162                assert_eq!(
163                    slate_config.object_store,
164                    ObjectStoreConfig::Local(LocalObjectStoreConfig {
165                        path: "/tmp/slatedb".to_string()
166                    })
167                );
168                assert!(slate_config.settings_path.is_none());
169            }
170            _ => panic!("Expected SlateDb config"),
171        }
172    }
173
174    #[test]
175    fn should_deserialize_slatedb_config_with_aws_object_store() {
176        // given
177        let yaml = r#"
178type: SlateDb
179path: my-data
180object_store:
181  type: Aws
182  region: us-west-2
183  bucket: my-bucket
184settings_path: slatedb.toml
185"#;
186
187        // when
188        let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
189
190        // then
191        match config {
192            StorageConfig::SlateDb(slate_config) => {
193                assert_eq!(slate_config.path, "my-data");
194                assert_eq!(
195                    slate_config.object_store,
196                    ObjectStoreConfig::Aws(AwsObjectStoreConfig {
197                        region: "us-west-2".to_string(),
198                        bucket: "my-bucket".to_string()
199                    })
200                );
201                assert_eq!(slate_config.settings_path, Some("slatedb.toml".to_string()));
202            }
203            _ => panic!("Expected SlateDb config"),
204        }
205    }
206
207    #[test]
208    fn should_deserialize_slatedb_config_with_in_memory_object_store() {
209        // given
210        let yaml = r#"
211type: SlateDb
212path: test-data
213object_store:
214  type: InMemory
215"#;
216
217        // when
218        let config: StorageConfig = serde_yaml::from_str(yaml).unwrap();
219
220        // then
221        match config {
222            StorageConfig::SlateDb(slate_config) => {
223                assert_eq!(slate_config.path, "test-data");
224                assert_eq!(slate_config.object_store, ObjectStoreConfig::InMemory);
225            }
226            _ => panic!("Expected SlateDb config"),
227        }
228    }
229
230    #[test]
231    fn should_serialize_slatedb_config() {
232        // given
233        let config = StorageConfig::SlateDb(SlateDbStorageConfig {
234            path: "my-data".to_string(),
235            object_store: ObjectStoreConfig::Local(LocalObjectStoreConfig {
236                path: "/tmp/slatedb".to_string(),
237            }),
238            settings_path: None,
239        });
240
241        // when
242        let yaml = serde_yaml::to_string(&config).unwrap();
243
244        // then
245        assert!(yaml.contains("type: SlateDb"));
246        assert!(yaml.contains("path: my-data"));
247        assert!(yaml.contains("type: Local"));
248        // settings_path should be omitted when None
249        assert!(!yaml.contains("settings_path"));
250    }
251}