hyperi-rustlib 2.8.6

There's plenty of sage advice out there about how to run Rust services in production at scale — config cascades, structured logging, masking secrets, multi-backend secrets management, Prometheus, OpenTelemetry, Kafka transports, tiered disk-spillover sinks, adaptive worker pools, graceful shutdown — but almost none of it as code you can just install and use. This is that code. Opinionated, drop-in, working out of the box. The patterns from blog posts, watercooler chats and beers with your Google mates as actual library — not a framework you assemble from twenty crates and 8 weeks of munging.
Documentation
// Project:   hyperi-rustlib
// File:      src/io/config.rs
// Purpose:   Shared file writer configuration types
// Language:  Rust
//
// License:   BUSL-1.1
// Copyright: (c) 2026 HYPERI PTY LIMITED

//! Shared configuration for NDJSON file writers.
//!
//! Used by both the DLQ file backend and the file output sink.

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// File rotation period.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RotationPeriod {
    /// Rotate files every hour.
    #[default]
    Hourly,
    /// Rotate files every day.
    Daily,
}

/// Shared file writer configuration.
///
/// Contains the core settings for rotating NDJSON file output. Used by
/// [`super::NdjsonWriter`] and consumed by both DLQ and file output modules.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileWriterConfig {
    /// Base directory for output files.
    pub path: PathBuf,

    /// File rotation period.
    pub rotation: RotationPeriod,

    /// Auto-cleanup files older than this many days.
    pub max_age_days: u32,

    /// Compress rotated files with flate2/gzip.
    pub compress_rotated: bool,
}

impl Default for FileWriterConfig {
    fn default() -> Self {
        Self {
            path: PathBuf::from("/var/spool/dfe"),
            rotation: RotationPeriod::default(),
            max_age_days: 30,
            compress_rotated: true,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_defaults() {
        let config = FileWriterConfig::default();
        assert_eq!(config.path, PathBuf::from("/var/spool/dfe"));
        assert_eq!(config.rotation, RotationPeriod::Hourly);
        assert_eq!(config.max_age_days, 30);
        assert!(config.compress_rotated);
    }

    #[test]
    fn test_rotation_period_serde() {
        let json = r#""hourly""#;
        let period: RotationPeriod = serde_json::from_str(json).expect("deserialise");
        assert_eq!(period, RotationPeriod::Hourly);

        let json = r#""daily""#;
        let period: RotationPeriod = serde_json::from_str(json).expect("deserialise");
        assert_eq!(period, RotationPeriod::Daily);
    }

    #[test]
    fn test_config_serde_roundtrip() {
        let config = FileWriterConfig {
            path: "/tmp/test".into(),
            rotation: RotationPeriod::Daily,
            max_age_days: 7,
            compress_rotated: false,
        };
        let json = serde_json::to_string(&config).expect("serialise");
        let parsed: FileWriterConfig = serde_json::from_str(&json).expect("deserialise");
        assert_eq!(parsed.path, PathBuf::from("/tmp/test"));
        assert_eq!(parsed.rotation, RotationPeriod::Daily);
        assert_eq!(parsed.max_age_days, 7);
        assert!(!parsed.compress_rotated);
    }
}