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
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 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 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 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 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 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 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 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 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
186 self.timeout_secs = timeout_secs;
187 self
188 }
189
190 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
192 self.retry_attempts = retry_attempts;
193 self
194 }
195
196 pub fn is_local(&self) -> bool {
198 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
199 }
200
201 pub fn has_auth(&self) -> bool {
203 self.api_key.is_some()
204 }
205}