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>, #[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>, #[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 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}