Skip to main content

ironflow_cli/
config.rs

1//! Configuration loading for the Ironflow CLI.
2//!
3//! Supports three configuration sources with the following priority
4//! (highest wins):
5//!
6//! 1. CLI arguments (`--url`, `--api-key`)
7//! 2. Environment variables (`IRONFLOW_URL`, `IRONFLOW_API_KEY`)
8//! 3. TOML file at `~/.ironflow.toml`
9
10use std::fs;
11use std::path::PathBuf;
12
13use anyhow::{Context, Result, bail};
14use serde::Deserialize;
15
16/// TOML file representation.
17#[derive(Debug, Deserialize)]
18struct FileConfig {
19    base_url: Option<String>,
20    api_key: Option<String>,
21}
22
23/// Resolved configuration ready to build an [`ironflow_sdk::IronflowClient`].
24#[derive(Debug, Clone)]
25pub struct Config {
26    /// Base URL of the Ironflow API.
27    pub base_url: String,
28    /// API key for Bearer authentication.
29    pub api_key: String,
30}
31
32/// Default config file path: `~/.ironflow.toml`.
33pub fn default_config_path() -> Option<PathBuf> {
34    dirs::home_dir().map(|h| h.join(".ironflow.toml"))
35}
36
37/// Load configuration by merging CLI args, env vars, and the TOML file.
38///
39/// # Errors
40///
41/// Returns an error when neither `base_url` nor `api_key` can be resolved
42/// from any source.
43pub 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}