Skip to main content

rskit_config/
service.rs

1use rskit_validation::Validate;
2use serde::{Deserialize, Serialize};
3use validator::{ValidationError, ValidationErrors};
4
5/// Base service configuration — embed this in every application config.
6#[derive(Debug, Clone, Deserialize)]
7pub struct ServiceConfig {
8    /// Service name used in logs, traces, and health responses.
9    #[serde(default = "ServiceConfig::default_name")]
10    pub name: String,
11
12    /// Deployment environment (development, staging, production).
13    #[serde(default)]
14    pub environment: Environment,
15
16    /// Semver version string, defaults to `CARGO_PKG_VERSION`.
17    #[serde(default = "ServiceConfig::default_version")]
18    pub version: String,
19
20    /// Network address the service binds to.
21    #[serde(default = "ServiceConfig::default_address")]
22    pub address: String,
23
24    /// Network port the service listens on.
25    #[serde(default = "ServiceConfig::default_port")]
26    pub port: u16,
27
28    /// Enable verbose debug output.
29    #[serde(default)]
30    pub debug: bool,
31
32    /// Logging configuration.
33    #[serde(default)]
34    pub logging: LoggingConfig,
35}
36
37impl Validate for ServiceConfig {
38    fn validate(&self) -> Result<(), ValidationErrors> {
39        let mut errors = ValidationErrors::new();
40        if self.name.trim().is_empty() {
41            errors.add("name", ValidationError::new("length"));
42        }
43        if errors.is_empty() {
44            Ok(())
45        } else {
46            Err(errors)
47        }
48    }
49}
50
51impl ServiceConfig {
52    fn default_name() -> String {
53        "service".to_string()
54    }
55    fn default_version() -> String {
56        rskit_version::package_version().to_string()
57    }
58    fn default_address() -> String {
59        "0.0.0.0".to_string()
60    }
61    fn default_port() -> u16 {
62        50051
63    }
64}
65
66impl Default for ServiceConfig {
67    fn default() -> Self {
68        Self {
69            name: Self::default_name(),
70            environment: Environment::default(),
71            version: Self::default_version(),
72            address: Self::default_address(),
73            port: Self::default_port(),
74            debug: false,
75            logging: LoggingConfig::default(),
76        }
77    }
78}
79
80/// Deployment environment.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
82#[non_exhaustive]
83#[serde(rename_all = "lowercase")]
84pub enum Environment {
85    /// Local development environment (default).
86    #[default]
87    Development,
88    /// Pre-production / staging environment.
89    Staging,
90    /// Live production environment.
91    Production,
92}
93
94impl Environment {
95    /// Returns `true` if this is the production environment.
96    pub fn is_production(&self) -> bool {
97        *self == Environment::Production
98    }
99}
100
101impl std::fmt::Display for Environment {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            Environment::Development => f.write_str("development"),
105            Environment::Staging => f.write_str("staging"),
106            Environment::Production => f.write_str("production"),
107        }
108    }
109}
110
111/// Logging configuration.
112#[derive(Debug, Clone, Deserialize)]
113pub struct LoggingConfig {
114    /// Minimum log level: `trace`, `debug`, `info`, `warn`, `error`.
115    #[serde(default = "LoggingConfig::default_level")]
116    pub level: String,
117
118    /// Log output format (JSON or console).
119    #[serde(default)]
120    pub format: LogFormat,
121
122    /// Override service name in log output (defaults to [`ServiceConfig::name`]).
123    pub service_name: Option<String>,
124
125    /// Where to write log output.
126    #[serde(default)]
127    pub output: LogOutput,
128
129    /// Include `file:line` caller location in every log line.
130    #[serde(default)]
131    pub with_caller: bool,
132}
133
134impl Default for LoggingConfig {
135    fn default() -> Self {
136        Self {
137            level: Self::default_level(),
138            format: LogFormat::default(),
139            service_name: None,
140            output: LogOutput::default(),
141            with_caller: false,
142        }
143    }
144}
145
146impl LoggingConfig {
147    fn default_level() -> String {
148        "info".to_string()
149    }
150}
151
152/// Log output format.
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
154#[non_exhaustive]
155#[serde(rename_all = "lowercase")]
156pub enum LogFormat {
157    /// Machine-readable JSON (use in production).
158    Json,
159    /// Human-readable coloured output (default, use in development).
160    #[default]
161    Console,
162}
163
164/// Where log output is written.
165#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
166#[non_exhaustive]
167#[serde(tag = "type", rename_all = "lowercase")]
168pub enum LogOutput {
169    /// Write to standard output (default).
170    #[default]
171    Stdout,
172    /// Write to standard error.
173    Stderr,
174    /// Write to a file at the given path.
175    File {
176        /// Absolute or relative path to the log file.
177        path: String,
178    },
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    // ── Environment enum ────────────────────────────────────────────
186
187    #[test]
188    fn environment_display_development() {
189        assert_eq!(Environment::Development.to_string(), "development");
190    }
191
192    #[test]
193    fn environment_display_staging() {
194        assert_eq!(Environment::Staging.to_string(), "staging");
195    }
196
197    #[test]
198    fn environment_display_production() {
199        assert_eq!(Environment::Production.to_string(), "production");
200    }
201
202    #[test]
203    fn environment_default_is_development() {
204        assert_eq!(Environment::default(), Environment::Development);
205    }
206
207    #[test]
208    fn environment_is_production_returns_true_for_production() {
209        assert!(Environment::Production.is_production());
210    }
211
212    #[test]
213    fn environment_is_production_returns_false_for_development() {
214        assert!(!Environment::Development.is_production());
215    }
216
217    #[test]
218    fn environment_is_production_returns_false_for_staging() {
219        assert!(!Environment::Staging.is_production());
220    }
221
222    #[test]
223    fn environment_deserialize_from_lowercase_string() {
224        let dev: Environment = serde_json::from_str(r#""development""#).unwrap();
225        assert_eq!(dev, Environment::Development);
226
227        let stg: Environment = serde_json::from_str(r#""staging""#).unwrap();
228        assert_eq!(stg, Environment::Staging);
229
230        let prd: Environment = serde_json::from_str(r#""production""#).unwrap();
231        assert_eq!(prd, Environment::Production);
232    }
233
234    #[test]
235    fn environment_deserialize_unknown_string_fails() {
236        let result: Result<Environment, _> = serde_json::from_str(r#""unknown""#);
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn environment_clone_and_eq() {
242        let env = Environment::Staging;
243        let cloned = env.clone();
244        assert_eq!(env, cloned);
245    }
246
247    // ── ServiceConfig ───────────────────────────────────────────────
248
249    #[test]
250    fn service_config_default_name() {
251        let cfg = ServiceConfig::default();
252        assert_eq!(cfg.name, "service");
253    }
254
255    #[test]
256    fn service_config_default_environment() {
257        let cfg = ServiceConfig::default();
258        assert_eq!(cfg.environment, Environment::Development);
259    }
260
261    #[test]
262    fn service_config_default_debug_false() {
263        let cfg = ServiceConfig::default();
264        assert!(!cfg.debug);
265    }
266
267    #[test]
268    fn service_config_default_version_is_cargo_pkg() {
269        let cfg = ServiceConfig::default();
270        assert_eq!(cfg.version, rskit_version::package_version());
271    }
272
273    #[test]
274    fn service_config_default_logging() {
275        let cfg = ServiceConfig::default();
276        assert_eq!(cfg.logging.level, "info");
277        assert_eq!(cfg.logging.format, LogFormat::Console);
278        assert_eq!(cfg.logging.output, LogOutput::Stdout);
279    }
280
281    #[test]
282    fn service_config_validation_empty_name_fails() {
283        use rskit_validation::Validate;
284        let cfg = ServiceConfig {
285            name: String::new(),
286            ..Default::default()
287        };
288        assert!(cfg.validate().is_err());
289    }
290
291    #[test]
292    fn service_config_validation_valid_name_passes() {
293        use rskit_validation::Validate;
294        let cfg = ServiceConfig::default();
295        assert!(cfg.validate().is_ok());
296    }
297
298    #[test]
299    fn service_config_validation_long_name_passes() {
300        use rskit_validation::Validate;
301        let cfg = ServiceConfig {
302            name: "a".repeat(1000),
303            ..Default::default()
304        };
305        assert!(cfg.validate().is_ok());
306    }
307
308    #[test]
309    fn service_config_all_fields_accessible() {
310        let cfg = ServiceConfig::default();
311        let _ = &cfg.name;
312        let _ = &cfg.environment;
313        let _ = &cfg.version;
314        let _ = &cfg.address;
315        let _ = cfg.port;
316        let _ = cfg.debug;
317        let _ = &cfg.logging;
318    }
319
320    #[test]
321    fn service_config_default_address() {
322        let cfg = ServiceConfig::default();
323        assert_eq!(cfg.address, "0.0.0.0");
324    }
325
326    #[test]
327    fn service_config_default_port() {
328        let cfg = ServiceConfig::default();
329        assert_eq!(cfg.port, 50051);
330    }
331
332    #[test]
333    fn service_config_debug_format() {
334        let cfg = ServiceConfig::default();
335        let debug_str = format!("{:?}", cfg);
336        assert!(debug_str.contains("ServiceConfig"));
337        assert!(debug_str.contains("service"));
338    }
339
340    // ── LoggingConfig ───────────────────────────────────────────────
341
342    #[test]
343    fn logging_config_default_level() {
344        let cfg = LoggingConfig::default();
345        assert_eq!(cfg.level, "info");
346    }
347
348    #[test]
349    fn logging_config_default_format() {
350        let cfg = LoggingConfig::default();
351        assert_eq!(cfg.format, LogFormat::Console);
352    }
353
354    #[test]
355    fn logging_config_default_output() {
356        let cfg = LoggingConfig::default();
357        assert_eq!(cfg.output, LogOutput::Stdout);
358    }
359
360    #[test]
361    fn logging_config_default_service_name_is_none() {
362        let cfg = LoggingConfig::default();
363        assert!(cfg.service_name.is_none());
364    }
365
366    #[test]
367    fn logging_config_default_with_caller_false() {
368        let cfg = LoggingConfig::default();
369        assert!(!cfg.with_caller);
370    }
371
372    // ── LogFormat ───────────────────────────────────────────────────
373
374    #[test]
375    fn log_format_default_is_console() {
376        assert_eq!(LogFormat::default(), LogFormat::Console);
377    }
378
379    #[test]
380    fn log_format_json_variant() {
381        let fmt = LogFormat::Json;
382        assert_ne!(fmt, LogFormat::Console);
383    }
384
385    #[test]
386    fn log_format_deserialize_json() {
387        let fmt: LogFormat = serde_json::from_str(r#""json""#).unwrap();
388        assert_eq!(fmt, LogFormat::Json);
389    }
390
391    #[test]
392    fn log_format_deserialize_console() {
393        let fmt: LogFormat = serde_json::from_str(r#""console""#).unwrap();
394        assert_eq!(fmt, LogFormat::Console);
395    }
396
397    // ── LogOutput ───────────────────────────────────────────────────
398
399    #[test]
400    fn log_output_default_is_stdout() {
401        assert_eq!(LogOutput::default(), LogOutput::Stdout);
402    }
403
404    #[test]
405    fn log_output_stderr_variant() {
406        let out = LogOutput::Stderr;
407        assert_ne!(out, LogOutput::Stdout);
408    }
409
410    #[test]
411    fn log_output_file_variant() {
412        let out = LogOutput::File {
413            path: "/var/log/app.log".to_string(),
414        };
415        assert_eq!(
416            out,
417            LogOutput::File {
418                path: "/var/log/app.log".to_string()
419            }
420        );
421    }
422
423    #[test]
424    fn log_output_deserialize_stdout() {
425        let out: LogOutput = serde_json::from_str(r#"{"type":"stdout"}"#).unwrap();
426        assert_eq!(out, LogOutput::Stdout);
427    }
428
429    #[test]
430    fn log_output_deserialize_stderr() {
431        let out: LogOutput = serde_json::from_str(r#"{"type":"stderr"}"#).unwrap();
432        assert_eq!(out, LogOutput::Stderr);
433    }
434
435    #[test]
436    fn log_output_deserialize_file() {
437        let out: LogOutput =
438            serde_json::from_str(r#"{"type":"file","path":"/logs/app.log"}"#).unwrap();
439        assert_eq!(
440            out,
441            LogOutput::File {
442                path: "/logs/app.log".to_string()
443            }
444        );
445    }
446}