lmrc-postgres 0.3.16

PostgreSQL management library for the LMRC Stack - comprehensive library for managing PostgreSQL installations on remote servers via SSH
Documentation
//! Configuration diff detection
//!
//! This module provides functionality to detect and track configuration changes
//! between desired and current PostgreSQL configurations.

use std::fmt;

/// Represents a single configuration change
#[derive(Debug, Clone, PartialEq)]
pub struct ConfigChange {
    /// Configuration parameter name
    pub parameter: String,
    /// Current value on the server (None if not set)
    pub current: Option<String>,
    /// Desired value from configuration
    pub desired: String,
    /// Type of change
    pub change_type: ChangeType,
}

/// Type of configuration change
#[derive(Debug, Clone, PartialEq)]
pub enum ChangeType {
    /// Parameter needs to be added
    Add,
    /// Parameter needs to be modified
    Modify,
    /// Parameter needs to be removed
    Remove,
}

impl fmt::Display for ConfigChange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.change_type {
            ChangeType::Add => write!(f, "+ {}: {}", self.parameter, self.desired),
            ChangeType::Modify => write!(
                f,
                "~ {}: {} -> {}",
                self.parameter,
                self.current.as_deref().unwrap_or("(unset)"),
                self.desired
            ),
            ChangeType::Remove => write!(
                f,
                "- {}: {}",
                self.parameter,
                self.current.as_deref().unwrap_or("(unset)")
            ),
        }
    }
}

/// Configuration diff result
///
/// Contains all detected changes between desired and current configuration.
///
/// # Example
///
/// ```rust
/// use lmrc_postgres::ConfigDiff;
///
/// # fn example(diff: ConfigDiff) {
/// if diff.has_changes() {
///     println!("Configuration changes detected:");
///     for change in diff.changes() {
///         println!("  {}", change);
///     }
/// }
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct ConfigDiff {
    changes: Vec<ConfigChange>,
}

impl ConfigDiff {
    /// Create a new empty diff
    pub fn new() -> Self {
        Self {
            changes: Vec::new(),
        }
    }

    /// Add a change to the diff
    pub fn add_change(&mut self, change: ConfigChange) {
        self.changes.push(change);
    }

    /// Check if there are any changes
    pub fn has_changes(&self) -> bool {
        !self.changes.is_empty()
    }

    /// Get all changes
    pub fn changes(&self) -> &[ConfigChange] {
        &self.changes
    }

    /// Get number of changes
    pub fn len(&self) -> usize {
        self.changes.len()
    }

    /// Check if diff is empty
    pub fn is_empty(&self) -> bool {
        self.changes.is_empty()
    }

    /// Get changes by type
    pub fn changes_by_type(&self, change_type: ChangeType) -> Vec<&ConfigChange> {
        self.changes
            .iter()
            .filter(|c| c.change_type == change_type)
            .collect()
    }

    /// Get additions
    pub fn additions(&self) -> Vec<&ConfigChange> {
        self.changes_by_type(ChangeType::Add)
    }

    /// Get modifications
    pub fn modifications(&self) -> Vec<&ConfigChange> {
        self.changes_by_type(ChangeType::Modify)
    }

    /// Get removals
    pub fn removals(&self) -> Vec<&ConfigChange> {
        self.changes_by_type(ChangeType::Remove)
    }

    /// Create a summary string
    pub fn summary(&self) -> String {
        if self.is_empty() {
            return "No changes".to_string();
        }

        let adds = self.additions().len();
        let mods = self.modifications().len();
        let rems = self.removals().len();

        let mut parts = Vec::new();
        if adds > 0 {
            parts.push(format!(
                "{} addition{}",
                adds,
                if adds == 1 { "" } else { "s" }
            ));
        }
        if mods > 0 {
            parts.push(format!(
                "{} modification{}",
                mods,
                if mods == 1 { "" } else { "s" }
            ));
        }
        if rems > 0 {
            parts.push(format!(
                "{} removal{}",
                rems,
                if rems == 1 { "" } else { "s" }
            ));
        }

        parts.join(", ")
    }
}

impl Default for ConfigDiff {
    fn default() -> Self {
        Self::new()
    }
}

impl fmt::Display for ConfigDiff {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.is_empty() {
            write!(f, "No configuration changes")
        } else {
            writeln!(f, "Configuration changes ({}):", self.summary())?;
            for change in &self.changes {
                writeln!(f, "  {}", change)?;
            }
            Ok(())
        }
    }
}

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

    #[test]
    fn test_config_change_display() {
        let change = ConfigChange {
            parameter: "max_connections".to_string(),
            current: Some("100".to_string()),
            desired: "200".to_string(),
            change_type: ChangeType::Modify,
        };

        assert_eq!(change.to_string(), "~ max_connections: 100 -> 200");

        let change = ConfigChange {
            parameter: "shared_buffers".to_string(),
            current: None,
            desired: "256MB".to_string(),
            change_type: ChangeType::Add,
        };

        assert_eq!(change.to_string(), "+ shared_buffers: 256MB");
    }

    #[test]
    fn test_config_diff() {
        let mut diff = ConfigDiff::new();
        assert!(!diff.has_changes());
        assert_eq!(diff.len(), 0);

        diff.add_change(ConfigChange {
            parameter: "max_connections".to_string(),
            current: Some("100".to_string()),
            desired: "200".to_string(),
            change_type: ChangeType::Modify,
        });

        diff.add_change(ConfigChange {
            parameter: "shared_buffers".to_string(),
            current: None,
            desired: "256MB".to_string(),
            change_type: ChangeType::Add,
        });

        assert!(diff.has_changes());
        assert_eq!(diff.len(), 2);
        assert_eq!(diff.additions().len(), 1);
        assert_eq!(diff.modifications().len(), 1);
        assert_eq!(diff.removals().len(), 0);
    }

    #[test]
    fn test_diff_summary() {
        let mut diff = ConfigDiff::new();
        assert_eq!(diff.summary(), "No changes");

        diff.add_change(ConfigChange {
            parameter: "test".to_string(),
            current: None,
            desired: "value".to_string(),
            change_type: ChangeType::Add,
        });

        assert_eq!(diff.summary(), "1 addition");

        diff.add_change(ConfigChange {
            parameter: "test2".to_string(),
            current: Some("old".to_string()),
            desired: "new".to_string(),
            change_type: ChangeType::Modify,
        });

        assert_eq!(diff.summary(), "1 addition, 1 modification");
    }

    #[test]
    fn test_diff_display() {
        let mut diff = ConfigDiff::new();
        diff.add_change(ConfigChange {
            parameter: "max_connections".to_string(),
            current: Some("100".to_string()),
            desired: "200".to_string(),
            change_type: ChangeType::Modify,
        });

        let display = diff.to_string();
        assert!(display.contains("Configuration changes"));
        assert!(display.contains("max_connections"));
    }
}