A Rust application to showcase badge awards in the AT Protocol ecosystem.
at main 6.0 kB view raw
1//! Configuration management for environment variables and application settings. 2//! 3//! Loads configuration from environment variables with sensible defaults 4//! for HTTP server, AT Protocol, and storage settings. 5 6use serde::{Deserialize, Serialize}; 7use std::time::Duration; 8 9use crate::errors::{Result, ShowcaseError}; 10 11/// Application configuration loaded from environment variables. 12#[derive(Debug, Clone, Serialize, Deserialize)] 13pub struct Config { 14 /// HTTP server configuration. 15 pub http: HttpConfig, 16 /// External base URL for the application. 17 pub external_base: String, 18 /// Certificate bundles for TLS verification. 19 pub certificate_bundles: Vec<String>, 20 /// User agent string for HTTP requests. 21 pub user_agent: String, 22 /// PLC server hostname for identity resolution. 23 pub plc_hostname: String, 24 /// DNS nameservers for domain resolution. 25 pub dns_nameservers: Vec<String>, 26 /// HTTP client timeout duration. 27 pub http_client_timeout: Duration, 28 /// List of trusted badge issuer DIDs. 29 pub badge_issuers: Vec<String>, 30 /// Directory path for badge image storage. 31 pub badge_image_storage: String, 32 /// Database connection URL. 33 pub database_url: String, 34 /// Optional path for persisting Jetstream cursor state. 35 pub jetstream_cursor_path: Option<String>, 36} 37 38/// HTTP server configuration. 39#[derive(Debug, Clone, Serialize, Deserialize)] 40pub struct HttpConfig { 41 /// HTTP server port. 42 pub port: u16, 43 /// Path to static assets directory. 44 pub static_path: String, 45 /// Path to templates directory. 46 pub templates_path: String, 47} 48 49impl Default for Config { 50 fn default() -> Self { 51 Self { 52 http: HttpConfig::default(), 53 external_base: "http://localhost:8080".to_string(), 54 certificate_bundles: Vec::new(), 55 user_agent: format!( 56 "showcase/{} (+https://tangled.sh/@smokesignal.events/showcase)", 57 env!("CARGO_PKG_VERSION") 58 ), 59 plc_hostname: "plc.directory".to_string(), 60 dns_nameservers: Vec::new(), 61 http_client_timeout: Duration::from_secs(10), 62 badge_issuers: Vec::new(), 63 badge_image_storage: "./badges".to_string(), 64 database_url: "sqlite://showcase.db".to_string(), 65 jetstream_cursor_path: None, 66 } 67 } 68} 69 70impl Default for HttpConfig { 71 fn default() -> Self { 72 Self { 73 port: 8080, 74 static_path: format!("{}/static", env!("CARGO_MANIFEST_DIR")), 75 templates_path: format!("{}/templates", env!("CARGO_MANIFEST_DIR")), 76 } 77 } 78} 79 80impl Config { 81 /// Create configuration from environment variables. 82 pub fn from_env() -> Result<Self> { 83 let mut config = Self::default(); 84 85 if let Ok(port) = std::env::var("HTTP_PORT") { 86 config.http.port = port 87 .parse() 88 .map_err(|_| ShowcaseError::ConfigHttpPortInvalid { port: port.clone() })?; 89 } 90 91 if let Ok(static_path) = std::env::var("HTTP_STATIC_PATH") { 92 config.http.static_path = static_path; 93 } 94 95 if let Ok(templates_path) = std::env::var("HTTP_TEMPLATES_PATH") { 96 config.http.templates_path = templates_path; 97 } 98 99 if let Ok(external_base) = std::env::var("EXTERNAL_BASE") { 100 config.external_base = external_base; 101 } 102 103 if let Ok(cert_bundles) = std::env::var("CERTIFICATE_BUNDLES") { 104 config.certificate_bundles = cert_bundles 105 .split(';') 106 .map(|s| s.trim().to_string()) 107 .filter(|s| !s.is_empty()) 108 .collect(); 109 } 110 111 if let Ok(user_agent) = std::env::var("USER_AGENT") { 112 config.user_agent = user_agent; 113 } 114 115 if let Ok(plc_hostname) = std::env::var("PLC_HOSTNAME") { 116 config.plc_hostname = plc_hostname; 117 } 118 119 if let Ok(dns_nameservers) = std::env::var("DNS_NAMESERVERS") { 120 config.dns_nameservers = dns_nameservers 121 .split(';') 122 .map(|s| s.trim().to_string()) 123 .filter(|s| !s.is_empty()) 124 .collect(); 125 } 126 127 if let Ok(timeout) = std::env::var("HTTP_CLIENT_TIMEOUT") { 128 config.http_client_timeout = duration_str::parse(&timeout).map_err(|e| { 129 ShowcaseError::ConfigHttpTimeoutInvalid { 130 details: e.to_string(), 131 } 132 })?; 133 } 134 135 if let Ok(badge_issuers) = std::env::var("BADGE_ISSUERS") { 136 config.badge_issuers = badge_issuers 137 .split(';') 138 .map(|s| s.trim().to_string()) 139 .filter(|s| !s.is_empty()) 140 .collect(); 141 } 142 143 if let Ok(badge_image_storage) = std::env::var("BADGE_IMAGE_STORAGE") { 144 config.badge_image_storage = badge_image_storage; 145 } 146 147 if let Ok(database_url) = std::env::var("DATABASE_URL") { 148 config.database_url = database_url; 149 } 150 151 if let Ok(jetstream_cursor_path) = std::env::var("JETSTREAM_CURSOR_PATH") { 152 config.jetstream_cursor_path = Some(jetstream_cursor_path); 153 } 154 155 Ok(config) 156 } 157} 158 159#[cfg(test)] 160mod tests { 161 use super::*; 162 163 #[test] 164 fn test_parse_duration() { 165 assert_eq!(duration_str::parse("10s").unwrap(), Duration::from_secs(10)); 166 assert_eq!( 167 duration_str::parse("500ms").unwrap(), 168 Duration::from_millis(500) 169 ); 170 assert_eq!(duration_str::parse("2m").unwrap(), Duration::from_secs(120)); 171 assert_eq!(duration_str::parse("30").unwrap(), Duration::from_secs(30)); 172 } 173 174 #[test] 175 fn test_default_config() { 176 let config = Config::default(); 177 assert_eq!(config.http.port, 8080); 178 assert_eq!(config.plc_hostname, "plc.directory"); 179 assert_eq!(config.http_client_timeout, Duration::from_secs(10)); 180 } 181}