use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
use crate::config::schema::ServerConfig;
use crate::config::sources::{merge_json, resolve_source, ConfigSource};
use crate::error::PodError;
#[derive(Clone)]
pub struct ConfigLoader {
sources: Vec<ConfigSource>,
warnings: Vec<String>,
}
impl Default for ConfigLoader {
fn default() -> Self {
Self::new()
}
}
impl ConfigLoader {
pub fn new() -> Self {
Self {
sources: Vec::new(),
warnings: Vec::new(),
}
}
pub fn with_defaults(mut self) -> Self {
if !self
.sources
.iter()
.any(|s| matches!(s, ConfigSource::Defaults))
{
self.sources.push(ConfigSource::Defaults);
}
self
}
pub fn with_file(mut self, path: impl Into<PathBuf>) -> Self {
self.sources.push(ConfigSource::File(path.into()));
self
}
pub fn with_env(mut self) -> Self {
self.sources.push(ConfigSource::EnvVars);
self
}
pub fn with_env_overlay(&mut self) -> &mut Self {
if !self
.sources
.iter()
.any(|s| matches!(s, ConfigSource::EnvVars))
{
self.sources.push(ConfigSource::EnvVars);
}
self
}
pub fn with_cli_overlay(&mut self, args: &CliArgs) -> &mut Self {
self.sources
.push(ConfigSource::CliOverlay(args.to_overlay()));
self
}
pub fn from_file<P: AsRef<Path>>(path: P) -> impl std::future::Future<Output = Result<ServerConfig, PodError>> {
let p = path.as_ref().to_path_buf();
async move {
ConfigLoader::new()
.with_defaults()
.with_file(p)
.load()
.await
}
}
pub async fn load(mut self) -> Result<ServerConfig, PodError> {
if self.sources.is_empty() {
self.sources.push(ConfigSource::Defaults);
}
let mut tree = Value::Object(Default::default());
for source in &self.sources {
let overlay = resolve_source(source)?;
merge_json(&mut tree, overlay);
if let ConfigSource::EnvVars = source {
let type_is_memory = tree
.get("storage")
.and_then(|s| s.get("type"))
.and_then(|t| t.as_str())
== Some("memory");
let root_was_set = std::env::var("JSS_STORAGE_ROOT").is_ok()
|| std::env::var("JSS_ROOT").is_ok();
if type_is_memory && root_was_set {
self.warnings.push(
"JSS_STORAGE_TYPE=memory with JSS_STORAGE_ROOT/JSS_ROOT set: \
memory backend wins, root ignored"
.to_string(),
);
}
}
}
for w in &self.warnings {
tracing::warn!(target: "solid_pod_rs::config", "{w}");
}
let cfg: ServerConfig = serde_json::from_value(tree).map_err(|e| {
PodError::Backend(format!("config merge produced invalid shape: {e}"))
})?;
cfg.validate().map_err(PodError::Backend)?;
Ok(cfg)
}
pub fn warnings(&self) -> &[String] {
&self.warnings
}
}
#[derive(Debug, Clone, Default)]
pub struct CliArgs {
pub host: Option<String>,
pub port: Option<u16>,
pub base_url: Option<String>,
pub storage_root: Option<String>,
pub storage_type: Option<String>,
pub oidc_enabled: Option<bool>,
pub oidc_issuer: Option<String>,
pub nip98_enabled: Option<bool>,
pub base_domain: Option<String>,
pub subdomains_enabled: Option<bool>,
}
impl CliArgs {
pub(crate) fn to_overlay(&self) -> Value {
let mut out = Map::new();
let mut server = Map::new();
let mut storage = Map::new();
let mut auth = Map::new();
let mut extras = Map::new();
if let Some(v) = &self.host {
server.insert("host".into(), Value::String(v.clone()));
}
if let Some(v) = self.port {
server.insert("port".into(), Value::Number(v.into()));
}
if let Some(v) = &self.base_url {
server.insert("base_url".into(), Value::String(v.clone()));
}
if let Some(v) = &self.storage_type {
storage.insert("type".into(), Value::String(v.clone()));
}
if let Some(v) = &self.storage_root {
storage.insert("type".into(), Value::String("fs".into()));
storage.insert("root".into(), Value::String(v.clone()));
}
if let Some(v) = self.oidc_enabled {
auth.insert("oidc_enabled".into(), Value::Bool(v));
}
if let Some(v) = &self.oidc_issuer {
auth.insert("oidc_issuer".into(), Value::String(v.clone()));
}
if let Some(v) = self.nip98_enabled {
auth.insert("nip98_enabled".into(), Value::Bool(v));
}
if let Some(v) = &self.base_domain {
extras.insert("base_domain".into(), Value::String(v.clone()));
}
if let Some(v) = self.subdomains_enabled {
extras.insert("subdomains_enabled".into(), Value::Bool(v));
}
if !server.is_empty() {
out.insert("server".into(), Value::Object(server));
}
if !storage.is_empty() {
out.insert("storage".into(), Value::Object(storage));
}
if !auth.is_empty() {
out.insert("auth".into(), Value::Object(auth));
}
if !extras.is_empty() {
out.insert("extras".into(), Value::Object(extras));
}
Value::Object(out)
}
}