1use std::{env, time::Duration};
2
3use serde::de::DeserializeOwned;
4
5use crate::{Environment, EnvironmentRequest, Error, Result, SpringConfigClient};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct BootstrapConfig {
13 server_url: String,
14 application: String,
15 profiles: Vec<String>,
16 label: Option<String>,
17 username: Option<String>,
18 password: Option<String>,
19 bearer_token: Option<String>,
20 accept_invalid_tls: bool,
21 timeout: Option<Duration>,
22}
23
24impl BootstrapConfig {
25 pub const SERVER_URL_ENV: &'static str = "SPRING_CONFIG_SERVER_URL";
27 pub const APPLICATION_ENV: &'static str = "SPRING_APPLICATION_NAME";
29 pub const PROFILES_ENV: &'static str = "SPRING_PROFILES_ACTIVE";
31 pub const LABEL_ENV: &'static str = "SPRING_CONFIG_LABEL";
33 pub const USERNAME_ENV: &'static str = "SPRING_CONFIG_USERNAME";
35 pub const PASSWORD_ENV: &'static str = "SPRING_CONFIG_PASSWORD";
37 pub const BEARER_TOKEN_ENV: &'static str = "SPRING_CONFIG_BEARER_TOKEN";
39 pub const INSECURE_TLS_ENV: &'static str = "SPRING_CONFIG_INSECURE_TLS";
41 pub const TIMEOUT_SECONDS_ENV: &'static str = "SPRING_CONFIG_TIMEOUT_SECS";
43
44 pub fn new<A, S, I, P>(server_url: A, application: S, profiles: I) -> Result<Self>
46 where
47 A: Into<String>,
48 S: Into<String>,
49 I: IntoIterator<Item = P>,
50 P: Into<String>,
51 {
52 Ok(Self {
53 server_url: server_url.into().trim().to_string(),
54 application: application.into().trim().to_string(),
55 profiles: profiles
56 .into_iter()
57 .map(Into::into)
58 .map(|value| value.trim().to_string())
59 .filter(|value| !value.is_empty())
60 .collect(),
61 label: None,
62 username: None,
63 password: None,
64 bearer_token: None,
65 accept_invalid_tls: false,
66 timeout: None,
67 }
68 .validate()?)
69 }
70
71 pub fn from_env() -> Result<Self> {
86 let server_url = required_env(Self::SERVER_URL_ENV)?;
87 let application = required_env(Self::APPLICATION_ENV)?;
88 let profiles = optional_env(Self::PROFILES_ENV)
89 .map(split_profiles)
90 .filter(|profiles| !profiles.is_empty())
91 .unwrap_or_else(|| vec!["default".to_string()]);
92 let label = optional_env(Self::LABEL_ENV);
93 let username = optional_env(Self::USERNAME_ENV);
94 let password = optional_env(Self::PASSWORD_ENV);
95 let bearer_token = optional_env(Self::BEARER_TOKEN_ENV);
96 let accept_invalid_tls = optional_env(Self::INSECURE_TLS_ENV)
97 .map(parse_env_bool)
98 .transpose()?
99 .unwrap_or(false);
100 let timeout = optional_env(Self::TIMEOUT_SECONDS_ENV)
101 .map(parse_timeout_seconds)
102 .transpose()?;
103
104 Self {
105 server_url,
106 application,
107 profiles,
108 label,
109 username,
110 password,
111 bearer_token,
112 accept_invalid_tls,
113 timeout,
114 }
115 .validate()
116 }
117
118 pub fn label(mut self, label: impl Into<String>) -> Self {
120 let label = label.into().trim().to_string();
121 self.label = if label.is_empty() { None } else { Some(label) };
122 self
123 }
124
125 pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
127 self.username = Some(username.into());
128 self.password = Some(password.into());
129 self.bearer_token = None;
130 self
131 }
132
133 pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
135 self.bearer_token = Some(token.into());
136 self.username = None;
137 self.password = None;
138 self
139 }
140
141 pub fn danger_accept_invalid_tls(mut self, enabled: bool) -> Self {
145 self.accept_invalid_tls = enabled;
146 self
147 }
148
149 pub fn timeout(mut self, timeout: Duration) -> Self {
151 self.timeout = Some(timeout);
152 self
153 }
154
155 pub fn server_url(&self) -> &str {
157 &self.server_url
158 }
159
160 pub fn application(&self) -> &str {
162 &self.application
163 }
164
165 pub fn profiles(&self) -> &[String] {
167 &self.profiles
168 }
169
170 pub fn label_ref(&self) -> Option<&str> {
172 self.label.as_deref()
173 }
174
175 pub fn build_client(&self) -> Result<SpringConfigClient> {
177 let mut builder = SpringConfigClient::builder(&self.server_url)?;
178
179 if self.accept_invalid_tls {
180 builder = builder.danger_accept_invalid_tls(true);
181 }
182
183 if let Some(label) = &self.label {
184 builder = builder.default_label(label);
185 }
186
187 if let Some(timeout) = self.timeout {
188 builder = builder.timeout(timeout);
189 }
190
191 if let Some(token) = &self.bearer_token {
192 builder = builder.bearer_auth(token);
193 } else if let Some(username) = &self.username {
194 builder = builder.basic_auth(username, self.password.clone().unwrap_or_default());
195 }
196
197 builder.build()
198 }
199
200 pub fn environment_request(&self) -> Result<EnvironmentRequest> {
202 let mut request = EnvironmentRequest::new(&self.application, self.profiles.clone())?;
203 if let Some(label) = &self.label {
204 request = request.label(label.clone());
205 }
206 Ok(request)
207 }
208
209 pub async fn load_environment(&self) -> Result<Environment> {
211 let client = self.build_client()?;
212 let request = self.environment_request()?;
213 client.fetch_environment(&request).await
214 }
215
216 pub async fn load_typed<T>(&self) -> Result<T>
218 where
219 T: DeserializeOwned,
220 {
221 let client = self.build_client()?;
222 let request = self.environment_request()?;
223 client.fetch_typed(&request).await
224 }
225
226 fn validate(mut self) -> Result<Self> {
227 self.server_url = self.server_url.trim().to_string();
228 self.application = self.application.trim().to_string();
229 self.profiles = self
230 .profiles
231 .into_iter()
232 .map(|value| value.trim().to_string())
233 .filter(|value| !value.is_empty())
234 .collect();
235 self.label = self
236 .label
237 .as_ref()
238 .map(|value| value.trim().to_string())
239 .filter(|value| !value.is_empty());
240
241 if self.server_url.is_empty() {
242 return Err(Error::MissingEnvironmentVariable {
243 name: Self::SERVER_URL_ENV,
244 });
245 }
246
247 if self.application.is_empty() {
248 return Err(Error::MissingEnvironmentVariable {
249 name: Self::APPLICATION_ENV,
250 });
251 }
252
253 if self.profiles.is_empty() {
254 return Err(Error::InvalidBootstrapConfiguration(
255 "at least one profile must be provided".to_string(),
256 ));
257 }
258
259 if self.bearer_token.is_some() && (self.username.is_some() || self.password.is_some()) {
260 return Err(Error::InvalidBootstrapConfiguration(
261 "basic authentication and bearer authentication are mutually exclusive".to_string(),
262 ));
263 }
264
265 Ok(self)
266 }
267}
268
269fn required_env(name: &'static str) -> Result<String> {
270 optional_env(name).ok_or(Error::MissingEnvironmentVariable { name })
271}
272
273fn optional_env(name: &'static str) -> Option<String> {
274 env::var(name)
275 .ok()
276 .map(|value| value.trim().to_string())
277 .filter(|value| !value.is_empty())
278}
279
280fn split_profiles(value: String) -> Vec<String> {
281 value
282 .split(',')
283 .map(|profile| profile.trim().to_string())
284 .filter(|profile| !profile.is_empty())
285 .collect()
286}
287
288fn parse_timeout_seconds(value: String) -> Result<Duration> {
289 let seconds = value
290 .parse::<u64>()
291 .map_err(|_| Error::InvalidEnvironmentVariable {
292 name: BootstrapConfig::TIMEOUT_SECONDS_ENV,
293 reason: "expected an unsigned integer",
294 value: value.clone(),
295 })?;
296
297 Ok(Duration::from_secs(seconds))
298}
299
300fn parse_env_bool(value: String) -> Result<bool> {
301 match value.trim().to_ascii_lowercase().as_str() {
302 "1" | "true" | "yes" => Ok(true),
303 "0" | "false" | "no" => Ok(false),
304 _ => Err(Error::InvalidEnvironmentVariable {
305 name: BootstrapConfig::INSECURE_TLS_ENV,
306 reason: "expected true, false, yes, no, 1, or 0",
307 value,
308 }),
309 }
310}