#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod apply;
mod lex;
mod parse;
mod schema;
mod size;
pub use schema::{
AppendFsync, Config, ConfigError, EvictionPolicy, ExpirySection, LogLevel,
LogOutput, LogSection, MemorySection, PersistenceSection, ServerSection,
};
pub use size::parse_size;
use std::path::{Path, PathBuf};
const AUTODETECT_PATHS: &[&str] = &[
"./kevy.toml",
"/etc/kevy/kevy.toml",
];
impl Config {
pub fn load(path: Option<&Path>) -> Result<Self, ConfigError> {
if let Some(p) = path {
let text = read_required(p)?;
return Self::from_toml_str(&text, Some(p));
}
if let Some(p) = autodetect() {
let text = read_required(&p)?;
let mut cfg = Self::from_toml_str(&text, Some(&p))?;
cfg.source_path = Some(p);
return Ok(cfg);
}
Ok(Self::default())
}
pub fn from_toml_str(text: &str, source_path: Option<&Path>) -> Result<Self, ConfigError> {
let mut cfg = Self::default();
let items = parse::parse(text)?;
for item in items {
cfg.apply_item(item)?;
}
if let Some(p) = source_path {
cfg.source_path = Some(p.to_path_buf());
}
Ok(cfg)
}
pub fn merge_env<I, K, V>(&mut self, env: I) -> Result<(), ConfigError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
for (k, v) in env {
self.apply_env_var(k.as_ref(), v.as_ref())?;
}
Ok(())
}
pub fn merge_cli(&mut self, cli: CliOverrides) -> Result<(), ConfigError> {
if let Some(bind) = cli.bind {
self.server.bind = bind;
}
if let Some(port) = cli.port {
self.server.port = port;
}
if let Some(t) = cli.threads {
self.server.threads = t;
}
if let Some(d) = cli.data_dir {
self.server.data_dir = d;
}
if let Some(aof) = cli.aof {
self.persistence.aof = aof;
}
Ok(())
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct CliOverrides {
pub bind: Option<[u8; 4]>,
pub port: Option<u16>,
pub threads: Option<usize>,
pub data_dir: Option<PathBuf>,
pub aof: Option<bool>,
}
fn read_required(p: &Path) -> Result<String, ConfigError> {
std::fs::read_to_string(p).map_err(|e| ConfigError::IoOpen {
path: p.to_path_buf(),
err: e.to_string(),
})
}
fn autodetect() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("KEVY_DIR") {
let p = PathBuf::from(dir).join("kevy.toml");
if p.exists() {
return Some(p);
}
}
for relative in AUTODETECT_PATHS {
let p = PathBuf::from(relative);
if p.exists() {
return Some(p);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_documented_values() {
let cfg = Config::default();
assert_eq!(cfg.server.bind, [127, 0, 0, 1]);
assert_eq!(cfg.server.port, 6004);
assert_eq!(cfg.server.threads, 0);
assert!(cfg.persistence.aof);
assert_eq!(cfg.persistence.appendfsync, AppendFsync::EverySec);
assert_eq!(cfg.memory.maxmemory, 0);
assert_eq!(cfg.memory.maxmemory_policy, EvictionPolicy::NoEviction);
assert_eq!(cfg.expiry.hz, 10);
assert_eq!(cfg.expiry.sample, 20);
assert_eq!(cfg.log.level, LogLevel::Info);
}
#[test]
fn cli_overrides_apply_in_order() {
let mut cfg = Config::default();
let cli = CliOverrides {
bind: Some([0, 0, 0, 0]),
port: Some(7000),
threads: Some(4),
..CliOverrides::default()
};
cfg.merge_cli(cli).unwrap();
assert_eq!(cfg.server.bind, [0, 0, 0, 0]);
assert_eq!(cfg.server.port, 7000);
assert_eq!(cfg.server.threads, 4);
}
#[test]
fn env_overrides_apply() {
let mut cfg = Config::default();
cfg.merge_env([
("KEVY_BIND", "0.0.0.0"),
("KEVY_PORT", "7001"),
("UNRELATED_VAR", "ignored"),
])
.unwrap();
assert_eq!(cfg.server.bind, [0, 0, 0, 0]);
assert_eq!(cfg.server.port, 7001);
}
}