use super::validation::validate_config_security;
use super::Config;
use serde_json::Value;
use std::fs;
use std::path::Path;
#[must_use]
pub fn load() -> Config {
use std::env;
use std::io::Read;
use std::path::PathBuf;
let defaults = Config::default();
let mut merged =
serde_json::to_value(&defaults).unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
if let Ok(json) = env::var("SIGNAL_FISH_CONFIG_JSON") {
if let Some(value) = parse_json_document(&json, "SIGNAL_FISH_CONFIG_JSON") {
merge_values(&mut merged, value);
}
}
if let Ok(val) = env::var("SIGNAL_FISH_CONFIG_STDIN") {
if env_var_truthy(&val) {
let mut buf = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut buf) {
eprintln!("Failed to read config from stdin: {e}");
} else if let Some(value) = parse_json_document(&buf, "stdin") {
merge_values(&mut merged, value);
}
}
}
if let Ok(path) = env::var("SIGNAL_FISH_CONFIG_PATH") {
let path = PathBuf::from(path);
merge_file_source(&mut merged, &path);
}
merge_file_source(&mut merged, &PathBuf::from("config.json"));
if let Ok(exe_path) = env::current_exe() {
if let Some(mut exe_dir) = exe_path.parent().map(std::path::Path::to_path_buf) {
exe_dir.push("config.json");
merge_file_source(&mut merged, &exe_dir);
}
}
apply_env_overrides(&mut merged);
let config = match serde_json::from_value::<Config>(merged) {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("Failed to deserialize config; using defaults: {e}");
defaults
}
};
if let Err(e) = validate_config_security(&config) {
eprintln!("Configuration validation error: {e}");
}
config
}
fn parse_json_document(raw: &str, label: &str) -> Option<Value> {
if raw.trim().is_empty() {
return None;
}
match serde_json::from_str(raw) {
Ok(value) => Some(value),
Err(err) => {
eprintln!("Failed to parse config from {label}: {err}");
None
}
}
}
fn merge_file_source(target: &mut Value, path: &Path) {
if path.as_os_str().is_empty() || !path.exists() {
return;
}
match fs::read_to_string(path) {
Ok(contents) => {
if let Some(value) = parse_json_document(&contents, &format!("file {}", path.display()))
{
merge_values(target, value);
}
}
Err(err) => {
eprintln!("Failed to read config from {}: {}", path.display(), err);
}
}
}
fn merge_values(target: &mut Value, source: Value) {
match (target, source) {
(Value::Object(target_map), Value::Object(source_map)) => {
for (key, value) in source_map {
match target_map.get_mut(&key) {
Some(existing) => merge_values(existing, value),
None => {
target_map.insert(key, value);
}
}
}
}
(target_slot, source_value) => {
*target_slot = source_value;
}
}
}
fn apply_env_overrides(root: &mut Value) {
for (key, raw_value) in std::env::vars() {
let Some(stripped) = key.strip_prefix("SIGNAL_FISH__") else {
continue;
};
let segments: Vec<String> = stripped
.split("__")
.filter(|segment| !segment.is_empty())
.map(str::to_ascii_lowercase)
.collect();
if segments.is_empty() {
continue;
}
let value = parse_env_value(&raw_value);
set_nested_value(root, &segments, value);
}
}
fn env_var_truthy(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
)
}
fn parse_env_value(raw: &str) -> Value {
let trimmed = raw.trim();
if trimmed.contains(',') {
let items = trimmed
.split(',')
.map(|segment| parse_scalar(segment.trim()))
.collect::<Vec<_>>();
return Value::Array(items);
}
parse_scalar(trimmed)
}
fn parse_scalar(raw: &str) -> Value {
if raw.is_empty() {
return Value::String(String::new());
}
serde_json::from_str(raw).unwrap_or_else(|_| Value::String(raw.to_string()))
}
fn set_nested_value(target: &mut Value, segments: &[String], value: Value) {
if segments.is_empty() {
*target = value;
return;
}
if segments.len() == 1 {
let map = ensure_object(target);
#[allow(clippy::indexing_slicing)]
map.insert(segments[0].clone(), value);
return;
}
let map = ensure_object(target);
#[allow(clippy::indexing_slicing)]
let key = segments[0].clone();
let entry = map
.entry(key)
.or_insert_with(|| Value::Object(serde_json::Map::new()));
#[allow(clippy::indexing_slicing)]
let rest = &segments[1..];
set_nested_value(entry, rest, value);
}
fn ensure_object(value: &mut Value) -> &mut serde_json::Map<String, Value> {
if !value.is_object() {
*value = Value::Object(serde_json::Map::new());
}
#[allow(clippy::expect_used)]
value
.as_object_mut()
.expect("value should be coerced into an object")
}