distri_types/
client_config.rs1use std::path::PathBuf;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6pub(crate) const DEFAULT_BASE_URL: &str = "https://api.distri.dev/v1";
8
9pub(crate) const ENV_BASE_URL: &str = "DISTRI_BASE_URL";
11
12pub(crate) const ENV_API_KEY: &str = "DISTRI_API_KEY";
14
15pub(crate) const ENV_WORKSPACE_ID: &str = "DISTRI_WORKSPACE_ID";
17
18const CONFIG_DIR_NAME: &str = ".distri";
19const CONFIG_FILE_NAME: &str = "config";
20
21#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
23pub struct DistriConfig {
24 pub base_url: String,
26
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub api_key: Option<String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub workspace_id: Option<String>,
34
35 #[serde(default = "default_timeout")]
37 pub timeout_secs: u64,
38
39 #[serde(default = "default_retries")]
41 pub retry_attempts: u32,
42
43 #[serde(skip)]
46 #[schemars(skip)]
47 pub traceparent: Option<String>,
48
49 #[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 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 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 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 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 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 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 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 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
193 self.timeout_secs = timeout_secs;
194 self
195 }
196
197 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
199 self.retry_attempts = retry_attempts;
200 self
201 }
202
203 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 pub fn is_local(&self) -> bool {
213 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
214 }
215
216 pub fn has_auth(&self) -> bool {
218 self.api_key.is_some()
219 }
220}