Skip to main content

rskit_server/
config.rs

1use rskit_validation::Validate;
2use serde::{Deserialize, Serialize};
3use validator::{ValidationError, ValidationErrors};
4
5/// TLS configuration for the gRPC server.
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct TlsConfig {
8    /// Path to the PEM-encoded TLS certificate file.
9    pub cert_path: String,
10    /// Path to the PEM-encoded TLS private key file.
11    pub key_path: String,
12}
13
14/// Configuration for the gRPC server component.
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct GrpcServerConfig {
17    /// Bind address (e.g. `"0.0.0.0"` or `"127.0.0.1"`).
18    pub host: String,
19
20    /// TCP port to listen on (1–65535).
21    pub port: u16,
22
23    /// Optional maximum number of concurrent connections.
24    pub max_connections: Option<usize>,
25
26    /// Optional TCP keep-alive interval in seconds.
27    pub keep_alive_secs: Option<u64>,
28
29    /// Optional TLS configuration.
30    ///
31    /// When configured, tonic/rustls modern defaults are used with TLS 1.3
32    /// preferred and TLS 1.2 as the minimum supported version.
33    pub tls: Option<TlsConfig>,
34}
35
36impl Validate for GrpcServerConfig {
37    fn validate(&self) -> Result<(), ValidationErrors> {
38        let mut errors = ValidationErrors::new();
39        if self.host.trim().is_empty() {
40            errors.add("host", ValidationError::new("length"));
41        }
42        if self.port == 0 {
43            errors.add("port", ValidationError::new("range"));
44        }
45        if errors.is_empty() {
46            Ok(())
47        } else {
48            Err(errors)
49        }
50    }
51}
52
53impl Default for GrpcServerConfig {
54    fn default() -> Self {
55        Self {
56            host: "0.0.0.0".into(),
57            port: 50051,
58            max_connections: None,
59            keep_alive_secs: None,
60            tls: None,
61        }
62    }
63}
64
65impl GrpcServerConfig {
66    /// Create a config with the given `host` and `port`.
67    pub fn new(host: impl Into<String>, port: u16) -> Self {
68        Self {
69            host: host.into(),
70            port,
71            ..Default::default()
72        }
73    }
74
75    /// Returns the `host:port` socket address string.
76    pub fn addr(&self) -> String {
77        format!("{}:{}", self.host, self.port)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use rskit_validation::Validate;
85
86    #[test]
87    fn default_config_is_valid() {
88        let cfg = GrpcServerConfig::default();
89        assert!(cfg.validate().is_ok());
90        assert_eq!(cfg.host, "0.0.0.0");
91        assert_eq!(cfg.port, 50051);
92    }
93
94    #[test]
95    fn new_sets_host_and_port() {
96        let cfg = GrpcServerConfig::new("127.0.0.1", 8080);
97        assert_eq!(cfg.host, "127.0.0.1");
98        assert_eq!(cfg.port, 8080);
99        assert!(cfg.validate().is_ok());
100    }
101
102    #[test]
103    fn addr_formats_correctly() {
104        let cfg = GrpcServerConfig::new("localhost", 9090);
105        assert_eq!(cfg.addr(), "localhost:9090");
106    }
107
108    #[test]
109    fn empty_host_fails_validation() {
110        let cfg = GrpcServerConfig {
111            host: String::new(),
112            ..Default::default()
113        };
114        assert!(cfg.validate().is_err());
115    }
116
117    #[test]
118    fn zero_port_fails_validation() {
119        let cfg = GrpcServerConfig {
120            port: 0,
121            ..Default::default()
122        };
123        let errors = cfg.validate().unwrap_err();
124        assert!(errors.field_errors().contains_key("port"));
125    }
126
127    #[test]
128    fn tls_config_stores_paths() {
129        let tls = TlsConfig {
130            cert_path: "/certs/server.crt".into(),
131            key_path: "/certs/server.key".into(),
132        };
133        let cfg = GrpcServerConfig {
134            tls: Some(tls.clone()),
135            ..Default::default()
136        };
137        let stored = cfg.tls.unwrap();
138        assert_eq!(stored.cert_path, "/certs/server.crt");
139        assert_eq!(stored.key_path, "/certs/server.key");
140    }
141
142    #[test]
143    fn optional_fields_default_to_none() {
144        let cfg = GrpcServerConfig::default();
145        assert!(cfg.max_connections.is_none());
146        assert!(cfg.keep_alive_secs.is_none());
147        assert!(cfg.tls.is_none());
148    }
149}