Skip to main content

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 (includes /v1 API version)
7pub(crate) const DEFAULT_BASE_URL: &str = "https://api.distri.dev/v1";
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
15/// Environment variable for the workspace ID
16pub(crate) const ENV_WORKSPACE_ID: &str = "DISTRI_WORKSPACE_ID";
17
18const CONFIG_DIR_NAME: &str = ".distri";
19const CONFIG_FILE_NAME: &str = "config";
20
21/// Configuration for the Distri client.
22#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
23pub struct DistriConfig {
24    /// Base URL of the Distri server
25    pub base_url: String,
26
27    /// Optional API key for authentication
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub api_key: Option<String>,
30
31    /// Optional workspace ID for multi-tenant context
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub workspace_id: Option<String>,
34
35    /// Request timeout in seconds (default: 30)
36    #[serde(default = "default_timeout")]
37    pub timeout_secs: u64,
38
39    /// Number of retry attempts for failed requests (default: 3)
40    #[serde(default = "default_retries")]
41    pub retry_attempts: u32,
42
43    /// W3C traceparent header value for distributed tracing.
44    /// Not serialized — set at runtime via CLI flag or programmatically.
45    #[serde(skip)]
46    #[schemars(skip)]
47    pub traceparent: Option<String>,
48
49    /// Extra HTTP headers to attach to every request (e.g. set via the
50    /// `--header k=v` CLI flag). Not serialized — runtime only.
51    #[serde(skip)]
52    #[schemars(skip)]
53    pub headers: Option<std::collections::HashMap<String, String>>,
54}
55
56fn default_timeout() -> u64 {
57    30
58}
59
60fn default_retries() -> u32 {
61    3
62}
63
64#[derive(Debug, Deserialize, Default)]
65struct FileConfig {
66    base_url: Option<String>,
67    api_key: Option<String>,
68    workspace_id: Option<String>,
69}
70
71fn normalize_optional(value: String) -> Option<String> {
72    let trimmed = value.trim();
73    if trimmed.is_empty() {
74        None
75    } else {
76        Some(trimmed.to_string())
77    }
78}
79
80fn normalize_base_url(value: String) -> Option<String> {
81    normalize_optional(value).map(|s| s.trim_end_matches('/').to_string())
82}
83
84impl FileConfig {
85    fn normalized(self) -> Self {
86        Self {
87            base_url: self.base_url.and_then(normalize_base_url),
88            api_key: self.api_key.and_then(normalize_optional),
89            workspace_id: self.workspace_id.and_then(normalize_optional),
90        }
91    }
92}
93
94impl Default for DistriConfig {
95    fn default() -> Self {
96        Self {
97            base_url: DEFAULT_BASE_URL.to_string(),
98            api_key: None,
99            workspace_id: None,
100            timeout_secs: default_timeout(),
101            retry_attempts: default_retries(),
102            traceparent: None,
103            headers: None,
104        }
105    }
106}
107
108impl DistriConfig {
109    /// Path to the local client config file (`~/.distri/config`).
110    pub fn config_path() -> Option<PathBuf> {
111        let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
112        let mut path = PathBuf::from(home);
113        path.push(CONFIG_DIR_NAME);
114        path.push(CONFIG_FILE_NAME);
115        Some(path)
116    }
117
118    /// Create a new config with the specified base URL.
119    pub fn new(base_url: impl Into<String>) -> Self {
120        Self {
121            base_url: base_url.into().trim_end_matches('/').to_string(),
122            ..Default::default()
123        }
124    }
125
126    /// Create a config from environment variables and the local config file.
127    ///
128    /// Precedence: environment variables > `~/.distri/config` > defaults.
129    /// `~/.distri/config` supports `base_url`, `api_key`, and `workspace_id`.
130    ///
131    /// - `DISTRI_BASE_URL`: Base URL (defaults to `https://api.distri.dev/v1`)
132    /// - `DISTRI_API_KEY`: Optional API key
133    /// - `DISTRI_WORKSPACE_ID`: Optional workspace ID (UUID)
134    pub fn from_env() -> Self {
135        let file_config = Self::config_path()
136            .and_then(|path| std::fs::read_to_string(path).ok())
137            .and_then(|contents| toml::from_str::<FileConfig>(&contents).ok())
138            .map(|cfg| cfg.normalized())
139            .unwrap_or_default();
140
141        let env_base_url = std::env::var(ENV_BASE_URL)
142            .ok()
143            .and_then(normalize_base_url);
144        let env_api_key = std::env::var(ENV_API_KEY).ok().and_then(normalize_optional);
145        let env_workspace_id = std::env::var(ENV_WORKSPACE_ID)
146            .ok()
147            .and_then(normalize_optional);
148
149        let base_url = env_base_url
150            .or(file_config.base_url)
151            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
152        let api_key = env_api_key.or(file_config.api_key);
153        let workspace_id = env_workspace_id.or(file_config.workspace_id);
154
155        Self {
156            base_url,
157            api_key,
158            workspace_id,
159            ..Default::default()
160        }
161    }
162
163    /// Set the API key for authentication.
164    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
165        self.api_key = Some(api_key.into());
166        self
167    }
168
169    /// Set the workspace ID for multi-tenant context.
170    pub fn with_workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
171        self.workspace_id = Some(workspace_id.into());
172        self
173    }
174
175    /// Set the API key if Some. Does not change the value when None.
176    pub fn with_maybe_api_key(mut self, api_key: Option<String>) -> Self {
177        if api_key.is_some() {
178            self.api_key = api_key;
179        }
180        self
181    }
182
183    /// Set the workspace ID if Some. Does not change the value when None.
184    pub fn with_maybe_workspace_id(mut self, workspace_id: Option<String>) -> Self {
185        if workspace_id.is_some() {
186            self.workspace_id = workspace_id;
187        }
188        self
189    }
190
191    /// Set the request timeout in seconds.
192    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
193        self.timeout_secs = timeout_secs;
194        self
195    }
196
197    /// Set the number of retry attempts.
198    pub fn with_retries(mut self, retry_attempts: u32) -> Self {
199        self.retry_attempts = retry_attempts;
200        self
201    }
202
203    /// Attach extra HTTP headers sent on every request.
204    pub fn with_headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
205        if !headers.is_empty() {
206            self.headers = Some(headers);
207        }
208        self
209    }
210
211    /// Check if the client is configured for local development (localhost).
212    pub fn is_local(&self) -> bool {
213        self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
214    }
215
216    /// Check if authentication is configured.
217    pub fn has_auth(&self) -> bool {
218        self.api_key.is_some()
219    }
220}