Skip to main content

sir_eel/
config.rs

1mod error;
2
3pub use error::Error as ConfigError;
4use error::{Error, Result};
5use serde::{Deserialize, Serialize};
6use std::{
7    fs,
8    path::{Path, PathBuf},
9};
10
11/// Configuration for the migration tool
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Config {
14    /// The SurrealDB connection URL (e.g. "rocksdb://data.db" or "ws://localhost:8000")
15    pub db_url: String,
16
17    /// The namespace to use for migrations
18    pub namespace: String,
19
20    /// The database name to apply migrations to
21    pub database: String,
22
23    /// Directory containing migration files
24    pub migrations_dir: PathBuf,
25}
26
27impl Config {
28    /// Create a new configuration builder
29    pub fn builder() -> ConfigBuilder {
30        ConfigBuilder::default()
31    }
32
33    pub fn from_file(file: &Path) -> Result<Self> {
34        let contents = fs::read_to_string(file).map_err(|_| Error::FileUnreadable)?;
35        let parsed: toml::Value =
36            toml::from_str(&contents).map_err(|_| Error::FileFormatUnrecognized)?;
37        let conf = parsed.get("sir-eel").ok_or(Error::FileFormatUnrecognized)?;
38        let db_url = conf
39            .get("db_url")
40            .and_then(|v| v.as_str())
41            .ok_or(Error::MissingProperty("Missing 'db_url'".to_string()))?
42            .to_string();
43        let database = conf
44            .get("database")
45            .and_then(|v| v.as_str())
46            .ok_or(Error::MissingProperty("Missing 'database'".to_string()))?
47            .to_string();
48        let namespace = conf
49            .get("namespace")
50            .and_then(|v| v.as_str())
51            .ok_or(Error::MissingProperty("Missing 'namespace'".to_string()))?
52            .to_string();
53        let migrations_dir = conf
54            .get("migrations_dir")
55            .and_then(|v| v.as_str())
56            .map(|dir| PathBuf::from(dir.to_string()))
57            .ok_or(Error::MissingProperty(
58                "Missing 'migrations_dir'".to_string(),
59            ))?;
60        Ok(Self {
61            db_url,
62            database,
63            namespace,
64            migrations_dir,
65        })
66    }
67}
68
69/// Builder for creating Config instances
70#[derive(Default)]
71pub struct ConfigBuilder {
72    db_url: Option<String>,
73    namespace: Option<String>,
74    database: Option<String>,
75    migrations_dir: Option<PathBuf>,
76}
77
78impl ConfigBuilder {
79    /// Set the database URL
80    pub fn db_url(mut self, url: impl Into<String>) -> Self {
81        self.db_url = Some(url.into());
82        self
83    }
84
85    /// Set the namespace
86    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
87        self.namespace = Some(namespace.into());
88        self
89    }
90
91    /// Set the database name
92    pub fn database(mut self, database: impl Into<String>) -> Self {
93        self.database = Some(database.into());
94        self
95    }
96
97    /// Set the migrations directory
98    pub fn migration_dir(mut self, dir: impl Into<PathBuf>) -> Self {
99        self.migrations_dir = Some(dir.into());
100        self
101    }
102
103    /// Build the configuration
104    pub fn build(self) -> Result<Config> {
105        Ok(Config {
106            db_url: self
107                .db_url
108                .ok_or(Error::MissingProperty("Missing 'db_url'".into()))?,
109            namespace: self
110                .namespace
111                .ok_or(Error::MissingProperty("Missing 'namespace'".into()))?,
112            database: self
113                .database
114                .ok_or(Error::MissingProperty("Missing 'database'".into()))?,
115            migrations_dir: self.migrations_dir.unwrap_or(PathBuf::from("migrations")),
116        })
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_builder_ok() -> Result<()> {
126        let config = Config::builder()
127            .db_url("rocksdb://test")
128            .database("testdb")
129            .namespace("testns")
130            .build()?;
131
132        assert_eq!(config.db_url, "rocksdb://test");
133
134        Ok(())
135    }
136
137    #[test]
138    fn test_builder_err_missing_db_url() -> Result<()> {
139        let res = Config::builder()
140            .database("testdb")
141            .namespace("testns")
142            .build();
143
144        assert!(matches!(
145            res,
146            Err(Error::MissingProperty(msg)) if msg == "Missing 'db_url'"
147        ));
148        Ok(())
149    }
150
151    #[test]
152    fn test_builder_err_missing_database() -> Result<()> {
153        let res = Config::builder()
154            .db_url("rocksdb://testdata")
155            .namespace("testns")
156            .build();
157
158        assert!(matches!(
159            res,
160            Err(Error::MissingProperty(msg)) if msg == "Missing 'database'"
161        ));
162        Ok(())
163    }
164
165    #[test]
166    fn test_builder_err_missing_namespace() -> Result<()> {
167        let res = Config::builder()
168            .db_url("rocksdb://testdata")
169            .database("testdb")
170            .build();
171
172        assert!(matches!(
173            res,
174            Err(Error::MissingProperty(msg)) if msg == "Missing 'namespace'"
175        ));
176        Ok(())
177    }
178
179    #[test]
180    fn test_from_file_ok() -> Result<()> {
181        let filepath = Path::new("sir-eel.test.toml");
182        let config = Config::from_file(filepath)?;
183
184        assert_eq!(config.db_url, "rocksdb://testdata");
185        assert_eq!(config.database, "testdatabase");
186        assert_eq!(config.namespace, "testnamespace");
187        assert_eq!(config.migrations_dir, PathBuf::from("migrations"));
188
189        Ok(())
190    }
191}