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
50fn default_timeout() -> u64 {
51    30
52}
53
54fn default_retries() -> u32 {
55    3
56}
57
58#[derive(Debug, Deserialize, Default)]
59struct FileConfig {
60    base_url: Option<String>,
61    api_key: Option<String>,
62    workspace_id: Option<String>,
63}
64
65fn normalize_optional(value: String) -> Option<String> {
66    let trimmed = value.trim();
67    if trimmed.is_empty() {
68        None
69    } else {
70        Some(trimmed.to_string())
71    }
72}
73
74fn normalize_base_url(value: String) -> Option<String> {
75    normalize_optional(value).map(|s| s.trim_end_matches('/').to_string())
76}
77
78impl FileConfig {
79    fn normalized(self) -> Self {
80        Self {
81            base_url: self.base_url.and_then(normalize_base_url),
82            api_key: self.api_key.and_then(normalize_optional),
83            workspace_id: self.workspace_id.and_then(normalize_optional),
84        }
85    }
86}
87
88impl Default for DistriConfig {
89    fn default() -> Self {
90        Self {
91            base_url: DEFAULT_BASE_URL.to_string(),
92            api_key: None,
93            workspace_id: None,
94            timeout_secs: default_timeout(),
95            retry_attempts: default_retries(),
96            traceparent: None,
97        }
98    }
99}
100
101impl DistriConfig {
102    /// Path to the local client config file (`~/.distri/config`).
103    pub fn config_path() -> Option<PathBuf> {
104        let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
105        let mut path = PathBuf::from(home);
106        path.push(CONFIG_DIR_NAME);
107        path.push(CONFIG_FILE_NAME);
108        Some(path)
109    }
110
111    /// Create a new config with the specified base URL.
112    pub fn new(base_url: impl Into<String>) -> Self {
113        Self {
114            base_url: base_url.into().trim_end_matches('/').to_string(),
115            ..Default::default()
116        }
117    }
118
119    /// Create a config from environment variables and the local config file.
120    ///
121    /// Precedence: environment variables > `~/.distri/config` > defaults.
122    /// `~/.distri/config` supports `base_url`, `api_key`, and `workspace_id`.
123    ///
124    /// - `DISTRI_BASE_URL`: Base URL (defaults to `https://api.distri.dev/v1`)
125    /// - `DISTRI_API_KEY`: Optional API key
126    /// - `DISTRI_WORKSPACE_ID`: Optional workspace ID (UUID)
127    pub fn from_env() -> Self {
128        let file_config = Self::config_path()
129            .and_then(|path| std::fs::read_to_string(path).ok())
130            .and_then(|contents| toml::from_str::<FileConfig>(&contents).ok())
131            .map(|cfg| cfg.normalized())
132            .unwrap_or_default();
133
134        let env_base_url = std::env::var(ENV_BASE_URL)
135            .ok()
136            .and_then(normalize_base_url);
137        let env_api_key = std::env::var(ENV_API_KEY).ok().and_then(normalize_optional);
138        let env_workspace_id = std::env::var(ENV_WORKSPACE_ID)
139            .ok()
140            .and_then(normalize_optional);
141
142        let base_url = env_base_url
143            .or(file_config.base_url)
144            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
145        let api_key = env_api_key.or(file_config.api_key);
146        let workspace_id = env_workspace_id.or(file_config.workspace_id);
147
148        Self {
149            base_url,
150            api_key,
151            workspace_id,
152            ..Default::default()
153        }
154    }
155
156    /// Set the API key for authentication.
157    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
158        self.api_key = Some(api_key.into());
159        self
160    }
161
162    /// Set the workspace ID for multi-tenant context.
163    pub fn with_workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
164        self.workspace_id = Some(workspace_id.into());
165        self
166    }
167
168    /// Set the API key if Some. Does not change the value when None.
169    pub fn with_maybe_api_key(mut self, api_key: Option<String>) -> Self {
170        if api_key.is_some() {
171            self.api_key = api_key;
172        }
173        self
174    }
175
176    /// Set the workspace ID if Some. Does not change the value when None.
177    pub fn with_maybe_workspace_id(mut self, workspace_id: Option<String>) -> Self {
178        if workspace_id.is_some() {
179            self.workspace_id = workspace_id;
180        }
181        self
182    }
183
184    /// Set the request timeout in seconds.
185    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
186        self.timeout_secs = timeout_secs;
187        self
188    }
189
190    /// Set the number of retry attempts.
191    pub fn with_retries(mut self, retry_attempts: u32) -> Self {
192        self.retry_attempts = retry_attempts;
193        self
194    }
195
196    /// Check if the client is configured for local development (localhost).
197    pub fn is_local(&self) -> bool {
198        self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
199    }
200
201    /// Check if authentication is configured.
202    pub fn has_auth(&self) -> bool {
203        self.api_key.is_some()
204    }
205}