Skip to main content

rs_zero/rpc/
service_config.rs

1use std::{collections::BTreeMap, net::SocketAddr};
2
3use serde::Deserialize;
4
5use crate::core::{
6    ConfigFeatureWarning, CoreError, CoreResult, DatabaseSection, LogConfig, LogSection,
7    RpcClientSection, ServiceConfig, dependency_feature_warnings, load_config,
8};
9
10/// RPC service configuration loaded by generated services.
11#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
12#[serde(default, deny_unknown_fields)]
13pub struct RpcServiceConfig {
14    /// Service name.
15    pub name: String,
16    /// Deployment mode.
17    pub mode: String,
18    /// Server listener and timeout settings.
19    pub server: RpcServerSection,
20    /// Logging configuration.
21    pub log: LogSection,
22    /// Middleware toggles.
23    pub middlewares: RpcMiddlewaresSection,
24    /// Outbound RPC clients used by this RPC service.
25    pub rpc_clients: BTreeMap<String, RpcClientSection>,
26    /// Optional database used by model repositories.
27    pub database: Option<DatabaseSection>,
28}
29
30impl Default for RpcServiceConfig {
31    fn default() -> Self {
32        let service = ServiceConfig::default();
33        Self {
34            name: service.name,
35            mode: service.mode,
36            server: RpcServerSection::default(),
37            log: service.log,
38            middlewares: RpcMiddlewaresSection::default(),
39            rpc_clients: BTreeMap::new(),
40            database: None,
41        }
42    }
43}
44
45impl RpcServiceConfig {
46    /// Loads RPC service configuration from `basename` and env vars with `env_prefix`.
47    pub fn load(basename: &str, env_prefix: &str) -> Result<Self, config::ConfigError> {
48        load_config(basename, env_prefix)
49    }
50
51    /// Returns the listening address.
52    pub fn addr(&self) -> CoreResult<SocketAddr> {
53        format!("{}:{}", self.server.host, self.server.port)
54            .parse()
55            .map_err(|error| {
56                config::ConfigError::Message(format!("invalid RPC listen address: {error}")).into()
57            })
58    }
59
60    /// Converts to runtime logging config.
61    pub fn log_config(&self) -> LogConfig {
62        self.log.to_log_config(&self.name)
63    }
64
65    /// Returns warnings for config options that are unavailable in this Cargo feature build.
66    pub fn validate_features(&self) -> Vec<ConfigFeatureWarning> {
67        let mut warnings = Vec::new();
68        if self.middlewares.resilience && !cfg!(feature = "resil") {
69            warnings.push(ConfigFeatureWarning::ignored(
70                "middlewares.resilience",
71                "resil",
72            ));
73        }
74        if self.middlewares.streaming && !cfg!(feature = "resil") {
75            warnings.push(ConfigFeatureWarning::ignored(
76                "middlewares.streaming",
77                "resil",
78            ));
79        }
80        warnings.extend(dependency_feature_warnings(
81            &self.rpc_clients,
82            self.database.as_ref(),
83        ));
84        warnings
85    }
86
87    /// Converts to runtime RPC server config.
88    pub fn rpc_server_config(&self) -> CoreResult<crate::rpc::RpcServerConfig> {
89        let mut config = if self.middlewares.resilience {
90            crate::rpc::RpcServerConfig::production_defaults(self.name.clone(), self.addr()?)
91        } else {
92            crate::rpc::RpcServerConfig::new(self.name.clone(), self.addr()?)
93        };
94        if !self.middlewares.streaming {
95            config.streaming = crate::rpc::RpcStreamingConfig::default();
96        }
97        Ok(config)
98    }
99
100    /// Returns one outbound RPC client dependency by logical name.
101    pub fn rpc_client(&self, name: &str) -> CoreResult<&RpcClientSection> {
102        self.rpc_clients.get(name).ok_or_else(|| {
103            CoreError::Config(config::ConfigError::Message(format!(
104                "missing rpc client config: {name}"
105            )))
106        })
107    }
108
109    /// Converts one outbound RPC client dependency to runtime RPC config.
110    #[cfg(feature = "rpc")]
111    pub fn rpc_client_config(&self, name: &str) -> CoreResult<crate::rpc::RpcClientConfig> {
112        self.rpc_client(name)?.to_rpc_client_config()
113    }
114
115    /// Converts the optional database dependency to runtime database config.
116    #[cfg(feature = "db")]
117    pub fn database_config(&self) -> Option<crate::db::DatabaseConfig> {
118        self.database
119            .as_ref()
120            .map(DatabaseSection::to_database_config)
121    }
122}
123
124/// RPC listener section.
125#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
126#[serde(default, deny_unknown_fields)]
127pub struct RpcServerSection {
128    pub host: String,
129    pub port: u16,
130    pub timeout_ms: u64,
131    pub health: bool,
132}
133
134impl Default for RpcServerSection {
135    fn default() -> Self {
136        Self {
137            host: "127.0.0.1".to_string(),
138            port: 50051,
139            timeout_ms: 5000,
140            health: true,
141        }
142    }
143}
144
145/// RPC middleware toggles.
146#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
147#[serde(default, deny_unknown_fields)]
148pub struct RpcMiddlewaresSection {
149    pub resilience: bool,
150    pub streaming: bool,
151}
152
153impl Default for RpcMiddlewaresSection {
154    fn default() -> Self {
155        Self {
156            resilience: true,
157            streaming: true,
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::RpcServiceConfig;
165
166    #[test]
167    fn validate_features_reflects_compile_time_features() {
168        let warnings = RpcServiceConfig::default().validate_features();
169        assert_eq!(
170            warnings
171                .iter()
172                .any(|warning| warning.option == "middlewares.resilience"),
173            !cfg!(feature = "resil")
174        );
175        assert_eq!(
176            warnings
177                .iter()
178                .any(|warning| warning.option == "middlewares.streaming"),
179            !cfg!(feature = "resil")
180        );
181    }
182}