vane 0.9.1

A flow-based reverse proxy with multi-layer routing and programmable pipelines.
/* src/common/config/loader.rs */

use crate::common::config::file_loader;
use fancy_log::{LogLevel, log};
use serde::de::DeserializeOwned;
use std::path::Path;
use tokio::fs;
use validator::Validate;

const EXTENSIONS: [&str; 4] = ["toml", "yaml", "yml", "json"];

pub enum LoadResult<T> {
	Ok(T),
	NotFound,
	Invalid,
}

pub trait PreProcess {
	fn pre_process(&mut self) {}
	fn set_context(&mut self, _context: &str) {}
}

pub async fn load_file<T>(path: &Path, context: Option<&str>) -> Option<T>
where
	T: DeserializeOwned + Validate + PreProcess,
{
	let config_dir = file_loader::get_config_dir();
	let root = fs::canonicalize(&config_dir)
		.await
		.unwrap_or_else(|_| config_dir.clone());

	let absolute_path = match fs::canonicalize(path).await {
		Ok(p) => p,
		Err(e) => {
			log(
				LogLevel::Error,
				&format!("✗ Failed to resolve config path {}: {}", path.display(), e),
			);
			return None;
		}
	};

	if !absolute_path.starts_with(&root) {
		log(
			LogLevel::Error,
			&format!(
				"✗ Security Violation: Config path {} is outside directory.",
				path.display()
			),
		);
		return None;
	}

	let content = match fs::read_to_string(&absolute_path).await {
		Ok(c) => c,
		Err(e) => {
			log(
				LogLevel::Error,
				&format!(
					"✗ Failed to read config file {}: {}",
					absolute_path.display(),
					e
				),
			);
			return None;
		}
	};

	let ext = absolute_path
		.extension()
		.and_then(|s| s.to_str())
		.unwrap_or("");

	let config_result: Result<T, String> = match ext {
		"toml" => toml::from_str(&content).map_err(|e| e.to_string()),
		"yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| e.to_string()),
		"json" => serde_json::from_str(&content).map_err(|e| e.to_string()),
		_ => return None,
	};

	match config_result {
		Ok(mut config) => {
			if let Some(ctx) = context {
				config.set_context(ctx);
			}
			config.pre_process();
			match config.validate() {
				Ok(_) => Some(config),
				Err(e) => {
					log(
						LogLevel::Error,
						&format!("✗ Validation failed for {}: {}", absolute_path.display(), e),
					);
					None
				}
			}
		}
		Err(e) => {
			log(
				LogLevel::Error,
				&format!(
					"✗ Failed to parse config file {}: {}",
					absolute_path.display(),
					e
				),
			);
			None
		}
	}
}

pub async fn load_config<T>(base_name: &str, base_path: &Path) -> LoadResult<T>
where
	T: DeserializeOwned + Validate + PreProcess,
{
	let mut matched_path = None;

	for ext in EXTENSIONS {
		let path = base_path.with_extension(ext);
		if fs::metadata(&path).await.is_ok() {
			if matched_path.is_some() {
				log(
					LogLevel::Warn,
					&format!(
						"✗ Multiple config files for {}. Ignoring: {}",
						base_name,
						path.display()
					),
				);
				continue;
			}
			matched_path = Some(path);
		}
	}

	match matched_path {
		Some(path) => match load_file(&path, Some(base_name)).await {
			Some(config) => LoadResult::Ok(config),
			None => LoadResult::Invalid,
		},
		None => LoadResult::NotFound,
	}
}