1#![cfg_attr(docsrs, feature(doc_cfg))]
2use owo_colors::*;
57use serde::de::DeserializeOwned;
58use std::fmt::Display;
59use std::path::Path;
60use std::sync::Arc;
61#[cfg(feature = "env")]
62use std::{env::VarError, collections::{BTreeMap, HashMap}};
63#[allow(unused)]
64use std::convert::Infallible;
65
66pub mod error;
67pub mod merge;
68pub mod parse;
69#[doc(hidden)]
70pub mod util;
71
72pub use error::Error;
73#[cfg(feature = "derive")]
109#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
110pub use metre_macros::Config;
111
112use error::{FromPartialError, MergeError};
113
114#[cfg(feature = "env")]
115#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
116use error::FromEnvError;
117
118pub trait Config: Sized {
123 type Partial: PartialConfig;
127
128 fn from_partial(partial: Self::Partial) -> Result<Self, FromPartialError>;
132}
133
134pub trait PartialConfig: DeserializeOwned + Default {
142 fn defaults() -> Self;
146
147 fn merge(&mut self, other: Self) -> Result<(), MergeError>;
149
150 fn list_missing_properties(&self) -> Vec<String>;
152
153 fn is_empty(&self) -> bool;
155
156 #[cfg(feature = "env")]
160 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
161 fn from_env_with_provider_and_optional_prefix<E: EnvProvider>(
162 env: &E,
163 prefix: Option<&str>,
164 ) -> Result<Self, FromEnvError>;
165
166 #[cfg(feature = "env")]
168 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
169 fn from_env_with_provider_and_prefix<E: EnvProvider, P: AsRef<str>>(
170 env: &E,
171 prefix: P,
172 ) -> Result<Self, FromEnvError> {
173 Self::from_env_with_provider_and_optional_prefix(env, Some(prefix.as_ref()))
174 }
175
176 #[cfg(feature = "env")]
178 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
179 fn from_env_with_provider<E: EnvProvider>(env: &E) -> Result<Self, FromEnvError> {
180 Self::from_env_with_provider_and_optional_prefix(env, None)
181 }
182
183 #[cfg(feature = "env")]
185 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
186 fn from_env_with_prefix<P: AsRef<str>>(prefix: P) -> Result<Self, FromEnvError> {
187 Self::from_env_with_provider_and_optional_prefix(&StdEnv, Some(prefix.as_ref()))
188 }
189
190 #[cfg(feature = "env")]
192 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
193 fn from_env() -> Result<Self, FromEnvError> {
194 Self::from_env_with_provider_and_optional_prefix(&StdEnv, None)
195 }
196}
197
198impl<T: Config> Config for Option<T> {
199 type Partial = Option<T::Partial>;
200 fn from_partial(partial: Self::Partial) -> Result<Self, FromPartialError> {
201 match partial {
202 None => Ok(None),
203 Some(inner) => {
204 if inner.is_empty() {
205 Ok(None)
206 } else {
207 let v = T::from_partial(inner)?;
208 Ok(Some(v))
209 }
210 }
211 }
212 }
213}
214
215impl<T: PartialConfig> PartialConfig for Option<T> {
216 fn defaults() -> Self {
217 let inner = T::defaults();
218 if inner.is_empty() {
219 None
220 } else {
221 Some(inner)
222 }
223 }
224
225 fn merge(&mut self, other: Self) -> Result<(), MergeError> {
226 match (self.as_mut(), other) {
227 (None, Some(other)) => *self = Some(other),
228 (Some(me), Some(other)) => me.merge(other)?,
229 (Some(_), None) => {}
230 (None, None) => {}
231 };
232
233 Ok(())
234 }
235
236 fn list_missing_properties(&self) -> Vec<String> {
237 match self {
238 None => vec![],
239 Some(me) => {
240 if !me.is_empty() {
241 me.list_missing_properties()
242 } else {
243 vec![]
244 }
245 }
246 }
247 }
248
249 fn is_empty(&self) -> bool {
250 match self {
251 None => true,
252 Some(me) => me.is_empty(),
253 }
254 }
255
256 #[cfg(feature = "env")]
257 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
258 fn from_env_with_provider_and_optional_prefix<E: EnvProvider>(
259 env: &E,
260 prefix: Option<&str>,
261 ) -> Result<Self, FromEnvError> {
262 let v = T::from_env_with_provider_and_optional_prefix(env, prefix)?;
263 if v.is_empty() {
264 Ok(None)
265 } else {
266 Ok(Some(v))
267 }
268 }
269}
270
271pub trait EnvProvider {
278 type Error: Display;
279 fn get(&self, key: &str) -> Result<Option<String>, Self::Error>;
285}
286
287#[cfg(feature = "env")]
288#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
289macro_rules! impl_env_provider_for_map {
290 ($ty:ty) => {
291 impl EnvProvider for $ty {
292 type Error = Infallible;
293 fn get(&self, key: &str) -> Result<Option<String>, Self::Error> {
294 Ok(self.get(key).map(ToString::to_string))
295 }
296 }
297 };
298}
299
300#[cfg(feature = "env")]
301impl_env_provider_for_map!(HashMap<String, String>);
302#[cfg(feature = "env")]
303impl_env_provider_for_map!(HashMap<&str, String>);
304#[cfg(feature = "env")]
305impl_env_provider_for_map!(HashMap<String, &str>);
306#[cfg(feature = "env")]
307impl_env_provider_for_map!(HashMap<&str, &str>);
308#[cfg(feature = "env")]
309impl_env_provider_for_map!(BTreeMap<String, String>);
310#[cfg(feature = "env")]
311impl_env_provider_for_map!(BTreeMap<&str, String>);
312#[cfg(feature = "env")]
313impl_env_provider_for_map!(BTreeMap<String, &str>);
314#[cfg(feature = "env")]
315impl_env_provider_for_map!(BTreeMap<&str, &str>);
316
317#[derive(Debug, Clone, Copy)]
319#[cfg(feature = "env")]
320#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
321pub struct StdEnv;
322
323#[cfg(feature = "env")]
324#[cfg_attr(docsrs, doc(cfg(feature = "env")))]
325impl EnvProvider for StdEnv {
326 type Error = VarError;
327 fn get(&self, key: &str) -> Result<Option<String>, Self::Error> {
328 match std::env::var(key) {
329 Err(e) => match &e {
330 VarError::NotPresent => Ok(None),
331 VarError::NotUnicode(_) => Err(e),
332 },
333 Ok(v) => Ok(Some(v)),
334 }
335 }
336}
337
338#[derive(Debug, Clone, Eq, PartialEq, Hash)]
342pub enum LoadLocation {
343 Memory,
344 File(String),
345 #[cfg(any(feature = "url-blocking", feature = "url-async"))]
346 #[cfg_attr(docsrs, doc(cfg(any(feature = "url-blocking", feature = "url-async"))))]
347 Url(String),
348}
349
350impl Display for LoadLocation {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 use LoadLocation::*;
353 match self {
354 Memory => write!(f, "{}", "memory".yellow()),
355 File(location) => write!(f, "file: {}", location.yellow()),
356 #[cfg(any(feature = "url-blocking", feature = "url-async"))]
357 Url(location) => write!(f, "url: {}", location.yellow()),
358 }
359 }
360}
361
362#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
364pub enum Format {
365 #[cfg(feature = "json")]
366 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
367 Json,
368 #[cfg(feature = "jsonc")]
369 #[cfg_attr(docsrs, doc(cfg(feature = "jsonc")))]
370 Jsonc,
371 #[cfg(feature = "toml")]
372 #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
373 Toml,
374 #[cfg(feature = "yaml")]
375 #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
376 Yaml,
377}
378
379#[derive(Debug, Clone, Eq, PartialEq, Hash)]
381pub struct ConfigLoader<T: Config> {
382 partial: T::Partial,
383}
384
385impl<T: Config> ConfigLoader<T> {
386 pub fn new() -> Self {
388 Self {
389 partial: T::Partial::default(),
390 }
391 }
392
393 #[allow(clippy::result_large_err)]
395 pub fn file(&mut self, path: &str, format: Format) -> Result<&mut Self, Error> {
396 let code = std::fs::read_to_string(path).map_err(|e| Error::Io {
397 path: path.into(),
398 source: Arc::new(e),
399 })?;
400
401 self.code_with_location(&code, format, LoadLocation::File(path.to_string()))
402 }
403
404 #[allow(clippy::result_large_err)]
406 pub fn file_optional(&mut self, path: &str, format: Format) -> Result<&mut Self, Error> {
407 let exists = Path::new(path).try_exists().map_err(|e| Error::Io {
408 path: path.into(),
409 source: Arc::new(e),
410 })?;
411
412 if exists {
413 self.file(path, format)
414 } else {
415 Ok(self)
416 }
417 }
418
419 #[cfg(feature = "env")]
421 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
422 #[inline(always)]
423 #[allow(clippy::result_large_err)]
424 pub fn env(&mut self) -> Result<&mut Self, Error> {
425 self._env(&StdEnv, None)
426 }
427
428 #[cfg(feature = "env")]
430 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
431 #[inline(always)]
432 #[allow(clippy::result_large_err)]
433 pub fn env_with_prefix(&mut self, prefix: &str) -> Result<&mut Self, Error> {
434 self._env(&StdEnv, Some(prefix))
435 }
436
437 #[cfg(feature = "env")]
443 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
444 #[inline(always)]
445 #[allow(clippy::result_large_err)]
446 pub fn env_with_provider<E: EnvProvider>(&mut self, env: &E) -> Result<&mut Self, Error> {
447 self._env(env, None)
448 }
449
450 #[cfg(feature = "env")]
452 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
453 #[inline(always)]
454 #[allow(clippy::result_large_err)]
455 pub fn env_with_provider_and_prefix<E: EnvProvider>(
456 &mut self,
457 env: &E,
458 prefix: &str,
459 ) -> Result<&mut Self, Error> {
460 self._env(env, Some(prefix))
461 }
462
463 #[inline(always)]
465 #[allow(clippy::result_large_err)]
466 pub fn code<S: AsRef<str>>(&mut self, code: S, format: Format) -> Result<&mut Self, Error> {
467 self._code(code.as_ref(), format, LoadLocation::Memory)
468 }
469
470 #[inline(always)]
474 #[allow(clippy::result_large_err)]
475 pub fn code_with_location<S: AsRef<str>>(
476 &mut self,
477 code: S,
478 format: Format,
479 location: LoadLocation,
480 ) -> Result<&mut Self, Error> {
481 self._code(code.as_ref(), format, location)
482 }
483
484 #[cfg(feature = "url-blocking")]
486 #[cfg_attr(docsrs, doc(cfg(feature = "url-blocking")))]
487 #[allow(clippy::result_large_err)]
488 pub fn url(&mut self, url: &str, format: Format) -> Result<&mut Self, Error> {
489 let map_err = |e| Error::Network {
490 url: url.to_string(),
491 source: Arc::new(e),
492 };
493
494 let code = reqwest::blocking::get(url)
495 .map_err(map_err)?
496 .text()
497 .map_err(map_err)?;
498
499 self._code(&code, format, LoadLocation::Url(url.to_string()))
500 }
501
502 #[cfg(feature = "url-async")]
503 #[cfg_attr(docsrs, doc(cfg(feature = "url-async")))]
504 pub async fn url_async(&mut self, url: &str, format: Format) -> Result<&mut Self, Error> {
506 let map_err = |e| Error::Network {
507 url: url.to_string(),
508 source: Arc::new(e),
509 };
510
511 let code = reqwest::get(url)
512 .await
513 .map_err(map_err)?
514 .text()
515 .await
516 .map_err(map_err)?;
517
518 self._code(&code, format, LoadLocation::Url(url.to_string()))
519 }
520
521 #[cfg(feature = "env")]
522 #[cfg_attr(docsrs, doc(cfg(feature = "env")))]
523 #[inline(always)]
524 #[allow(clippy::result_large_err)]
525 fn _env<E: EnvProvider>(&mut self, env: &E, prefix: Option<&str>) -> Result<&mut Self, Error> {
526 let partial = T::Partial::from_env_with_provider_and_optional_prefix(env, prefix)?;
527 self._add(partial)
528 }
529
530 #[allow(unused)]
531 #[allow(clippy::result_large_err)]
532 fn _code(
533 &mut self,
534 code: &str,
535 format: Format,
536 location: LoadLocation,
537 ) -> Result<&mut Self, Error> {
538 let partial = match format {
539 #[cfg(feature = "json")]
540 #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
541 Format::Json => serde_json::from_str(code).map_err(|e| Error::Json {
542 location,
543 source: Arc::new(e),
544 })?,
545
546 #[cfg(feature = "jsonc")]
547 #[cfg_attr(docsrs, doc(cfg(feature = "jsonc")))]
548 Format::Jsonc => {
549 let reader = json_comments::StripComments::new(code.as_bytes());
550 serde_json::from_reader(reader).map_err(|e| Error::Json {
551 location,
552 source: Arc::new(e),
553 })?
554 }
555
556 #[cfg(feature = "toml")]
557 #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
558 Format::Toml => toml::from_str(code).map_err(|e| Error::Toml {
559 location,
560 source: e,
561 })?,
562
563 #[cfg(feature = "yaml")]
564 #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
565 Format::Yaml => serde_yaml::from_str(code).map_err(|e| Error::Yaml {
566 location,
567 source: Arc::new(e),
568 })?,
569 };
570
571 self._add(partial)
572 }
573
574 #[inline(always)]
576 #[allow(clippy::result_large_err)]
577 pub fn defaults(&mut self) -> Result<&mut Self, Error> {
578 self._add(T::Partial::defaults())
579 }
580
581 #[inline(always)]
583 #[allow(clippy::result_large_err)]
584 pub fn partial(&mut self, partial: T::Partial) -> Result<&mut Self, Error> {
585 self._add(partial)
586 }
587
588 #[inline(always)]
589 #[allow(clippy::result_large_err)]
590 fn _add(&mut self, partial: T::Partial) -> Result<&mut Self, Error> {
591 self.partial.merge(partial)?;
592 Ok(self)
593 }
594
595 #[inline(always)]
597 #[allow(clippy::result_large_err)]
598 pub fn partial_state(&self) -> &T::Partial {
599 &self.partial
600 }
601
602 #[inline(always)]
604 #[allow(clippy::result_large_err)]
605 pub fn partial_state_mut(&mut self) -> &mut T::Partial {
606 &mut self.partial
607 }
608
609 #[inline(always)]
613 #[allow(clippy::result_large_err)]
614 pub fn finish(self) -> Result<T, Error> {
615 let v = T::from_partial(self.partial)?;
616 Ok(v)
617 }
618}
619
620impl<T: Config> Default for ConfigLoader<T> {
621 fn default() -> Self {
622 Self::new()
623 }
624}