use serde::{Deserialize, Serialize}; use std::time::Duration; use crate::errors::{BlahgError, Result}; /// Application configuration loaded from environment variables. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// HTTP server configuration pub http: HttpConfig, /// Base URL for external links and content pub external_base: String, /// Certificate bundles for TLS verification pub certificate_bundles: Vec, /// User agent string for HTTP requests pub user_agent: String, /// Hostname for PLC directory lookups pub plc_hostname: String, /// DNS nameservers for resolution pub dns_nameservers: Vec, /// HTTP client timeout duration pub http_client_timeout: Duration, /// Author name for the blog pub author: String, /// Storage path for attachments pub attachment_storage: String, /// Database connection URL pub database_url: String, /// Path to jetstream cursor file pub jetstream_cursor_path: Option, /// Whether to enable jetstream event consumption pub enable_jetstream: bool, /// Syntect theme for syntax highlighting pub syntect_theme: String, } /// HTTP server configuration options. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpConfig { /// Port to bind the HTTP server to pub port: u16, /// Path to static assets pub static_path: String, /// Path to template files pub templates_path: String, } impl Default for Config { fn default() -> Self { Self { http: HttpConfig::default(), external_base: "http://localhost:8080".to_string(), certificate_bundles: Vec::new(), user_agent: format!( "Blahg/{} (+https://tangled.sh/@smokesignal.events/blahg)", env!("CARGO_PKG_VERSION") ), plc_hostname: "plc.directory".to_string(), dns_nameservers: Vec::new(), http_client_timeout: Duration::from_secs(10), author: String::new(), attachment_storage: "./attachments".to_string(), database_url: "sqlite://blahg.db".to_string(), jetstream_cursor_path: None, enable_jetstream: true, syntect_theme: std::env::var("SYNTECT_THEME").unwrap_or_else(|_| "base16-ocean.dark".to_string()), } } } impl Default for HttpConfig { fn default() -> Self { Self { port: 8080, static_path: format!("{}/static", env!("CARGO_MANIFEST_DIR")), templates_path: format!("{}/templates", env!("CARGO_MANIFEST_DIR")), } } } impl Config { /// Create configuration from environment variables. pub fn from_env() -> Result { let mut config = Self::default(); if let Ok(port) = std::env::var("HTTP_PORT") { config.http.port = port .parse() .map_err(|_| BlahgError::ConfigHttpPortInvalid { port: port.clone() })?; } if let Ok(static_path) = std::env::var("HTTP_STATIC_PATH") { config.http.static_path = static_path; } if let Ok(templates_path) = std::env::var("HTTP_TEMPLATES_PATH") { config.http.templates_path = templates_path; } if let Ok(external_base) = std::env::var("EXTERNAL_BASE") { config.external_base = external_base; } if let Ok(cert_bundles) = std::env::var("CERTIFICATE_BUNDLES") { config.certificate_bundles = cert_bundles .split(';') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); } if let Ok(user_agent) = std::env::var("USER_AGENT") { config.user_agent = user_agent; } if let Ok(plc_hostname) = std::env::var("PLC_HOSTNAME") { config.plc_hostname = plc_hostname; } if let Ok(dns_nameservers) = std::env::var("DNS_NAMESERVERS") { config.dns_nameservers = dns_nameservers .split(',') .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| { s.parse::() .map_err(|e| BlahgError::NameserverParsingFailed(s.to_string(), e)) }) .collect::>>()?; } if let Ok(timeout) = std::env::var("HTTP_CLIENT_TIMEOUT") { config.http_client_timeout = duration_str::parse(&timeout).map_err(|e| { BlahgError::ConfigHttpTimeoutInvalid { details: e.to_string(), } })?; } if let Ok(author) = std::env::var("AUTHOR") { config.author = author; } if let Ok(attachment_storage) = std::env::var("ATTACHMENT_STORAGE") { config.attachment_storage = attachment_storage; } if let Ok(database_url) = std::env::var("DATABASE_URL") { config.database_url = database_url; } if let Ok(jetstream_cursor_path) = std::env::var("JETSTREAM_CURSOR_PATH") { config.jetstream_cursor_path = Some(jetstream_cursor_path); } if let Ok(enable_jetstream) = std::env::var("ENABLE_JETSTREAM") { config.enable_jetstream = enable_jetstream.parse().unwrap_or(true); } if let Ok(syntect_theme) = std::env::var("SYNTECT_THEME") { config.syntect_theme = syntect_theme; } Ok(config) } }