aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.rs')
-rw-r--r--src/config.rs104+104 −0
1 files changed, 104 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use serde::Deserialize;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum ConfigError {
+ #[error("failed to read config file: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("failed to parse config file: {0}")]
+ Parse(#[from] toml::de::Error),
+
+ #[error("appname '{0}' contains invalid characters (only a-z, A-Z, 0-9, -, _ allowed)")]
+ InvalidAppname(String),
+
+ #[error("dirname '{0}' does not exist")]
+ DirNotFound(PathBuf),
+
+ #[error("dirname '{0}' is not a directory")]
+ NotADirectory(PathBuf),
+
+ #[error("dirname '{0}' is not writable: {1}")]
+ NotWritable(PathBuf, std::io::Error),
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(default)]
+pub struct ConfigService {
+ listen: String,
+ appname: String,
+ dirname: PathBuf,
+}
+
+impl Default for ConfigService {
+ fn default() -> Self {
+ Self {
+ listen: "0.0.0.0:3000".to_string(),
+ appname: "zorian".to_string(),
+ dirname: PathBuf::from("./.zorian-state"),
+ }
+ }
+}
+
+impl ConfigService {
+ pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
+ let content = fs::read_to_string(path)?;
+ let config: Self = toml::from_str(&content)?;
+ Ok(config)
+ }
+
+ pub fn validate(&self) -> Result<(), ConfigError> {
+ let mut chars = self.appname.chars();
+ if !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
+ return Err(ConfigError::InvalidAppname(self.appname.clone()));
+ }
+
+ let metadata = fs::metadata(&self.dirname).map_err(|e| {
+ if e.kind() == std::io::ErrorKind::NotFound {
+ ConfigError::DirNotFound(self.dirname.clone())
+ } else {
+ ConfigError::Io(e)
+ }
+ })?;
+
+ if !metadata.is_dir() {
+ return Err(ConfigError::NotADirectory(self.dirname.clone()));
+ }
+
+ let testfile = self.dirname.join(".health");
+ fs::write(&testfile, std::process::id().to_string())
+ .map_err(|e| ConfigError::NotWritable(self.dirname.clone(), e))?;
+ fs::remove_file(&testfile)?;
+
+ Ok(())
+ }
+
+ pub fn appname(&self) -> &str {
+ &self.appname
+ }
+
+ pub fn listen(&self) -> &str {
+ &self.listen
+ }
+
+ pub fn dirname(&self) -> &Path {
+ &self.dirname
+ }
+}
+
+#[cfg(test)]
+impl ConfigService {
+ pub fn for_test(dirname: PathBuf) -> Self {
+ Self {
+ listen: "127.0.0.1:0".to_string(),
+ appname: "test".to_string(),
+ dirname,
+ }
+ }
+}