wick_settings/
settings.rs

1use 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  /// Logging configuration.
18  pub trace: TraceSettings,
19  /// Registry credentials.
20  #[serde(default, skip_serializing_if = "Vec::is_empty")]
21  pub credentials: Vec<Credential>,
22  #[serde(skip)]
23  /// Where this configuration was loaded from.
24  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]
31/// Logging configuration.
32pub struct TraceSettings {
33  /// OTLP endpoint endpoint.
34  #[serde(default, skip_serializing_if = "Option::is_none")]
35  pub otlp: Option<String>,
36  /// Logging level.
37  #[serde(default)]
38  pub level: LogLevel,
39  /// Logging modifier.
40  #[serde(default)]
41  pub modifier: LogModifier,
42  /// Telemetry filter settings.
43  #[serde(default, skip_serializing_if = "Option::is_none")]
44  pub telemetry: Option<LogSettings>,
45  /// STDERR logging settings.
46  #[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)]
54/// Log filter settings
55pub struct LogSettings {
56  /// Log event filter.
57  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)]
64/// Registry credentials.
65pub struct Credential {
66  /// Registry URL.
67  pub scope: String,
68  /// Authentication method.
69  pub auth: Auth,
70}
71
72impl Credential {
73  /// Create a new credential entry.
74  #[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")]
84/// Authentication methods.
85pub enum Auth {
86  /// Basic authentication.
87  Basic(BasicAuth),
88}
89
90#[derive(Deserialize, Serialize)]
91#[non_exhaustive]
92#[serde(deny_unknown_fields)]
93#[serde(rename_all = "snake_case")]
94/// Basic authentication.
95pub struct BasicAuth {
96  /// Username.
97  pub username: String,
98  /// Password.
99  pub password: String,
100}
101
102impl BasicAuth {
103  /// Create a new basic authentication entry.
104  #[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")]
122/// Logging levels.
123pub enum LogLevel {
124  /// Disable loging
125  Off,
126  /// Errors only.
127  Error,
128  /// Warnings and errors only.
129  Warn,
130  /// Info-level logging.
131  Info,
132  /// Debug logging.
133  Debug,
134  /// Trace logging.
135  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")]
147/// Logging modifiers.
148pub enum LogModifier {
149  /// No modifiers.
150  None,
151  /// Additional location and file data.
152  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; // only load the first one, fix when merging is implemented.
213            }
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}