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";
8
9pub(crate) const ENV_BASE_URL: &str = "DISTRI_BASE_URL";
11
12pub(crate) const ENV_API_KEY: &str = "DISTRI_API_KEY";
14
15const CONFIG_DIR_NAME: &str = ".distri";
16const CONFIG_FILE_NAME: &str = "config";
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
20pub struct DistriConfig {
21 pub base_url: String,
23
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub api_key: Option<String>,
27
28 #[serde(default = "default_timeout")]
30 pub timeout_secs: u64,
31
32 #[serde(default = "default_retries")]
34 pub retry_attempts: u32,
35}
36
37fn default_timeout() -> u64 {
38 30
39}
40
41fn default_retries() -> u32 {
42 3
43}
44
45#[derive(Debug, Deserialize, Default)]
46struct FileConfig {
47 base_url: Option<String>,
48 api_key: Option<String>,
49}
50
51fn normalize_optional(value: String) -> Option<String> {
52 let trimmed = value.trim();
53 if trimmed.is_empty() {
54 None
55 } else {
56 Some(trimmed.to_string())
57 }
58}
59
60fn normalize_base_url(value: String) -> Option<String> {
61 normalize_optional(value).map(|s| s.trim_end_matches('/').to_string())
62}
63
64impl FileConfig {
65 fn normalized(self) -> Self {
66 Self {
67 base_url: self.base_url.and_then(normalize_base_url),
68 api_key: self.api_key.and_then(normalize_optional),
69 }
70 }
71}
72
73impl Default for DistriConfig {
74 fn default() -> Self {
75 Self {
76 base_url: DEFAULT_BASE_URL.to_string(),
77 api_key: None,
78 timeout_secs: default_timeout(),
79 retry_attempts: default_retries(),
80 }
81 }
82}
83
84impl DistriConfig {
85 pub fn config_path() -> Option<PathBuf> {
87 let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
88 let mut path = PathBuf::from(home);
89 path.push(CONFIG_DIR_NAME);
90 path.push(CONFIG_FILE_NAME);
91 Some(path)
92 }
93
94 pub fn new(base_url: impl Into<String>) -> Self {
96 Self {
97 base_url: base_url.into().trim_end_matches('/').to_string(),
98 ..Default::default()
99 }
100 }
101
102 pub fn from_env() -> Self {
110 let file_config = Self::config_path()
111 .and_then(|path| std::fs::read_to_string(path).ok())
112 .and_then(|contents| toml::from_str::<FileConfig>(&contents).ok())
113 .map(|cfg| cfg.normalized())
114 .unwrap_or_default();
115
116 let env_base_url = std::env::var(ENV_BASE_URL)
117 .ok()
118 .and_then(normalize_base_url);
119 let env_api_key = std::env::var(ENV_API_KEY).ok().and_then(normalize_optional);
120
121 let base_url = env_base_url
122 .or(file_config.base_url)
123 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
124 let api_key = env_api_key.or(file_config.api_key);
125
126 Self {
127 base_url,
128 api_key,
129 ..Default::default()
130 }
131 }
132
133 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
135 self.api_key = Some(api_key.into());
136 self
137 }
138
139 pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
141 self.timeout_secs = timeout_secs;
142 self
143 }
144
145 pub fn with_retries(mut self, retry_attempts: u32) -> Self {
147 self.retry_attempts = retry_attempts;
148 self
149 }
150
151 pub fn is_local(&self) -> bool {
153 self.base_url.contains("localhost") || self.base_url.contains("127.0.0.1")
154 }
155
156 pub fn has_auth(&self) -> bool {
158 self.api_key.is_some()
159 }
160}