wick_settings/
settings.rs1use std::path::PathBuf;
2use std::vec;
3
4use serde::{Deserialize, Serialize};
5use tracing::{debug, warn};
6
7use crate::error::Error;
8
9#[derive(Debug, Default, Deserialize, Serialize, derive_builder::Builder)]
10#[allow(unused)]
11#[non_exhaustive]
12#[serde(rename_all = "snake_case")]
13#[serde(deny_unknown_fields)]
14#[builder(pattern = "owned", default)]
15pub struct Settings {
16 #[serde(default)]
17 pub trace: TraceSettings,
19 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub credentials: Vec<Credential>,
22 #[serde(skip)]
23 pub source: Option<PathBuf>,
25}
26
27#[derive(Debug, Default, Deserialize, Serialize, Clone)]
28#[serde(rename_all = "snake_case")]
29#[serde(deny_unknown_fields)]
30#[non_exhaustive]
31pub struct TraceSettings {
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub otlp: Option<String>,
36 #[serde(default)]
38 pub level: LogLevel,
39 #[serde(default)]
41 pub modifier: LogModifier,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub telemetry: Option<LogSettings>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub stderr: Option<LogSettings>,
48}
49
50#[derive(Debug, Default, Deserialize, Serialize, Clone)]
51#[non_exhaustive]
52#[serde(rename_all = "snake_case")]
53#[serde(deny_unknown_fields)]
54pub struct LogSettings {
56 pub filter: Option<String>,
58}
59
60#[derive(Debug, Deserialize, Serialize)]
61#[non_exhaustive]
62#[serde(rename_all = "snake_case")]
63#[serde(deny_unknown_fields)]
64pub struct Credential {
66 pub scope: String,
68 pub auth: Auth,
70}
71
72impl Credential {
73 #[must_use]
75 pub const fn new(scope: String, auth: Auth) -> Self {
76 Self { scope, auth }
77 }
78}
79
80#[derive(Debug, Deserialize, Serialize)]
81#[allow(clippy::exhaustive_enums)]
82#[serde(rename_all = "snake_case")]
83#[serde(tag = "type")]
84pub enum Auth {
86 Basic(BasicAuth),
88}
89
90#[derive(Deserialize, Serialize)]
91#[non_exhaustive]
92#[serde(deny_unknown_fields)]
93#[serde(rename_all = "snake_case")]
94pub struct BasicAuth {
96 pub username: String,
98 pub password: String,
100}
101
102impl BasicAuth {
103 #[must_use]
105 pub const fn new(username: String, password: String) -> Self {
106 Self { username, password }
107 }
108}
109
110impl std::fmt::Debug for BasicAuth {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("BasicAuth")
113 .field("username", &self.username)
114 .field("password", &"<HIDDEN>")
115 .finish()
116 }
117}
118
119#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)]
120#[allow(clippy::exhaustive_enums)]
121#[serde(rename_all = "snake_case")]
122pub enum LogLevel {
124 Off,
126 Error,
128 Warn,
130 Info,
132 Debug,
134 Trace,
136}
137
138impl Default for LogLevel {
139 fn default() -> Self {
140 Self::Info
141 }
142}
143
144#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)]
145#[allow(clippy::exhaustive_enums)]
146#[serde(rename_all = "snake_case")]
147pub enum LogModifier {
149 None,
151 Verbose,
153}
154
155impl Default for LogModifier {
156 fn default() -> Self {
157 Self::None
158 }
159}
160
161impl Settings {
162 pub fn new() -> Self {
163 let _span = tracing::info_span!("settings").entered();
164 let extensions = vec!["yaml", "yml"];
165
166 let xdg = wick_xdg::Settings::new();
167
168 let config_locations = vec![
169 xdg.config_dir().join(xdg.configfile_basename()),
170 PathBuf::from(xdg.configfile_basename()),
171 ];
172
173 tracing::debug!(
174 paths = ?config_locations.iter().map(|v| format!("{}.({})",v.display(),extensions.join(","))).collect::<Vec<_>>(),
175 "searching for config files"
176 );
177
178 let mut files = find_settings(&config_locations, &extensions);
179
180 debug!("loaded");
181
182 if !files.is_empty() {
183 files.remove(0)
184 } else {
185 Self::default()
186 }
187 }
188
189 pub fn save(&self) -> Result<(), Error> {
190 let source = self.source.as_ref().ok_or(Error::NoSource)?;
191 let yaml = serde_yaml::to_string(self).unwrap();
192 std::fs::write(source, yaml).map_err(|e| Error::SaveFailed(source.clone(), e))?;
193 Ok(())
194 }
195}
196
197#[allow(clippy::cognitive_complexity)]
198fn find_settings(config_locations: &[PathBuf], extensions: &[&str]) -> Vec<Settings> {
199 let mut files = Vec::new();
200 for path in config_locations {
201 for ext in extensions {
202 let mut path = path.clone();
203 path.set_extension(ext);
204 if path.exists() {
205 match std::fs::read_to_string(&path) {
206 Ok(src) => match serde_yaml::from_str::<Settings>(&src) {
207 Ok(mut settings) => {
208 debug!(file=%path.display(),"found config");
209 settings.source = Some(path.clone());
210 files.push(settings);
211
212 break; }
214 Err(e) => {
215 warn!(error=%e,file=%path.display(),"failed to parse config");
216 }
217 },
218 Err(e) => {
219 warn!(error=%e,file=%path.display(),"failed to read config");
220 }
221 };
222 }
223 }
224 }
225 files
226}