cargo-sideload 0.8.1

Tools for working with private Cargo registries
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, convert::TryFrom, fmt::Display, str::FromStr};

pub const CONFIG_FILE_DIR: &str = "cargo-sideload";
pub const CONFIG_FILE_NAME: &str = "config.toml";

/// Represents the entire persistent config file
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
    pub default_registry: Option<String>,
    #[serde(default)]
    pub registries: HashMap<String, RegistryConfig>,
}

impl Config {
    pub fn load() -> anyhow::Result<Option<Self>> {
        match dirs::config_dir() {
            Some(config_dir) => {
                let config_path = config_dir.join(CONFIG_FILE_DIR).join(CONFIG_FILE_NAME);
                if config_path.is_file() {
                    let bytes = std::fs::read(config_path)?;
                    let config: Config = toml::from_slice(&bytes)?;
                    Ok(Some(config))
                } else {
                    Ok(None)
                }
            }
            None => Ok(None),
        }
    }
}

/// Configuration for an individual registry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryConfig {
    pub default_headers: Vec<Header>,
}

/// Header name and value with the string representation `[Header-Name]: [Header Value]`
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "&str")]
#[serde(into = "String")]
pub struct Header {
    pub name: String,
    pub value: String,
}

impl FromStr for Header {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let split: Vec<&str> = s.splitn(2, ':').collect();

        if split.len() != 2 {
            anyhow::bail!("Invalid auth header format. Expected `[Header-Name]: [Header Value]`");
        }

        let name = split[0].to_string();
        let value = split[1].trim_start().to_string();

        Ok(Self { name, value })
    }
}

impl TryFrom<&str> for Header {
    type Error = anyhow::Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Header::from_str(value)
    }
}

impl Display for Header {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}: {}", self.name, self.value)
    }
}

impl From<Header> for String {
    fn from(header: Header) -> Self {
        header.to_string()
    }
}

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

    #[test]
    fn test_header() {
        let header = Header::from_str("Authorization: Bearer abcd1234").unwrap();
        assert_eq!(header.name, "Authorization");
        assert_eq!(header.value, "Bearer abcd1234");

        assert!(Header::from_str("Authorization").is_err());
        assert!(Header::from_str("").is_err());
    }

    #[test]
    fn test_config_full() {
        let config_str = r#"
            default_registry = "test_registry"
    
            [registries.test_registry]
            default_headers = [ "Authorization: Blah abcd1234" ] 
            
            [registries.other_registry]
            default_headers = [ 
                "PRIVATE-KEY: abcdef",
                "Some-Other-Header: And its value",
            ]
        "#;

        let config: Config = toml::from_str(config_str).unwrap();

        assert_eq!(config.default_registry, Some("test_registry".to_string()));

        let test_registry_config = config.registries.get("test_registry").unwrap();

        let first_header = &test_registry_config.default_headers[0];
        assert_eq!(first_header.name, "Authorization");
        assert_eq!(first_header.value, "Blah abcd1234");

        let other_registry_config = config.registries.get("other_registry").unwrap();
        let header = &other_registry_config.default_headers[0];
        assert_eq!(header.name, "PRIVATE-KEY");
        assert_eq!(header.value, "abcdef");

        let second_header = &other_registry_config.default_headers[1];
        assert_eq!(second_header.name, "Some-Other-Header");
        assert_eq!(second_header.value, "And its value");
    }

    #[test]
    fn test_config_with_default_registry_only() {
        let config_str = r#"
            default_registry = "test_registry"
        "#;

        let config: Config = toml::from_str(config_str).unwrap();

        assert_eq!(config.default_registry, Some("test_registry".to_string()));
        assert!(config.registries.is_empty());
    }

    #[test]
    fn test_config_with_default_headers_only() {
        let config_str = r#"
            [registries.test_registry]
            default_headers = [ "Authorization: Blah abcd1234" ] 
            
            [registries.other_registry]
            default_headers = [ 
                "PRIVATE-KEY: abcdef",
                "Some-Other-Header: And its value",
            ]
        "#;

        let config: Config = toml::from_str(config_str).unwrap();

        assert!(config.default_registry.is_none());

        let test_registry_config = config.registries.get("test_registry").unwrap();

        let first_header = &test_registry_config.default_headers[0];
        assert_eq!(first_header.name, "Authorization");
        assert_eq!(first_header.value, "Blah abcd1234");

        let other_registry_config = config.registries.get("other_registry").unwrap();
        let header = &other_registry_config.default_headers[0];
        assert_eq!(header.name, "PRIVATE-KEY");
        assert_eq!(header.value, "abcdef");

        let second_header = &other_registry_config.default_headers[1];
        assert_eq!(second_header.name, "Some-Other-Header");
        assert_eq!(second_header.value, "And its value");
    }

    #[test]
    fn test_config_empty() {
        let config_str = "";

        let config: Config = toml::from_str(config_str).unwrap();

        assert!(config.default_registry.is_none());
        assert!(config.registries.is_empty());
    }
}