firecloud-cli 0.2.0

Command-line interface for FireCloud P2P messaging and file sharing
//! Configuration file handling for FireCloud CLI
//!
//! Configuration is stored in TOML format at `<data_dir>/config.toml`
//!
//! Example config:
//! ```toml
//! [network]
//! port = 4001
//! enable_mdns = true
//!
//! [bootstrap]
//! peers = [
//!     "/ip4/203.0.113.1/udp/4001/quic-v1/p2p/12D3KooW...",
//!     "/ip4/198.51.100.2/udp/4001/quic-v1/p2p/12D3KooW...",
//! ]
//! ```

use anyhow::{Context, Result};
use libp2p::Multiaddr;
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Main configuration structure
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FireCloudConfig {
    /// Network settings
    #[serde(default)]
    pub network: NetworkConfig,

    /// Bootstrap peer configuration
    #[serde(default)]
    pub bootstrap: BootstrapConfig,
    
    /// Storage quota in bytes (None if not a provider)
    #[serde(default)]
    pub storage_quota: Option<u64>,
}

/// Network configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
    /// Port to listen on (0 for random)
    #[serde(default)]
    pub port: u16,

    /// Enable mDNS for local peer discovery
    #[serde(default = "default_true")]
    pub enable_mdns: bool,

    /// Connection timeout in seconds
    #[serde(default = "default_connection_timeout")]
    pub connection_timeout_secs: u64,
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            port: 0,
            enable_mdns: true,
            connection_timeout_secs: 60,
        }
    }
}

fn default_true() -> bool {
    true
}

fn default_connection_timeout() -> u64 {
    60
}

/// Bootstrap peer configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BootstrapConfig {
    /// List of bootstrap peer multiaddresses
    #[serde(default)]
    pub peers: Vec<String>,
}

impl FireCloudConfig {
    /// Load configuration from a file, or return default if file doesn't exist
    pub fn load(config_path: &Path) -> Result<Self> {
        if config_path.exists() {
            let content = std::fs::read_to_string(config_path)
                .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
            let config: FireCloudConfig = toml::from_str(&content)
                .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
            Ok(config)
        } else {
            Ok(Self::default())
        }
    }

    /// Save configuration to a file
    pub fn save(&self, config_path: &Path) -> Result<()> {
        let content = toml::to_string_pretty(self)
            .context("Failed to serialize config")?;
        std::fs::write(config_path, content)
            .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
        Ok(())
    }

    /// Create a default config file if it doesn't exist
    pub fn create_default_if_missing(config_path: &Path) -> Result<()> {
        if !config_path.exists() {
            let config = Self::default();
            config.save(config_path)?;
        }
        Ok(())
    }

    /// Parse bootstrap peers into Multiaddrs
    pub fn parse_bootstrap_peers(&self) -> Vec<Multiaddr> {
        self.bootstrap
            .peers
            .iter()
            .filter_map(|s| {
                match s.parse::<Multiaddr>() {
                    Ok(addr) => Some(addr),
                    Err(e) => {
                        tracing::warn!("Invalid bootstrap peer address '{}': {}", s, e);
                        None
                    }
                }
            })
            .collect()
    }
}

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

    #[test]
    fn test_default_config() {
        let config = FireCloudConfig::default();
        assert_eq!(config.network.port, 0);
        assert!(config.network.enable_mdns);
        assert!(config.bootstrap.peers.is_empty());
    }

    #[test]
    fn test_parse_config() {
        let toml_str = r#"
[network]
port = 4001
enable_mdns = true

[bootstrap]
peers = [
    "/ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN",
]
"#;
        let config: FireCloudConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.network.port, 4001);
        assert!(config.network.enable_mdns);
        assert_eq!(config.bootstrap.peers.len(), 1);
    }

    #[test]
    fn test_parse_bootstrap_peers() {
        let toml_str = r#"
[bootstrap]
peers = [
    "/ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN",
    "invalid-address",
]
"#;
        let config: FireCloudConfig = toml::from_str(toml_str).unwrap();
        let addrs = config.parse_bootstrap_peers();
        // Should only parse 1 valid address
        assert_eq!(addrs.len(), 1);
    }
}