Skip to main content

confik/
lib.rs

1#![doc = include_str!("./lib.md")]
2#![deny(rust_2018_idioms, nonstandard_style, future_incompatible)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5use std::{borrow::Cow, error::Error as StdError, ops::Not};
6
7#[doc(hidden)]
8pub use confik_macros::*;
9use serde::de::DeserializeOwned;
10
11#[doc(hidden)]
12pub mod __exports {
13    /// Re-export [`Deserialize`] for use in case `serde` is not otherwise used
14    /// whilst we are.
15    ///
16    /// As serde then calls into other serde functions, we need to re-export the whole of serde,
17    /// instead of just [`Deserialize`].
18    ///
19    /// [`Deserialize`]: serde::Deserialize
20    pub use serde as __serde;
21}
22
23// Enable use of macros inside the crate
24#[allow(unused_extern_crates)] // false positive
25extern crate self as confik;
26
27mod builder;
28#[cfg(feature = "common")]
29pub mod common;
30mod errors;
31pub mod helpers;
32mod path;
33#[cfg(feature = "reloading")]
34mod reloading;
35mod secrets;
36mod sources;
37mod std_impls;
38mod third_party;
39
40use self::path::Path;
41#[cfg(feature = "reloading")]
42pub use self::reloading::{ReloadCallback, ReloadableConfig, ReloadingConfig};
43#[cfg(feature = "env")]
44pub use self::sources::env_source::EnvSource;
45#[cfg(feature = "json")]
46pub use self::sources::json_source::JsonSource;
47#[cfg(feature = "toml")]
48pub use self::sources::toml_source::TomlSource;
49#[cfg(feature = "humantime")]
50pub use self::third_party::humantime;
51pub use self::{
52    builder::ConfigBuilder,
53    errors::Error,
54    secrets::{SecretBuilder, SecretOption, UnexpectedSecret},
55    sources::{file_source::FileSource, offset_source::OffsetSource, Source},
56};
57
58/// Captures the path of a missing value.
59#[derive(Debug, Default, thiserror::Error)]
60#[error("Missing value for path `{0}`")]
61pub struct MissingValue(Path);
62
63impl MissingValue {
64    /// Prepends a path segment as we return back up the call-stack.
65    #[must_use]
66    pub fn prepend(mut self, path_segment: impl Into<Cow<'static, str>>) -> Self {
67        self.0 .0.push(path_segment.into());
68        self
69    }
70}
71
72/// Captures the path and error of a failed conversion.
73#[derive(Debug, thiserror::Error)]
74#[error("Failed try_into for path `{0}`")]
75pub struct FailedTryInto(Path, #[source] Box<dyn StdError + Send + Sync>);
76
77impl FailedTryInto {
78    /// Creates a new [`Self`] with a blank path.
79    pub fn new(err: impl StdError + Send + Sync + 'static) -> Self {
80        Self(Path::new(), Box::new(err))
81    }
82
83    /// Prepends a path segment as we return back up the call-stack.
84    #[must_use]
85    pub fn prepend(mut self, path_segment: impl Into<Cow<'static, str>>) -> Self {
86        self.0 .0.push(path_segment.into());
87        self
88    }
89}
90
91/// Converts the sources, in order, into [`Configuration::Builder`] and
92/// [`ConfigurationBuilder::merge`]s them, passing any errors back.
93fn build_from_sources<'a, Target, Iter>(sources: Iter) -> Result<Target, Error>
94where
95    Target: Configuration,
96    Iter: IntoIterator<Item = Box<dyn Source<Target::Builder> + 'a>>,
97{
98    sources
99        .into_iter()
100        // Convert each source to a `Target::Builder`
101        .map::<Result<Target::Builder, Error>, _>(
102            |source: Box<dyn Source<Target::Builder> + 'a>| {
103                let debug = || format!("{source:?}");
104                let res = source.provide().map_err(|e| Error::Source(e, debug()))?;
105                if source.allows_secrets().not() {
106                    res.contains_non_secret_data()
107                        .map_err(|e| Error::UnexpectedSecret(e, debug()))?;
108                }
109                Ok(res)
110            },
111        )
112        // Merge the builders
113        .reduce(|first, second| Ok(Target::Builder::merge(first?, second?)))
114        // If there was no data then we're missing values
115        .ok_or_else(|| Error::MissingValue(MissingValue::default()))??
116        .try_build()
117}
118
119/// The target to be deserialized from multiple sources.
120///
121/// This will normally be created by the derive macro which also creates a [`ConfigurationBuilder`]
122/// implementation.
123///
124/// For types with no contents, e.g. empty structs, or simple enums, this can be implemented very
125/// easily by specifying only the builder type as `Option<Self>`. For anything more complicated,
126/// complete target and builder implementations will be needed.
127///
128/// # Examples
129///
130/// ```
131/// use confik::Configuration;
132///
133/// #[derive(serde::Deserialize)]
134/// enum MyEnum { A, B, C }
135///
136/// impl Configuration for MyEnum {
137///     type Builder = Option<Self>;
138/// }
139/// ```
140pub trait Configuration: Sized {
141    /// The builder that accumulates the deserializations.
142    type Builder: ConfigurationBuilder<Target = Self>;
143
144    /// Creates an instance of [`ConfigBuilder`] tied to this type.
145    #[must_use]
146    fn builder<'a>() -> ConfigBuilder<'a, Self> {
147        ConfigBuilder::<Self>::default()
148    }
149}
150
151/// A builder for a multi-source config deserialization.
152///
153/// This will almost never be implemented manually, instead being derived.
154///
155/// Builders must implement [`Default`] so that if the structure is nested in another then it being
156/// missing is not an error.
157/// For trivial cases, this is solved by using an `Option<Configuration>`.
158/// See the worked example on [`Configuration`].
159pub trait ConfigurationBuilder: Default + DeserializeOwned {
160    /// The target that will be converted into. See [`Configuration`].
161    type Target;
162
163    /// Combines two builders recursively, preferring `self`'s data, if present.
164    #[must_use]
165    fn merge(self, other: Self) -> Self;
166
167    /// This will probably delegate to `TryInto` but allows it to be implemented for types foreign
168    /// to the library.
169    fn try_build(self) -> Result<Self::Target, Error>;
170
171    /// Called recursively on each field, aiming to hit all [`SecretBuilder`]s. This is only called
172    /// when [`Source::allows_secrets`] is `false`.
173    ///
174    /// If any data is present then `Ok(true)` is returned, unless the data is wrapped in a
175    /// [`SecretBuilder`] in which case [`UnexpectedSecret`] is passed, which will then be built
176    /// into the path to the secret data.
177    fn contains_non_secret_data(&self) -> Result<bool, UnexpectedSecret>;
178}
179
180/// Implementations for trivial types via `Option`.
181///
182/// This can also be used for user types, such as an `enum` with no variants containing fields. See
183/// the worked example on [`Configuration`].
184impl<T> ConfigurationBuilder for Option<T>
185where
186    T: DeserializeOwned + Configuration,
187{
188    type Target = T;
189
190    fn merge(self, other: Self) -> Self {
191        self.or(other)
192    }
193
194    fn try_build(self) -> Result<Self::Target, Error> {
195        self.ok_or_else(|| Error::MissingValue(MissingValue::default()))
196    }
197
198    /// Should not have an `Option` wrapping a secret as `<Option<T> as ConfigurationBuilder` is
199    /// used for terminal types, therefore the `SecretBuilder` wrapping would be external to it.
200    fn contains_non_secret_data(&self) -> Result<bool, UnexpectedSecret> {
201        Ok(self.is_some())
202    }
203}