diff --git a/src/lib.rs b/src/lib.rs index 6684c94..19340aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,11 +16,41 @@ pub mod internal; pub mod source; +/// A configuration object that can be deserialized in layers via `serde`. +/// +/// You would usually derive this trait for your own type and then load the +/// configuration with one of the provided methods, like +/// [`from_sources`][Self::from_sources]. +/// +/// # Deriving +/// +/// This trait is usually derived as implementing it manually usually entails +/// writing some boilerplate code, that goes against the "don't repeat yourself" +/// principle. +/// +/// TODO pub trait Config: Sized { + /// A version of `Self` that represents a potetially partial configuration. + /// + /// This type is supposed to have the exact same fields as this one, but + /// with every field being optional. Its main use is to have a layered + /// configuration from multiple sources where each layer might not contain + /// all required values. The only thing that matters is that combining all + /// layers will result in a configuration object that has all required + /// values defined. type Partial: Partial; + /// Tries to create `Self` from a potentially partial object. + /// + /// If any required values are not defined in `partial`, an [`Error`] is + /// returned. fn from_partial(partial: Self::Partial) -> Result; + /// Tries to load configuration values from all given sources, merging all + /// layers and returning the result. Sources earlier in the given slice have + /// a higher priority. + /// + /// TODO: example fn from_sources(sources: &[&dyn Source]) -> Result { let mut partial = Self::Partial::default_values(); for src in sources.iter().rev() { @@ -32,18 +62,32 @@ pub trait Config: Sized { } } +/// A potentially partial configuration object that can be directly deserialized +/// via `serde`. pub trait Partial: for<'de> Deserialize<'de> { + /// Returns `Self` where all fields/values are `None` or empty. fn empty() -> Self; + + /// Returns an object containing all default values (i.e. set via + /// `#[config(default = ...)]` when deriving `Config`) with all remaining + /// values/fields set to `None`/being empty. fn default_values() -> Self; + + /// Combines two partial configuration objects. `self` has a higher + /// priority; missing values in `self` are filled with values in `fallback`, + /// if they exist. The semantics of this method is basically like in + /// [`Option::or`]. fn with_fallback(self, fallback: Self) -> Self; } -/// A source of configuration values for the configuration `T`, e.g. a file or -/// environment variables. +/// A source of configuration values for the configuration object `T`, e.g. a +/// file or environment variables. pub trait Source { + /// Attempts to load a potentially partially configuration object. fn load(&self) -> Result; } +/// Type describing all errors that can occur in this library. pub struct Error { inner: Box, } diff --git a/src/source.rs b/src/source.rs index 348b8de..db6f6ab 100644 --- a/src/source.rs +++ b/src/source.rs @@ -26,6 +26,12 @@ impl Source for PathBuf { } } +/// A file as source for configuration. +/// +/// Most of the time, you can problably use the [`Source`] impl for +/// `Path`/`PathBuf`, but this type gives you more control. For one, you can +/// explicitly set the file format. You can also mark a file as required, +/// meaning that an error will be returned if the file does not exist. pub struct File { path: PathBuf, format: FileFormat, @@ -102,12 +108,17 @@ impl Source for File { } } +/// All file formats supported by confique. +/// +/// All enum variants are `#[cfg]` guarded with the respective crate feature. pub enum FileFormat { #[cfg(feature = "toml")] Toml, #[cfg(feature = "yaml")] Yaml, } impl FileFormat { + /// Guesses the file format from a file extension, returning `None` if the + /// extension is unknown or if the respective crate feature is not enabled. pub fn from_extension(ext: impl AsRef) -> Option { match ext.as_ref().to_str()? { #[cfg(feature = "toml")]