Skip to main content

redis_common/
config.rs

1use anyhow::{Context, Result};
2use directories::ProjectDirs;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Serialize, Deserialize, Default, Clone)]
9pub struct Config {
10    #[serde(default)]
11    pub default: Option<String>, // Name of the default profile
12    #[serde(default)]
13    pub profiles: HashMap<String, Profile>,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct Profile {
18    pub deployment_type: DeploymentType,
19    #[serde(flatten)]
20    pub credentials: ProfileCredentials,
21}
22
23#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, clap::ValueEnum)]
24pub enum DeploymentType {
25    Cloud,
26    Enterprise,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
30#[serde(untagged)]
31pub enum ProfileCredentials {
32    Cloud {
33        api_key: String,
34        api_secret: String,
35        #[serde(default = "default_cloud_url")]
36        api_url: String,
37    },
38    Enterprise {
39        url: String,
40        username: String,
41        password: Option<String>, // Optional for prompting
42        #[serde(default)]
43        insecure: bool,
44    },
45}
46
47fn default_cloud_url() -> String {
48    "https://api.redislabs.com/v1".to_string()
49}
50
51impl ProfileCredentials {
52    pub fn has_password(&self) -> bool {
53        match self {
54            ProfileCredentials::Enterprise { password, .. } => password.is_some(),
55            _ => false,
56        }
57    }
58}
59
60impl Config {
61    pub fn load() -> Result<Self> {
62        let config_path = Self::config_path()?;
63
64        if !config_path.exists() {
65            return Ok(Config::default());
66        }
67
68        let content = fs::read_to_string(&config_path)
69            .with_context(|| format!("Failed to read config from {:?}", config_path))?;
70
71        toml::from_str(&content)
72            .with_context(|| format!("Failed to parse config from {:?}", config_path))
73    }
74
75    pub fn save(&self) -> Result<()> {
76        let config_path = Self::config_path()?;
77
78        // Create parent directories if they don't exist
79        if let Some(parent) = config_path.parent() {
80            fs::create_dir_all(parent)
81                .with_context(|| format!("Failed to create config directory {:?}", parent))?;
82        }
83
84        let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
85
86        fs::write(&config_path, content)
87            .with_context(|| format!("Failed to write config to {:?}", config_path))?;
88
89        Ok(())
90    }
91
92    pub fn get_profile(&self, name: Option<&str>) -> Option<&Profile> {
93        let env_profile = std::env::var("REDISCTL_PROFILE").ok();
94        let profile_name = name
95            .or(self.default.as_deref())
96            .or(env_profile.as_deref())?;
97
98        self.profiles.get(profile_name)
99    }
100
101    pub fn set_profile(&mut self, name: String, profile: Profile) {
102        self.profiles.insert(name, profile);
103    }
104
105    pub fn remove_profile(&mut self, name: &str) -> Option<Profile> {
106        self.profiles.remove(name)
107    }
108
109    pub fn list_profiles(&self) -> Vec<(&String, &Profile)> {
110        let mut profiles: Vec<_> = self.profiles.iter().collect();
111        profiles.sort_by_key(|(name, _)| *name);
112        profiles
113    }
114
115    fn config_path() -> Result<PathBuf> {
116        let proj_dirs = ProjectDirs::from("com", "redis", "redisctl")
117            .context("Failed to determine config directory")?;
118
119        Ok(proj_dirs.config_dir().join("config.toml"))
120    }
121}