1use figment::{
11 providers::{Env, Format, Serialized, Toml},
12 Figment,
13};
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use std::path::{Path, PathBuf};
16use std::time::Duration;
17
18use crate::error::Result;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(bound(serialize = "T: Serialize", deserialize = "T: DeserializeOwned"))]
43pub struct Config<T = ()>
44where
45 T: Serialize + DeserializeOwned + Clone + Default + Send + Sync + 'static,
46{
47 pub service: ServiceConfig,
49
50 #[serde(default)]
52 pub token: Option<TokenConfig>,
53
54 pub rate_limit: RateLimitConfig,
56
57 #[serde(default)]
59 pub middleware: MiddlewareConfig,
60
61 #[serde(default)]
63 pub database: Option<DatabaseConfig>,
64
65 #[cfg(feature = "turso")]
67 #[serde(default)]
68 pub turso: Option<TursoConfig>,
69
70 #[cfg(feature = "surrealdb")]
72 #[serde(default)]
73 pub surrealdb: Option<SurrealDbConfig>,
74
75 #[serde(default)]
77 pub redis: Option<RedisConfig>,
78
79 #[serde(default)]
81 pub nats: Option<NatsConfig>,
82
83 #[cfg(feature = "clickhouse")]
85 #[serde(default)]
86 pub clickhouse: Option<ClickHouseConfig>,
87
88 #[serde(default)]
90 pub otlp: Option<OtlpConfig>,
91
92 #[serde(default)]
94 pub grpc: Option<GrpcConfig>,
95
96 #[cfg(feature = "websocket")]
98 #[serde(default)]
99 pub websocket: Option<crate::websocket::WebSocketConfig>,
100
101 #[cfg(feature = "cedar-authz")]
103 #[serde(default)]
104 pub cedar: Option<CedarConfig>,
105
106 #[cfg(feature = "session")]
108 #[serde(default)]
109 pub session: Option<crate::session::SessionConfig>,
110
111 #[cfg(feature = "audit")]
113 #[serde(default)]
114 pub audit: Option<crate::audit::AuditConfig>,
115
116 #[cfg(feature = "auth")]
118 #[serde(default)]
119 pub auth: Option<crate::auth::AuthConfig>,
120
121 #[cfg(feature = "login-lockout")]
123 #[serde(default)]
124 pub lockout: Option<crate::lockout::LockoutConfig>,
125
126 #[cfg(feature = "tls")]
128 #[serde(default)]
129 pub tls: Option<TlsConfig>,
130
131 #[cfg(feature = "journald")]
133 #[serde(default)]
134 pub journald: Option<JournaldConfig>,
135
136 #[cfg(feature = "accounts")]
138 #[serde(default)]
139 pub accounts: Option<crate::accounts::AccountsConfig>,
140
141 #[serde(default)]
143 pub background_worker: Option<crate::agents::BackgroundWorkerConfig>,
144
145 #[serde(flatten)]
150 pub custom: T,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ServiceConfig {
156 pub name: String,
158
159 #[serde(default = "default_port")]
161 pub port: u16,
162
163 #[serde(default = "default_log_level")]
165 pub log_level: String,
166
167 #[serde(default = "default_timeout")]
169 pub timeout_secs: u64,
170
171 #[serde(default = "default_environment")]
173 pub environment: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(tag = "format", rename_all = "lowercase")]
182pub enum TokenConfig {
183 Paseto(PasetoConfig),
185 #[cfg(feature = "jwt")]
187 Jwt(JwtConfig),
188}
189
190impl Default for TokenConfig {
191 fn default() -> Self {
192 TokenConfig::Paseto(PasetoConfig::default())
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PasetoConfig {
201 #[serde(default = "default_paseto_version")]
203 pub version: String,
204
205 #[serde(default = "default_paseto_purpose")]
207 pub purpose: String,
208
209 pub key_path: PathBuf,
213
214 #[serde(default)]
216 pub issuer: Option<String>,
217
218 #[serde(default)]
220 pub audience: Option<String>,
221}
222
223impl Default for PasetoConfig {
224 fn default() -> Self {
225 Self {
226 version: default_paseto_version(),
227 purpose: default_paseto_purpose(),
228 key_path: PathBuf::from("./keys/paseto.key"),
229 issuer: None,
230 audience: None,
231 }
232 }
233}
234
235fn default_paseto_version() -> String {
236 "v4".to_string()
237}
238
239fn default_paseto_purpose() -> String {
240 "local".to_string()
241}
242
243#[cfg(feature = "jwt")]
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct JwtConfig {
247 pub public_key_path: PathBuf,
249
250 #[serde(default = "default_jwt_algorithm")]
252 pub algorithm: String,
253
254 #[serde(default)]
256 pub issuer: Option<String>,
257
258 #[serde(default)]
260 pub audience: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct RateLimitConfig {
266 #[serde(default = "default_per_user_rpm")]
268 pub per_user_rpm: u32,
269
270 #[serde(default = "default_per_client_rpm")]
272 pub per_client_rpm: u32,
273
274 #[serde(default = "default_window_secs")]
276 pub window_secs: u64,
277
278 #[serde(default)]
299 pub routes: std::collections::HashMap<String, RouteRateLimitConfig>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct RouteRateLimitConfig {
308 pub requests_per_minute: u32,
310
311 #[serde(default = "default_route_burst_size")]
316 pub burst_size: u32,
317
318 #[serde(default = "default_true")]
326 pub per_user: bool,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct DatabaseConfig {
332 pub url: String,
334
335 #[serde(default = "default_max_connections")]
337 pub max_connections: u32,
338
339 #[serde(default = "default_min_connections")]
341 pub min_connections: u32,
342
343 #[serde(default = "default_connection_timeout")]
345 pub connection_timeout_secs: u64,
346
347 #[serde(default = "default_max_retries")]
349 pub max_retries: u32,
350
351 #[serde(default = "default_retry_delay")]
353 pub retry_delay_secs: u64,
354
355 #[serde(default = "default_false")]
357 pub optional: bool,
358
359 #[serde(default = "default_lazy_init")]
361 pub lazy_init: bool,
362}
363
364#[cfg(feature = "turso")]
366#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
367#[serde(rename_all = "snake_case")]
368pub enum TursoMode {
369 #[default]
371 Local,
372 Remote,
374 EmbeddedReplica,
376}
377
378#[cfg(feature = "turso")]
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct TursoConfig {
382 #[serde(default)]
384 pub mode: TursoMode,
385
386 #[serde(default)]
388 pub path: Option<PathBuf>,
389
390 #[serde(default)]
393 pub url: Option<String>,
394
395 #[serde(default)]
397 pub auth_token: Option<String>,
398
399 #[serde(default)]
402 pub sync_interval_secs: Option<u64>,
403
404 #[serde(default)]
406 pub encryption_key: Option<String>,
407
408 #[serde(default = "default_true")]
411 pub read_your_writes: bool,
412
413 #[serde(default = "default_max_retries")]
415 pub max_retries: u32,
416
417 #[serde(default = "default_retry_delay")]
419 pub retry_delay_secs: u64,
420
421 #[serde(default = "default_false")]
423 pub optional: bool,
424
425 #[serde(default = "default_lazy_init")]
427 pub lazy_init: bool,
428}
429
430#[cfg(feature = "surrealdb")]
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct SurrealDbConfig {
434 pub url: String,
436
437 #[serde(default = "default_surrealdb_namespace")]
439 pub namespace: String,
440
441 #[serde(default = "default_surrealdb_database")]
443 pub database: String,
444
445 #[serde(default)]
447 pub username: Option<String>,
448
449 #[serde(default)]
451 pub password: Option<String>,
452
453 #[serde(default = "default_max_retries")]
455 pub max_retries: u32,
456
457 #[serde(default = "default_retry_delay")]
459 pub retry_delay_secs: u64,
460
461 #[serde(default = "default_false")]
463 pub optional: bool,
464
465 #[serde(default = "default_lazy_init")]
467 pub lazy_init: bool,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct RedisConfig {
473 pub url: String,
475
476 #[serde(default = "default_redis_max_connections")]
478 pub max_connections: usize,
479
480 #[serde(default = "default_connection_timeout")]
482 pub connection_timeout_secs: u64,
483
484 #[serde(default = "default_max_retries")]
486 pub max_retries: u32,
487
488 #[serde(default = "default_retry_delay")]
490 pub retry_delay_secs: u64,
491
492 #[serde(default = "default_false")]
494 pub optional: bool,
495
496 #[serde(default = "default_lazy_init")]
498 pub lazy_init: bool,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct NatsConfig {
504 pub url: String,
506
507 #[serde(default)]
509 pub name: Option<String>,
510
511 #[serde(default = "default_max_reconnects")]
513 pub max_reconnects: usize,
514
515 #[serde(default = "default_max_retries")]
517 pub max_retries: u32,
518
519 #[serde(default = "default_retry_delay")]
521 pub retry_delay_secs: u64,
522
523 #[serde(default = "default_false")]
525 pub optional: bool,
526
527 #[serde(default = "default_lazy_init")]
529 pub lazy_init: bool,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct ClickHouseConfig {
539 pub url: String,
541
542 #[serde(default = "default_clickhouse_database")]
544 pub database: String,
545
546 #[serde(default)]
548 pub username: Option<String>,
549
550 #[serde(default)]
552 pub password: Option<String>,
553
554 #[serde(default = "default_max_retries")]
556 pub max_retries: u32,
557
558 #[serde(default = "default_retry_delay")]
560 pub retry_delay_secs: u64,
561
562 #[serde(default = "default_false")]
564 pub optional: bool,
565
566 #[serde(default = "default_lazy_init")]
568 pub lazy_init: bool,
569}
570
571fn default_clickhouse_database() -> String {
572 "default".to_string()
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct OtlpConfig {
578 pub endpoint: String,
580
581 #[serde(default)]
583 pub service_name: Option<String>,
584
585 #[serde(default = "default_true")]
587 pub enabled: bool,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct GrpcConfig {
593 #[serde(default = "default_true")]
595 pub enabled: bool,
596
597 #[serde(default = "default_false")]
599 pub use_separate_port: bool,
600
601 #[serde(default = "default_grpc_port")]
603 pub port: u16,
604
605 #[serde(default = "default_true")]
607 pub reflection_enabled: bool,
608
609 #[serde(default = "default_true")]
611 pub health_check_enabled: bool,
612
613 #[serde(default = "default_grpc_max_message_mb")]
615 pub max_message_size_mb: usize,
616
617 #[serde(default = "default_connection_timeout")]
619 pub connection_timeout_secs: u64,
620
621 #[serde(default = "default_timeout")]
623 pub timeout_secs: u64,
624
625 #[serde(default)]
627 pub proto: ProtoConfig,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct ProtoConfig {
636 #[serde(default = "default_proto_dir")]
641 pub dir: String,
642
643 #[serde(default)]
647 pub service_registry: Option<String>,
648
649 #[serde(default)]
653 pub service_mesh_endpoint: Option<String>,
654
655 #[serde(default = "default_false")]
657 pub validation_enabled: bool,
658
659 #[serde(default)]
663 pub metadata: std::collections::HashMap<String, String>,
664}
665
666impl Default for ProtoConfig {
667 fn default() -> Self {
668 Self {
669 dir: default_proto_dir(),
670 service_registry: None,
671 service_mesh_endpoint: None,
672 validation_enabled: false,
673 metadata: std::collections::HashMap::new(),
674 }
675 }
676}
677
678impl GrpcConfig {
679 pub fn effective_port(&self, http_port: u16) -> u16 {
681 if self.use_separate_port {
682 self.port
683 } else {
684 http_port
685 }
686 }
687
688 pub fn max_message_size_bytes(&self) -> usize {
690 self.max_message_size_mb * 1024 * 1024
691 }
692
693 pub fn connection_timeout(&self) -> Duration {
695 Duration::from_secs(self.connection_timeout_secs)
696 }
697
698 pub fn timeout(&self) -> Duration {
700 Duration::from_secs(self.timeout_secs)
701 }
702}
703
704#[cfg(feature = "cedar-authz")]
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct CedarConfig {
708 #[serde(default = "default_false")]
710 pub enabled: bool,
711
712 pub policy_path: PathBuf,
714
715 #[serde(default = "default_false")]
717 pub hot_reload: bool,
718
719 #[serde(default = "default_cedar_hot_reload_interval")]
721 pub hot_reload_interval_secs: u64,
722
723 #[serde(default = "default_true")]
725 pub cache_enabled: bool,
726
727 #[serde(default = "default_cedar_policy_cache_ttl")]
729 pub cache_ttl_secs: u64,
730
731 #[serde(default = "default_false")]
735 pub fail_open: bool,
736}
737
738#[cfg(feature = "cedar-authz")]
739impl CedarConfig {
740 pub fn hot_reload_interval(&self) -> Duration {
742 Duration::from_secs(self.hot_reload_interval_secs)
743 }
744
745 pub fn cache_ttl(&self) -> Duration {
747 Duration::from_secs(self.cache_ttl_secs)
748 }
749}
750
751#[cfg(feature = "tls")]
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub struct TlsConfig {
758 #[serde(default = "default_true")]
760 pub enabled: bool,
761
762 pub cert_path: PathBuf,
764
765 pub key_path: PathBuf,
767}
768
769#[cfg(feature = "journald")]
774#[derive(Debug, Clone, Serialize, Deserialize)]
775pub struct JournaldConfig {
776 #[serde(default = "default_true")]
778 pub enabled: bool,
779
780 #[serde(default)]
783 pub syslog_identifier: Option<String>,
784
785 #[serde(default)]
788 pub field_prefix: Option<String>,
789
790 #[serde(default = "default_false")]
793 pub disable_fmt_layer: bool,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct SecurityHeadersConfig {
802 #[serde(default = "default_true")]
804 pub enabled: bool,
805
806 #[serde(default = "default_true")]
808 pub hsts: bool,
809
810 #[serde(default = "default_hsts_max_age")]
812 pub hsts_max_age_secs: u64,
813
814 #[serde(default = "default_false")]
816 pub hsts_include_subdomains: bool,
817
818 #[serde(default = "default_false")]
820 pub hsts_preload: bool,
821
822 #[serde(default = "default_true")]
824 pub x_content_type_options: bool,
825
826 #[serde(default = "default_x_frame_options")]
828 pub x_frame_options: String,
829
830 #[serde(default = "default_true")]
832 pub x_xss_protection: bool,
833
834 #[serde(default = "default_referrer_policy")]
836 pub referrer_policy: String,
837
838 #[serde(default)]
840 pub permissions_policy: Option<String>,
841}
842
843impl Default for SecurityHeadersConfig {
844 fn default() -> Self {
845 Self {
846 enabled: true,
847 hsts: true,
848 hsts_max_age_secs: default_hsts_max_age(),
849 hsts_include_subdomains: false,
850 hsts_preload: false,
851 x_content_type_options: true,
852 x_frame_options: default_x_frame_options(),
853 x_xss_protection: true,
854 referrer_policy: default_referrer_policy(),
855 permissions_policy: None,
856 }
857 }
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct MiddlewareConfig {
863 #[serde(default)]
865 pub request_tracking: RequestTrackingConfig,
866
867 #[serde(default)]
869 pub resilience: Option<ResilienceConfig>,
870
871 #[serde(default)]
873 pub metrics: Option<MetricsConfig>,
874
875 #[serde(default)]
877 pub governor: Option<LocalRateLimitConfig>,
878
879 #[serde(default = "default_body_limit_mb")]
881 pub body_limit_mb: usize,
882
883 #[serde(default = "default_true")]
885 pub catch_panic: bool,
886
887 #[serde(default = "default_true")]
889 pub compression: bool,
890
891 #[serde(default = "default_cors_mode")]
893 pub cors_mode: String,
894
895 #[serde(default)]
897 pub security_headers: SecurityHeadersConfig,
898}
899
900impl Default for MiddlewareConfig {
901 fn default() -> Self {
902 Self {
903 request_tracking: RequestTrackingConfig::default(),
904 resilience: None,
905 metrics: None,
906 governor: None,
907 body_limit_mb: default_body_limit_mb(),
908 catch_panic: true,
909 compression: true,
910 cors_mode: default_cors_mode(),
911 security_headers: SecurityHeadersConfig::default(),
912 }
913 }
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct RequestTrackingConfig {
919 #[serde(default = "default_true")]
921 pub request_id_enabled: bool,
922
923 #[serde(default = "default_request_id_header")]
925 pub request_id_header: String,
926
927 #[serde(default = "default_true")]
929 pub propagate_headers: bool,
930
931 #[serde(default = "default_true")]
933 pub mask_sensitive_headers: bool,
934}
935
936impl Default for RequestTrackingConfig {
937 fn default() -> Self {
938 Self {
939 request_id_enabled: true,
940 request_id_header: default_request_id_header(),
941 propagate_headers: true,
942 mask_sensitive_headers: true,
943 }
944 }
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize)]
949pub struct ResilienceConfig {
950 #[serde(default = "default_true")]
952 pub circuit_breaker_enabled: bool,
953
954 #[serde(default = "default_circuit_breaker_threshold")]
956 pub circuit_breaker_threshold: f64,
957
958 #[serde(default = "default_circuit_breaker_min_requests")]
960 pub circuit_breaker_min_requests: u64,
961
962 #[serde(default = "default_circuit_breaker_wait_secs")]
964 pub circuit_breaker_wait_secs: u64,
965
966 #[serde(default = "default_true")]
968 pub retry_enabled: bool,
969
970 #[serde(default = "default_retry_max_attempts")]
972 pub retry_max_attempts: usize,
973
974 #[serde(default = "default_retry_base_delay_ms")]
976 pub retry_base_delay_ms: u64,
977
978 #[serde(default = "default_retry_max_delay_ms")]
980 pub retry_max_delay_ms: u64,
981
982 #[serde(default = "default_true")]
984 pub bulkhead_enabled: bool,
985
986 #[serde(default = "default_bulkhead_max_concurrent")]
988 pub bulkhead_max_concurrent: usize,
989
990 #[serde(default = "default_bulkhead_max_queued")]
992 pub bulkhead_max_queued: usize,
993}
994
995impl ResilienceConfig {
996 pub fn circuit_breaker_wait_duration(&self) -> Duration {
998 Duration::from_secs(self.circuit_breaker_wait_secs)
999 }
1000
1001 pub fn retry_base_delay(&self) -> Duration {
1002 Duration::from_millis(self.retry_base_delay_ms)
1003 }
1004
1005 pub fn retry_max_delay(&self) -> Duration {
1006 Duration::from_millis(self.retry_max_delay_ms)
1007 }
1008}
1009
1010#[derive(Debug, Clone, Serialize, Deserialize)]
1012pub struct MetricsConfig {
1013 #[serde(default = "default_true")]
1015 pub enabled: bool,
1016
1017 #[serde(default = "default_true")]
1019 pub include_path: bool,
1020
1021 #[serde(default = "default_true")]
1023 pub include_method: bool,
1024
1025 #[serde(default = "default_true")]
1027 pub include_status: bool,
1028
1029 #[serde(default = "default_latency_buckets")]
1031 pub latency_buckets_ms: Vec<f64>,
1032}
1033
1034impl MetricsConfig {
1035 pub fn latency_buckets_as_duration(&self) -> Vec<Duration> {
1036 self.latency_buckets_ms
1037 .iter()
1038 .map(|&ms| Duration::from_millis(ms as u64))
1039 .collect()
1040 }
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1045pub struct LocalRateLimitConfig {
1046 #[serde(default = "default_true")]
1048 pub enabled: bool,
1049
1050 #[serde(default = "default_governor_requests")]
1052 pub requests_per_period: u32,
1053
1054 #[serde(default = "default_governor_period_secs")]
1056 pub period_secs: u64,
1057
1058 #[serde(default = "default_governor_burst")]
1060 pub burst_size: u32,
1061}
1062
1063impl LocalRateLimitConfig {
1064 pub fn period(&self) -> Duration {
1065 Duration::from_secs(self.period_secs)
1066 }
1067}
1068
1069fn default_port() -> u16 {
1071 8080
1072}
1073
1074fn default_log_level() -> String {
1075 "info".to_string()
1076}
1077
1078fn default_timeout() -> u64 {
1079 30
1080}
1081
1082fn default_environment() -> String {
1083 "dev".to_string()
1084}
1085
1086#[cfg(feature = "jwt")]
1087fn default_jwt_algorithm() -> String {
1088 "RS256".to_string()
1089}
1090
1091fn default_per_user_rpm() -> u32 {
1092 200
1093}
1094
1095fn default_per_client_rpm() -> u32 {
1096 1000
1097}
1098
1099fn default_window_secs() -> u64 {
1100 60
1101}
1102
1103fn default_route_burst_size() -> u32 {
1104 10 }
1106
1107fn default_max_connections() -> u32 {
1108 50
1109}
1110
1111fn default_min_connections() -> u32 {
1112 5
1113}
1114
1115fn default_connection_timeout() -> u64 {
1116 10
1117}
1118
1119fn default_redis_max_connections() -> usize {
1120 20
1121}
1122
1123fn default_max_reconnects() -> usize {
1124 10
1125}
1126
1127fn default_true() -> bool {
1128 true
1129}
1130
1131fn default_false() -> bool {
1132 false
1133}
1134
1135fn default_max_retries() -> u32 {
1136 5
1137}
1138
1139fn default_retry_delay() -> u64 {
1140 2
1141}
1142
1143fn default_lazy_init() -> bool {
1144 true
1145}
1146
1147#[cfg(feature = "surrealdb")]
1148fn default_surrealdb_namespace() -> String {
1149 "default".to_string()
1150}
1151
1152#[cfg(feature = "surrealdb")]
1153fn default_surrealdb_database() -> String {
1154 "default".to_string()
1155}
1156
1157fn default_hsts_max_age() -> u64 {
1159 63_072_000 }
1161
1162fn default_x_frame_options() -> String {
1163 "DENY".to_string()
1164}
1165
1166fn default_referrer_policy() -> String {
1167 "strict-origin-when-cross-origin".to_string()
1168}
1169
1170fn default_body_limit_mb() -> usize {
1172 10 }
1174
1175fn default_cors_mode() -> String {
1176 "restrictive".to_string()
1177}
1178
1179fn default_request_id_header() -> String {
1180 "x-request-id".to_string()
1181}
1182
1183fn default_circuit_breaker_threshold() -> f64 {
1185 0.5 }
1187
1188fn default_circuit_breaker_min_requests() -> u64 {
1189 10
1190}
1191
1192fn default_circuit_breaker_wait_secs() -> u64 {
1193 30
1194}
1195
1196fn default_retry_max_attempts() -> usize {
1197 3
1198}
1199
1200fn default_retry_base_delay_ms() -> u64 {
1201 100
1202}
1203
1204fn default_retry_max_delay_ms() -> u64 {
1205 10000 }
1207
1208fn default_bulkhead_max_concurrent() -> usize {
1209 100
1210}
1211
1212fn default_bulkhead_max_queued() -> usize {
1213 200
1214}
1215
1216fn default_latency_buckets() -> Vec<f64> {
1218 vec![
1219 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0,
1220 ]
1221}
1222
1223fn default_governor_requests() -> u32 {
1225 100
1226}
1227
1228fn default_governor_period_secs() -> u64 {
1229 60
1230}
1231
1232fn default_governor_burst() -> u32 {
1233 10
1234}
1235
1236fn default_grpc_port() -> u16 {
1238 9090
1239}
1240
1241fn default_grpc_max_message_mb() -> usize {
1242 4 }
1244
1245fn default_proto_dir() -> String {
1246 "proto".to_string()
1247}
1248
1249#[cfg(feature = "cedar-authz")]
1251fn default_cedar_hot_reload_interval() -> u64 {
1252 60 }
1254
1255#[cfg(feature = "cedar-authz")]
1256fn default_cedar_policy_cache_ttl() -> u64 {
1257 300 }
1259
1260impl<T> Config<T>
1261where
1262 T: Serialize + DeserializeOwned + Clone + Default + Send + Sync + 'static,
1263{
1264 pub fn load() -> Result<Self> {
1275 let service_name = std::env::current_exe()
1277 .ok()
1278 .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
1279 .unwrap_or_else(|| "acton-service".to_string());
1280
1281 Self::load_for_service(&service_name)
1282 }
1283
1284 pub fn load_for_service(service_name: &str) -> Result<Self> {
1288 let config_paths = Self::find_config_paths(service_name);
1289
1290 tracing::debug!("Searching for config files in order:");
1292 for path in &config_paths {
1293 tracing::debug!(" - {}", path.display());
1294 }
1295
1296 let mut figment = Figment::new()
1297 .merge(Serialized::defaults(Config::<T>::default()));
1299
1300 for path in config_paths.iter().rev() {
1303 if path.exists() {
1304 tracing::info!("Loading configuration from: {}", path.display());
1305 figment = figment.merge(Toml::file(path));
1306 }
1307 }
1308
1309 figment = figment.merge(Env::prefixed("ACTON_").split("_"));
1311
1312 let config = figment.extract()?;
1313 Ok(config)
1314 }
1315
1316 pub fn load_from(path: &str) -> Result<Self> {
1321 let config = Figment::new()
1322 .merge(Serialized::defaults(Config::<T>::default()))
1324 .merge(Toml::file(path))
1326 .merge(Env::prefixed("ACTON_").split("_"))
1328 .extract()?;
1329
1330 Ok(config)
1331 }
1332
1333 fn find_config_paths(service_name: &str) -> Vec<PathBuf> {
1340 let mut paths = Vec::new();
1341
1342 paths.push(PathBuf::from("config.toml"));
1344
1345 let xdg_dirs = xdg::BaseDirectories::with_prefix("acton-service");
1348 let config_file_path = Path::new(service_name).join("config.toml");
1349 if let Some(path) = xdg_dirs.find_config_file(&config_file_path) {
1350 paths.push(path);
1351 }
1352
1353 paths.push(
1355 PathBuf::from("/etc/acton-service")
1356 .join(service_name)
1357 .join("config.toml"),
1358 );
1359
1360 paths
1361 }
1362
1363 pub fn recommended_path(service_name: &str) -> PathBuf {
1368 let xdg_dirs = xdg::BaseDirectories::with_prefix("acton-service");
1369 let config_file_path = Path::new(service_name).join("config.toml");
1370
1371 xdg_dirs
1373 .place_config_file(&config_file_path)
1374 .unwrap_or_else(|_| {
1375 PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| String::from("~")))
1377 .join(".config/acton-service")
1378 .join(service_name)
1379 .join("config.toml")
1380 })
1381 }
1382
1383 pub fn create_config_dir(service_name: &str) -> Result<PathBuf> {
1387 let xdg_dirs = xdg::BaseDirectories::with_prefix("acton-service");
1388 let config_file_path = Path::new(service_name).join("config.toml");
1389
1390 let config_path = xdg_dirs.place_config_file(&config_file_path).map_err(|e| {
1392 crate::error::Error::Internal(format!("Failed to create config directory: {}", e))
1393 })?;
1394
1395 Ok(config_path
1397 .parent()
1398 .ok_or_else(|| crate::error::Error::Internal("Invalid config path".to_string()))?
1399 .to_path_buf())
1400 }
1401
1402 pub fn database_url(&self) -> Option<&str> {
1404 self.database.as_ref().map(|db| db.url.as_str())
1405 }
1406
1407 pub fn redis_url(&self) -> Option<&str> {
1409 self.redis.as_ref().map(|r| r.url.as_str())
1410 }
1411
1412 pub fn nats_url(&self) -> Option<&str> {
1414 self.nats.as_ref().map(|n| n.url.as_str())
1415 }
1416
1417 #[cfg(feature = "turso")]
1419 pub fn turso_url(&self) -> Option<&str> {
1420 self.turso.as_ref().and_then(|t| t.url.as_deref())
1421 }
1422
1423 #[cfg(feature = "surrealdb")]
1425 pub fn surrealdb_url(&self) -> Option<&str> {
1426 self.surrealdb.as_ref().map(|s| s.url.as_str())
1427 }
1428
1429 pub fn with_development_cors(&mut self) -> &mut Self {
1457 tracing::warn!(
1458 "⚠️ CORS set to permissive mode - DO NOT USE IN PRODUCTION! \
1459 This allows any origin to access your API. \
1460 Use only for local development."
1461 );
1462 self.middleware.cors_mode = "permissive".to_string();
1463 self
1464 }
1465}
1466
1467impl<T> Default for Config<T>
1468where
1469 T: Serialize + DeserializeOwned + Clone + Default + Send + Sync + 'static,
1470{
1471 fn default() -> Self {
1472 Self {
1473 service: ServiceConfig {
1474 name: "acton-service".to_string(),
1475 port: default_port(),
1476 log_level: default_log_level(),
1477 timeout_secs: default_timeout(),
1478 environment: default_environment(),
1479 },
1480 token: None,
1481 rate_limit: RateLimitConfig {
1482 per_user_rpm: default_per_user_rpm(),
1483 per_client_rpm: default_per_client_rpm(),
1484 window_secs: default_window_secs(),
1485 routes: std::collections::HashMap::new(),
1486 },
1487 middleware: MiddlewareConfig::default(),
1488 database: None,
1489 #[cfg(feature = "turso")]
1490 turso: None,
1491 #[cfg(feature = "surrealdb")]
1492 surrealdb: None,
1493 redis: None,
1494 nats: None,
1495 #[cfg(feature = "clickhouse")]
1496 clickhouse: None,
1497 otlp: None,
1498 grpc: None,
1499 #[cfg(feature = "websocket")]
1500 websocket: None,
1501 #[cfg(feature = "cedar-authz")]
1502 cedar: None,
1503 #[cfg(feature = "session")]
1504 session: None,
1505 #[cfg(feature = "audit")]
1506 audit: None,
1507 #[cfg(feature = "auth")]
1508 auth: None,
1509 #[cfg(feature = "login-lockout")]
1510 lockout: None,
1511 #[cfg(feature = "tls")]
1512 tls: None,
1513 #[cfg(feature = "journald")]
1514 journald: None,
1515 #[cfg(feature = "accounts")]
1516 accounts: None,
1517 background_worker: None,
1518 custom: T::default(),
1519 }
1520 }
1521}
1522
1523#[cfg(test)]
1524mod tests {
1525 use super::*;
1526 use std::collections::HashMap;
1527
1528 #[test]
1529 fn test_default_config() {
1530 let config = Config::<()>::default();
1531 assert_eq!(config.service.port, 8080);
1532 assert_eq!(config.service.log_level, "info");
1533 assert_eq!(config.rate_limit.per_user_rpm, 200);
1534 }
1535
1536 #[test]
1537 fn test_default_config_with_unit_type() {
1538 let config = Config::<()>::default();
1539 assert_eq!(config.service.port, 8080);
1540 assert_eq!(config.service.name, "acton-service");
1541 }
1543
1544 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
1545 struct CustomConfig {
1546 api_key: String,
1547 timeout_ms: u32,
1548 feature_flags: HashMap<String, bool>,
1549 }
1550
1551 #[test]
1552 fn test_config_with_custom_type() {
1553 let custom = CustomConfig {
1554 api_key: "test-key-123".to_string(),
1555 timeout_ms: 5000,
1556 feature_flags: {
1557 let mut map = HashMap::new();
1558 map.insert("new_ui".to_string(), true);
1559 map.insert("beta_features".to_string(), false);
1560 map
1561 },
1562 };
1563
1564 let config = Config {
1565 service: ServiceConfig {
1566 name: "test-service".to_string(),
1567 port: 9090,
1568 log_level: "debug".to_string(),
1569 timeout_secs: 30,
1570 environment: "test".to_string(),
1571 },
1572 token: Some(TokenConfig::Paseto(PasetoConfig {
1573 version: "v4".to_string(),
1574 purpose: "local".to_string(),
1575 key_path: PathBuf::from("./test-key.key"),
1576 issuer: Some("test-issuer".to_string()),
1577 audience: None,
1578 })),
1579 rate_limit: RateLimitConfig {
1580 per_user_rpm: 100,
1581 per_client_rpm: 500,
1582 window_secs: 60,
1583 routes: std::collections::HashMap::new(),
1584 },
1585 middleware: MiddlewareConfig::default(),
1586 database: None,
1587 #[cfg(feature = "turso")]
1588 turso: None,
1589 #[cfg(feature = "surrealdb")]
1590 surrealdb: None,
1591 redis: None,
1592 nats: None,
1593 #[cfg(feature = "clickhouse")]
1594 clickhouse: None,
1595 otlp: None,
1596 grpc: None,
1597 #[cfg(feature = "websocket")]
1598 websocket: None,
1599 #[cfg(feature = "cedar-authz")]
1600 cedar: None,
1601 #[cfg(feature = "session")]
1602 session: None,
1603 #[cfg(feature = "audit")]
1604 audit: None,
1605 #[cfg(feature = "auth")]
1606 auth: None,
1607 #[cfg(feature = "login-lockout")]
1608 lockout: None,
1609 #[cfg(feature = "tls")]
1610 tls: None,
1611 #[cfg(feature = "journald")]
1612 journald: None,
1613 #[cfg(feature = "accounts")]
1614 accounts: None,
1615 background_worker: None,
1616 custom,
1617 };
1618
1619 assert_eq!(config.service.name, "test-service");
1620 assert_eq!(config.custom.api_key, "test-key-123");
1621 assert_eq!(config.custom.timeout_ms, 5000);
1622 assert_eq!(config.custom.feature_flags.get("new_ui"), Some(&true));
1623 }
1624
1625 #[test]
1626 fn test_config_serialization_with_custom() {
1627 let custom = CustomConfig {
1628 api_key: "secret-key".to_string(),
1629 timeout_ms: 3000,
1630 feature_flags: HashMap::new(),
1631 };
1632
1633 let config = Config {
1634 service: ServiceConfig {
1635 name: "test".to_string(),
1636 port: 8080,
1637 log_level: "info".to_string(),
1638 timeout_secs: 30,
1639 environment: "dev".to_string(),
1640 },
1641 token: None,
1642 rate_limit: RateLimitConfig {
1643 per_user_rpm: 200,
1644 per_client_rpm: 1000,
1645 window_secs: 60,
1646 routes: std::collections::HashMap::new(),
1647 },
1648 middleware: MiddlewareConfig::default(),
1649 database: None,
1650 #[cfg(feature = "turso")]
1651 turso: None,
1652 #[cfg(feature = "surrealdb")]
1653 surrealdb: None,
1654 redis: None,
1655 nats: None,
1656 #[cfg(feature = "clickhouse")]
1657 clickhouse: None,
1658 otlp: None,
1659 grpc: None,
1660 #[cfg(feature = "websocket")]
1661 websocket: None,
1662 #[cfg(feature = "cedar-authz")]
1663 cedar: None,
1664 #[cfg(feature = "session")]
1665 session: None,
1666 #[cfg(feature = "audit")]
1667 audit: None,
1668 #[cfg(feature = "auth")]
1669 auth: None,
1670 #[cfg(feature = "login-lockout")]
1671 lockout: None,
1672 #[cfg(feature = "tls")]
1673 tls: None,
1674 #[cfg(feature = "journald")]
1675 journald: None,
1676 #[cfg(feature = "accounts")]
1677 accounts: None,
1678 background_worker: None,
1679 custom: custom.clone(),
1680 };
1681
1682 let json = serde_json::to_string(&config).expect("Failed to serialize");
1684
1685 let deserialized: Config<CustomConfig> =
1687 serde_json::from_str(&json).expect("Failed to deserialize");
1688
1689 assert_eq!(deserialized.custom, custom);
1690 assert_eq!(deserialized.service.name, "test");
1691 }
1692
1693 #[test]
1694 fn test_config_deserialization_with_flatten() {
1695 let json_str = r#"{
1697 "service": {
1698 "name": "my-service",
1699 "port": 9000,
1700 "log_level": "debug",
1701 "timeout_secs": 60,
1702 "environment": "production"
1703 },
1704 "token": {
1705 "format": "paseto",
1706 "version": "v4",
1707 "purpose": "local",
1708 "key_path": "./keys/paseto.key"
1709 },
1710 "rate_limit": {
1711 "per_user_rpm": 150,
1712 "per_client_rpm": 750,
1713 "window_secs": 60
1714 },
1715 "middleware": {
1716 "cors_mode": "restrictive",
1717 "body_limit_mb": 10,
1718 "compression_enabled": true
1719 },
1720 "api_key": "prod-api-key",
1721 "timeout_ms": 10000,
1722 "feature_flags": {
1723 "new_dashboard": true,
1724 "analytics": true
1725 }
1726 }"#;
1727
1728 let config: Config<CustomConfig> =
1729 serde_json::from_str(json_str).expect("Failed to parse JSON");
1730
1731 assert_eq!(config.service.name, "my-service");
1733 assert_eq!(config.service.port, 9000);
1734 assert_eq!(config.service.log_level, "debug");
1735
1736 assert_eq!(config.custom.api_key, "prod-api-key");
1738 assert_eq!(config.custom.timeout_ms, 10000);
1739 assert_eq!(
1740 config.custom.feature_flags.get("new_dashboard"),
1741 Some(&true)
1742 );
1743 assert_eq!(config.custom.feature_flags.get("analytics"), Some(&true));
1744 }
1745}