use super::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub database_path: PathBuf,
#[serde(rename = "s3")]
pub s3: S3Config,
#[serde(default)]
pub retention: RetentionConfig,
#[serde(rename = "base", default)]
pub base: BaseConfig,
#[serde(rename = "segment", default)]
pub segment: SegmentConfig,
#[serde(default = "default_true")]
pub gzip: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct S3Config {
pub bucket: String,
pub prefix: String,
pub region: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_key_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_access_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetentionConfig {
#[serde(default = "default_retention_bases")]
pub bases: usize,
#[serde(default = "default_max_segments_bytes")]
pub max_segments_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseConfig {
#[serde(default = "default_base_interval")]
pub interval_minutes: u64,
#[serde(default = "default_base_segment_mb")]
pub segment_size_mb: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SegmentConfig {
#[serde(default = "default_segment_min_bytes")]
pub min_bytes: u64,
#[serde(default = "default_debounce_ms")]
pub debounce_ms: u64,
}
impl Default for RetentionConfig {
fn default() -> Self {
Self {
bases: default_retention_bases(),
max_segments_bytes: default_max_segments_bytes(),
}
}
}
impl Default for BaseConfig {
fn default() -> Self {
Self {
interval_minutes: default_base_interval(),
segment_size_mb: default_base_segment_mb(),
}
}
}
impl Default for SegmentConfig {
fn default() -> Self {
Self {
min_bytes: default_segment_min_bytes(),
debounce_ms: default_debounce_ms(),
}
}
}
fn default_true() -> bool {
true
}
fn default_retention_bases() -> usize {
3
}
fn default_max_segments_bytes() -> u64 {
100 * 1024 * 1024 * 1024 }
fn default_base_interval() -> u64 {
60 }
fn default_base_segment_mb() -> u64 {
100 }
fn default_segment_min_bytes() -> u64 {
1024 }
fn default_debounce_ms() -> u64 {
2000 }
impl Config {
pub fn from_file(path: &std::path::Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)
.map_err(|e| Error::Config(format!("Failed to parse config: {}", e)))?;
if !config.database_path.exists() {
return Err(Error::Config(format!(
"Database file does not exist: {}",
config.database_path.display()
)));
}
Ok(config)
}
pub fn state_file_path(&self) -> PathBuf {
if let Some(stem) = self.database_path.file_stem() {
self.database_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(format!("{}.stream.toml", stem.to_string_lossy()))
} else {
self.database_path.with_extension("stream.toml")
}
}
pub fn s3_prefix(&self) -> String {
format!(
"{}/{}",
self.s3.prefix,
self.database_path.file_name().unwrap().to_string_lossy()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_defaults() {
let config = Config {
database_path: PathBuf::from("/tmp/test.teg"),
s3: S3Config {
bucket: "my-bucket".to_string(),
prefix: "dbs".to_string(),
region: "us-east-1".to_string(),
endpoint: None,
access_key_id: None,
secret_access_key: None,
},
retention: RetentionConfig::default(),
base: BaseConfig::default(),
segment: SegmentConfig::default(),
gzip: true,
};
assert_eq!(config.retention.bases, 3);
assert_eq!(config.base.interval_minutes, 60);
}
}