A Rust application to showcase badge awards in the AT Protocol ecosystem.
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}