distri_types/
client_config.rs

1use std::path::PathBuf;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// Default base URL for the Distri cloud service
7pub(crate) const DEFAULT_BASE_URL: &str = "https://api.distri.dev";
8
9/// Environment variable for the base URL
10pub(crate) const ENV_BASE_URL: &str = "DISTRI_BASE_URL";
11
12/// Environment variable for the API key
13pub(crate) const ENV_API_KEY: &str = "DISTRI_API_KEY";
14
15const CONFIG_DIR_NAME: &str = ".distri";
16const CONFIG_FILE_NAME: &str = "config";
17
18/// Configuration for the Distri client.
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
20pub struct DistriConfig {
21    /// Base URL of the Distri server
22    pub base_url: String,
23
24    /// Optional API key for authentication
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub api_key: Option<String>,
27
28    /// Request timeout in seconds (default: 30)
29    #[serde(default = "default_timeout")]
30    pub timeout_secs: u64,
31
32    /// Number of retry attempts for failed requests (default: 3)
33    #[serde(default = "default_retries")]
34    pub retry_attempts: u32,
35}
36
37fn default_timeout() -> u64 {
38    30
39}
40
41fn default_retries() -> u32 {
42    3
43}
44
45#[derive(Debug, Deserialize, Default)]
46struct FileConfig {
47    base_url: Option<String>,
48    api_key: Option<String>,
49}
50
51fn normalize_optional(value: String) -> Option<String> {
52    let trimmed = value.trim();
53    if trimmed.is_empty() {
54        None
55    } else {
56        Some(trimmed.to_string())
57    }
58}
59
60fn normalize_base_url(value: String) -> Option<String> {
61    normalize_optional(value).map(|s| s.trim_end_matches('/').to_string())
62}
63
64impl FileConfig {
65    fn normalized(self) -> Self {
66        Self {
67            base_url: self.base_url.and_then(normalize_base_url),
68            api_key: self.api_key.and_then(normalize_optional),
69        }
70    }
71}
72
73impl Default for DistriConfig {
74    fn default() -> Self {
75        Self {
76            base_url: DEFAULT_BASE_URL.to_string(),
77            api_key: None,
78            timeout_secs: default_timeout(),
79            retry_attempts: default_retries(),
80        }
81    }
82}
83
84impl DistriConfig {
85    /// Path to the local client config file (`~/.distri/config`).
86    pub fn config_path() -> Option<PathBuf> {
87        let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
88        let mut path = PathBuf::from(home);
89        path.push(CONFIG_DIR_NAME);
90        path.push(CONFIG_FILE_NAME);
91        Some(path)
92    }
93
94    /// Create a new config with the specified base URL.
95    pub fn new(base_url: impl Into<String>) -> Self {
96        Self {
97            base_url: base_url.into().trim_end_matches('/').to_string(),
98            ..Default::default()
99        }
100    }
101
102    /// Create a config from environment variables and the local config file.
103    ///
104    /// Precedence: environment variables > `~/.distri/config` > defaults.
105    /// `~/.distri/config` supports `base_url` and `api_key`.
106    ///
107    /// - `DISTRI_BASE_URL`: Base URL (defaults to `https://api.distri.dev`)
108    /// - `DISTRI_API_KEY`: Optional API key
109    pub fn from_env() -> Self {
110        let file_config = Self::config_path()
111            .and_then(|path| std::fs::read_to_string(path).ok())
112            .and_then(|contents| toml::from_str::<FileConfig>(&contents).ok())
113            .map(|cfg| cfg.normalized())
114            .unwrap_or_default();
115
116        let env_base_url = std::env::var(ENV_BASE_URL)
117            .ok()
118            .and_then(normalize_base_url);
119        let env_api_key = std::env::var(ENV_API_KEY).ok().and_then(normalize_optional);
120
121        let base_url = env_base_url
122            .or(file_config.base_url)
123            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
124        let api_key = env_api_key.or(file_config.api_key);
125
126        Self {
127            base_url,
128            api_key,
129            ..Default::default()
130        }
131    }
132
133    /// Set the API key for authentication.
134    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
135        self.api_key = Some(api_key.into());
136        self
137    }
138
139    /// Set the request timeout in seconds.
140    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
141        self.timeout_secs = timeout_secs;
142        self
143    }
144
145    /// Set the number of retry attempts.
146    pub fn with_retries(mut self, retry_attempts: u32) -> Self {
147        self.retry_attempts = retry_attempts;
148        self
149    }
150
151    /// Check if the client is configured for local development (localhost).
152    pub fn is_local(&self) -> bool {
153        self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
154    }
155
156    /// Check if authentication is configured.
157    pub fn has_auth(&self) -> bool {
158        self.api_key.is_some()
159    }
160}