1use std::fs;
11use std::path::PathBuf;
12
13use anyhow::{Context, Result, bail};
14use serde::Deserialize;
15
16#[derive(Debug, Deserialize)]
18struct FileConfig {
19 base_url: Option<String>,
20 api_key: Option<String>,
21}
22
23#[derive(Debug, Clone)]
25pub struct Config {
26 pub base_url: String,
28 pub api_key: String,
30}
31
32pub fn default_config_path() -> Option<PathBuf> {
34 dirs::home_dir().map(|h| h.join(".ironflow.toml"))
35}
36
37pub fn load(cli_url: Option<&str>, cli_api_key: Option<&str>) -> Result<Config> {
44 let file_config = default_config_path().filter(|p| p.exists()).and_then(|p| {
45 let content = fs::read_to_string(&p).ok()?;
46 toml::from_str::<FileConfig>(&content).ok()
47 });
48
49 let base_url = cli_url
50 .map(String::from)
51 .or_else(|| std::env::var("IRONFLOW_URL").ok())
52 .or_else(|| file_config.as_ref().and_then(|f| f.base_url.clone()));
53
54 let api_key = cli_api_key
55 .map(String::from)
56 .or_else(|| std::env::var("IRONFLOW_API_KEY").ok())
57 .or_else(|| file_config.as_ref().and_then(|f| f.api_key.clone()));
58
59 let base_url = base_url
60 .context("missing base_url: set --url, IRONFLOW_URL, or base_url in ~/.ironflow.toml")?;
61 let api_key = api_key.context(
62 "missing api_key: set --api-key, IRONFLOW_API_KEY, or api_key in ~/.ironflow.toml",
63 )?;
64
65 if base_url.is_empty() {
66 bail!("base_url cannot be empty");
67 }
68 if api_key.is_empty() {
69 bail!("api_key cannot be empty");
70 }
71
72 Ok(Config { base_url, api_key })
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn cli_args_override_everything() {
81 let config = load(Some("https://cli.example.com"), Some("cli-key")).unwrap();
82 assert_eq!(config.base_url, "https://cli.example.com");
83 assert_eq!(config.api_key, "cli-key");
84 }
85
86 #[test]
87 fn error_when_empty_base_url() {
88 let result = load(Some(""), Some("key"));
89 assert!(result.is_err());
90 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
91 }
92
93 #[test]
94 fn error_when_empty_api_key() {
95 let result = load(Some("https://example.com"), Some(""));
96 assert!(result.is_err());
97 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
98 }
99
100 #[test]
101 fn toml_parsing_works() {
102 let toml_content = r#"
103base_url = "https://toml.example.com"
104api_key = "toml-key"
105"#;
106 let parsed: FileConfig = toml::from_str(toml_content).unwrap();
107 assert_eq!(parsed.base_url.unwrap(), "https://toml.example.com");
108 assert_eq!(parsed.api_key.unwrap(), "toml-key");
109 }
110
111 #[test]
112 fn toml_partial_config() {
113 let toml_content = r#"
114base_url = "https://toml.example.com"
115"#;
116 let parsed: FileConfig = toml::from_str(toml_content).unwrap();
117 assert_eq!(parsed.base_url.unwrap(), "https://toml.example.com");
118 assert!(parsed.api_key.is_none());
119 }
120
121 #[test]
122 fn default_config_path_ends_with_ironflow_toml() {
123 let path = default_config_path();
124 assert!(path.is_some());
125 assert!(path.unwrap().ends_with(".ironflow.toml"));
126 }
127}