use std::ffi::OsString;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct FileConfig {
database_url: Option<String>,
bind: Option<String>,
web_dir: Option<String>,
cors_origins: Option<String>,
max_tenants: Option<i64>,
rust_log: Option<String>,
#[serde(default)]
embedding: EmbeddingConfig,
#[serde(default)]
logging: LoggingConfig,
#[serde(default)]
auth_rate_limit: AuthRateLimitConfig,
#[serde(default)]
db_load_guard: DbLoadGuardConfig,
search_tokens_per_minute: Option<u32>,
snapshot_retention_days: Option<i32>,
#[allow(dead_code)]
license_key: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct DbLoadGuardConfig {
threshold: Option<u32>,
poll_secs: Option<u64>,
sustain_secs: Option<u64>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct EmbeddingConfig {
provider: Option<String>,
dimensions: Option<u32>,
base_url: Option<String>,
model: Option<String>,
api_key: Option<String>,
send_dimensions_param: Option<bool>,
onnx_model_path: Option<String>,
onnx_tokenizer_path: Option<String>,
onnx_max_sequence_length: Option<u32>,
onnx_pooling: Option<String>,
onnx_query_instruction: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct LoggingConfig {
target: Option<String>,
dir: Option<String>,
syslog_socket: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct AuthRateLimitConfig {
max: Option<u32>,
window_secs: Option<u64>,
}
unsafe fn apply_if_unset(key: &str, value: Option<String>) {
if let Some(value) = value
&& std::env::var_os(key).is_none()
{
unsafe { std::env::set_var(key, value) };
}
}
pub const PACKAGED_CONFIG_PATH: &str = "/etc/yorishiro/config.yml";
pub(crate) fn config_path_from(
explicit: Option<OsString>,
exists: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
if let Some(named) = explicit {
let named = PathBuf::from(named);
return exists(&named).then_some(named);
}
let local = PathBuf::from("config.yml");
if exists(&local) {
return Some(local);
}
let packaged = PathBuf::from(PACKAGED_CONFIG_PATH);
exists(&packaged).then_some(packaged)
}
pub unsafe fn load_and_apply_env_overrides() -> Result<()> {
let explicit = std::env::var_os("YORISHIRO_CONFIG_PATH");
let Some(path) = config_path_from(explicit, |p| p.exists()) else {
return Ok(());
};
let path = path.as_path();
let contents = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config file '{}'", path.display()))?;
let config: FileConfig = serde_yaml_ng::from_str(&contents)
.with_context(|| format!("failed to parse config file '{}'", path.display()))?;
unsafe {
apply_if_unset("DATABASE_URL", config.database_url);
apply_if_unset("YORISHIRO_BIND", config.bind);
apply_if_unset("YORISHIRO_WEB_DIR", config.web_dir);
apply_if_unset("YORISHIRO_CORS_ORIGINS", config.cors_origins);
apply_if_unset(
"YORISHIRO_MAX_TENANTS",
config.max_tenants.map(|n| n.to_string()),
);
apply_if_unset("RUST_LOG", config.rust_log);
apply_if_unset("YORISHIRO_EMBEDDING_PROVIDER", config.embedding.provider);
apply_if_unset(
"YORISHIRO_EMBEDDING_DIMENSIONS",
config.embedding.dimensions.map(|n| n.to_string()),
);
apply_if_unset("YORISHIRO_EMBEDDING_BASE_URL", config.embedding.base_url);
apply_if_unset("YORISHIRO_EMBEDDING_MODEL", config.embedding.model);
apply_if_unset("YORISHIRO_EMBEDDING_API_KEY", config.embedding.api_key);
apply_if_unset(
"YORISHIRO_EMBEDDING_SEND_DIMENSIONS_PARAM",
config
.embedding
.send_dimensions_param
.map(|b| b.to_string()),
);
apply_if_unset(
"YORISHIRO_ONNX_MODEL_PATH",
config.embedding.onnx_model_path,
);
apply_if_unset("YORISHIRO_ONNX_POOLING", config.embedding.onnx_pooling);
apply_if_unset(
"YORISHIRO_ONNX_QUERY_INSTRUCTION",
config.embedding.onnx_query_instruction,
);
apply_if_unset(
"YORISHIRO_ONNX_TOKENIZER_PATH",
config.embedding.onnx_tokenizer_path,
);
apply_if_unset(
"YORISHIRO_ONNX_MAX_SEQUENCE_LENGTH",
config
.embedding
.onnx_max_sequence_length
.map(|n| n.to_string()),
);
apply_if_unset("YORISHIRO_LOG_TARGET", config.logging.target);
apply_if_unset("YORISHIRO_LOG_DIR", config.logging.dir);
apply_if_unset("YORISHIRO_SYSLOG_SOCKET", config.logging.syslog_socket);
apply_if_unset(
"YORISHIRO_DB_LOAD_THRESHOLD",
config.db_load_guard.threshold.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_DB_LOAD_POLL_SECS",
config.db_load_guard.poll_secs.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_DB_LOAD_SUSTAIN_SECS",
config.db_load_guard.sustain_secs.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_AUTH_RATE_LIMIT_MAX",
config.auth_rate_limit.max.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_AUTH_RATE_LIMIT_WINDOW_SECS",
config.auth_rate_limit.window_secs.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_SEARCH_TOKENS_PER_MINUTE",
config.search_tokens_per_minute.map(|n| n.to_string()),
);
apply_if_unset(
"YORISHIRO_SNAPSHOT_RETENTION_DAYS",
config.snapshot_retention_days.map(|n| n.to_string()),
);
}
Ok(())
}
#[cfg(test)]
#[path = "../../tests/config/mod.rs"]
mod tests;