use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
static SETTINGS: OnceLock<ApcoreSettings> = OnceLock::new();
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApcoreSettings {
pub module_dir: PathBuf,
pub auto_discover: bool,
pub binding_pattern: String,
pub module_packages: Vec<String>,
pub serve_transport: String,
pub serve_host: String,
pub serve_port: u16,
pub server_name: String,
pub explorer_prefix: String,
pub explorer_enabled: bool,
pub jwt_secret: Option<String>,
pub jwt_algorithm: String,
pub acl_path: Option<String>,
pub tracing: bool,
pub metrics: bool,
pub observability_logging: bool,
pub embedded_server: bool,
pub task_max_concurrent: usize,
pub task_max_tasks: usize,
pub task_cleanup_age: u64,
pub scanner_source: String,
pub hot_reload: bool,
pub output_formatter: Option<String>,
pub extra: HashMap<String, serde_json::Value>,
}
impl Default for ApcoreSettings {
fn default() -> Self {
Self {
module_dir: PathBuf::from("apcore_modules"),
auto_discover: true,
binding_pattern: "*.binding.yaml".into(),
module_packages: vec![],
serve_transport: "streamable-http".into(),
serve_host: "127.0.0.1".into(),
serve_port: 9090,
server_name: "axum-apcore".into(),
explorer_prefix: "/explorer".into(),
explorer_enabled: true,
jwt_secret: None,
jwt_algorithm: "HS256".into(),
acl_path: None,
tracing: false,
metrics: false,
observability_logging: false,
embedded_server: false,
task_max_concurrent: 10,
task_max_tasks: 100,
task_cleanup_age: 3600,
scanner_source: "native".into(),
hot_reload: false,
output_formatter: None,
extra: HashMap::new(),
}
}
}
impl ApcoreSettings {
pub fn from_env() -> Self {
let mut settings = Self::default();
if let Ok(v) = std::env::var("APCORE_MODULE_DIR") {
settings.module_dir = PathBuf::from(v);
}
if let Ok(v) = std::env::var("APCORE_AUTO_DISCOVER") {
settings.auto_discover = parse_bool(&v);
}
if let Ok(v) = std::env::var("APCORE_BINDING_PATTERN") {
settings.binding_pattern = v;
}
if let Ok(v) = std::env::var("APCORE_MODULE_PACKAGES") {
settings.module_packages = v.split(',').map(|s| s.trim().to_string()).collect();
}
if let Ok(v) = std::env::var("APCORE_SERVE_TRANSPORT") {
settings.serve_transport = v;
}
if let Ok(v) = std::env::var("APCORE_SERVE_HOST") {
settings.serve_host = v;
}
if let Ok(v) = std::env::var("APCORE_SERVE_PORT") {
if let Ok(port) = v.parse() {
settings.serve_port = port;
}
}
if let Ok(v) = std::env::var("APCORE_SERVER_NAME") {
settings.server_name = v;
}
if let Ok(v) = std::env::var("APCORE_EXPLORER_PREFIX") {
settings.explorer_prefix = v;
}
if let Ok(v) = std::env::var("APCORE_EXPLORER_ENABLED") {
settings.explorer_enabled = parse_bool(&v);
}
settings.jwt_secret = std::env::var("APCORE_JWT_SECRET").ok();
if let Ok(v) = std::env::var("APCORE_JWT_ALGORITHM") {
settings.jwt_algorithm = v;
}
settings.acl_path = std::env::var("APCORE_ACL_PATH").ok();
if let Ok(v) = std::env::var("APCORE_TRACING") {
settings.tracing = parse_bool(&v);
}
if let Ok(v) = std::env::var("APCORE_METRICS") {
settings.metrics = parse_bool(&v);
}
if let Ok(v) = std::env::var("APCORE_OBSERVABILITY_LOGGING") {
settings.observability_logging = parse_bool(&v);
}
if let Ok(v) = std::env::var("APCORE_EMBEDDED_SERVER") {
settings.embedded_server = parse_bool(&v);
}
if let Ok(v) = std::env::var("APCORE_TASK_MAX_CONCURRENT") {
if let Ok(n) = v.parse() {
settings.task_max_concurrent = n;
}
}
if let Ok(v) = std::env::var("APCORE_TASK_MAX_TASKS") {
if let Ok(n) = v.parse() {
settings.task_max_tasks = n;
}
}
if let Ok(v) = std::env::var("APCORE_TASK_CLEANUP_AGE") {
if let Ok(n) = v.parse() {
settings.task_cleanup_age = n;
}
}
if let Ok(v) = std::env::var("APCORE_SCANNER_SOURCE") {
settings.scanner_source = v;
}
if let Ok(v) = std::env::var("APCORE_HOT_RELOAD") {
settings.hot_reload = parse_bool(&v);
}
settings.output_formatter = std::env::var("APCORE_OUTPUT_FORMATTER").ok();
settings
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let valid_transports = ["stdio", "streamable-http", "sse"];
if !valid_transports.contains(&self.serve_transport.as_str()) {
errors.push(format!(
"Invalid APCORE_SERVE_TRANSPORT: '{}'. Must be one of: {}",
self.serve_transport,
valid_transports.join(", ")
));
}
let valid_sources = ["native", "openapi"];
if !valid_sources.contains(&self.scanner_source.as_str()) {
errors.push(format!(
"Invalid APCORE_SCANNER_SOURCE: '{}'. Must be one of: {}",
self.scanner_source,
valid_sources.join(", ")
));
}
if self.serve_port == 0 {
errors.push("APCORE_SERVE_PORT must be > 0".into());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
pub fn get_apcore_settings() -> &'static ApcoreSettings {
SETTINGS.get_or_init(|| {
let settings = ApcoreSettings::from_env();
if let Err(errors) = settings.validate() {
tracing::warn!("ApcoreSettings validation warnings: {:?}", errors);
}
settings
})
}
fn parse_bool(s: &str) -> bool {
matches!(s.to_lowercase().as_str(), "1" | "true" | "yes" | "on")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_settings() {
let s = ApcoreSettings::default();
assert!(s.auto_discover);
assert_eq!(s.serve_port, 9090);
assert_eq!(s.serve_transport, "streamable-http");
assert_eq!(s.scanner_source, "native");
}
#[test]
fn test_validate_valid() {
let s = ApcoreSettings::default();
assert!(s.validate().is_ok());
}
#[test]
fn test_validate_invalid_transport() {
let s = ApcoreSettings {
serve_transport: "invalid".into(),
..ApcoreSettings::default()
};
let err = s.validate().unwrap_err();
assert!(err[0].contains("APCORE_SERVE_TRANSPORT"));
}
#[test]
fn test_validate_invalid_scanner() {
let s = ApcoreSettings {
scanner_source: "bad".into(),
..ApcoreSettings::default()
};
let err = s.validate().unwrap_err();
assert!(err[0].contains("APCORE_SCANNER_SOURCE"));
}
#[test]
fn test_parse_bool() {
assert!(parse_bool("true"));
assert!(parse_bool("1"));
assert!(parse_bool("yes"));
assert!(parse_bool("on"));
assert!(parse_bool("TRUE"));
assert!(!parse_bool("false"));
assert!(!parse_bool("0"));
assert!(!parse_bool(""));
}
}