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