1use anyhow::Context;
2use std::env::VarError;
3use std::path::PathBuf;
4
5#[derive(clap::Args, Debug, Clone)]
6pub struct ConfigArgs {
7 #[clap(short, long, global = true)]
8 pub profile: Option<String>,
10}
11
12pub fn load_env_files(app_name: &str, args: &ConfigArgs) -> anyhow::Result<()> {
13 let config_dir = {
14 let xdg_config_dir = std::env::var("XDG_CONFIG_HOME")
15 .map(|config_path| PathBuf::from(config_path).join("dcli"))
16 .ok()
17 .filter(|path| std::fs::exists(path).is_ok_and(|exists| exists));
18
19 let home_config_dir = std::env::var("HOME")
20 .map(|home_dir| PathBuf::from(home_dir).join(".dcli"))
21 .ok()
22 .filter(|path| std::fs::exists(path).is_ok_and(|exists| exists));
23
24 match (xdg_config_dir, home_config_dir) {
25 (Some(xdg_config_dir), Some(home_config_dir)) => {
26 anyhow::bail!(
27 "found configurations in both {} and {}. Please merge configurations in one location",
28 xdg_config_dir.display(),
29 home_config_dir.display()
30 )
31 }
32 (Some(xdg_config_dir), None) => {
33 tracing::info!("Using XDG_CONFIG_HOME={}", xdg_config_dir.display());
34 xdg_config_dir
35 }
36 (None, Some(home_config_dir)) => {
37 tracing::info!("Using HOME={}", home_config_dir.display());
38 home_config_dir
39 }
40 (None, None) => {
41 anyhow::bail!(
42 "Unable to find configuration in either $HOME/.dcli or $XDG_CONFIG_HOME/dcli. Please create one of them and try again."
43 );
44 }
45 }
46 };
47
48 if let Some(profile) = args.profile.as_deref() {
50 load_file(config_dir.join(format!("{profile}.env")))
51 .context("Unable to load configuration file for profile")?;
52 } else {
53 load_file_opt(config_dir.join("default.env"))?;
54 }
55
56 load_file_opt(config_dir.join(format!("{app_name}.env")))?;
58
59 load_file_opt(config_dir.join("global.env"))?;
61
62 Ok(())
63}
64
65fn load_file(path: PathBuf) -> anyhow::Result<()> {
66 tracing::debug!(file = %path.display(), "loading mandatory file");
67 dotenvy::from_path(path).context("File is required")
68}
69
70fn load_file_opt(path: PathBuf) -> anyhow::Result<()> {
71 tracing::debug!(file = %path.display(), "loading optional file");
72 if let Err(e) = dotenvy::from_path(&path)
73 && !e.not_found()
74 {
75 tracing::debug!(file = %path.display(), "file does not exist, ignoring");
76 Err(e)?;
77 }
78
79 Ok(())
80}
81
82pub fn get(name: &str) -> anyhow::Result<String> {
83 get_opt(name)?.ok_or_else(|| is_mandatory_err(name))
84}
85
86pub fn get_opt(name: &str) -> anyhow::Result<Option<String>> {
87 let plain = read_env_var(name)?;
88
89 Ok(plain)
92}
93
94pub fn get_bool(name: &str) -> anyhow::Result<bool> {
95 get_bool_opt(name)?.ok_or_else(|| is_mandatory_err(name))
96}
97
98pub fn get_bool_opt(name: &str) -> anyhow::Result<Option<bool>> {
99 let Some(value) = get_opt(name)? else {
100 return Ok(None);
101 };
102 match value.as_str() {
103 "yes" | "true" | "1" | "on" | "enable" | "enabled" => Ok(Some(true)),
104 "no" | "false" | "0" | "off" | "disable" | "disabled" => Ok(Some(false)),
105 _ => Err(anyhow::anyhow!(
106 "Invalid boolean value for {name}: `{value}`"
107 ))?,
108 }
109}
110
111pub fn get_json<T: serde::de::DeserializeOwned>(name: &str) -> anyhow::Result<T> {
112 get_json_opt(name)?.ok_or_else(|| is_mandatory_err(name))
113}
114
115pub fn get_json_opt<T: serde::de::DeserializeOwned>(name: &str) -> anyhow::Result<Option<T>> {
116 let Some(json) = get_opt(name)? else {
117 return Ok(None);
118 };
119
120 serde_json::from_str(&json).context(format!("Invalid JSON for {name}: `{json}`"))
121}
122
123fn read_env_var(name: &str) -> anyhow::Result<Option<String>> {
124 match std::env::var(name) {
125 Ok(value) => Ok(Some(value).filter(|value| !value.is_empty())),
126 Err(VarError::NotPresent) => Ok(None),
127 Err(e) => Err(e).context(format!(
128 "Environment variable value can not be parsed as UTF-8 ({name})"
129 )),
130 }
131}
132
133fn is_mandatory_err(name: &str) -> anyhow::Error {
134 anyhow::anyhow!("{name} is not set, but it is required")
135}