mirror of
https://github.com/OMGeeky/confique.git
synced 2025-12-30 08:13:51 +01:00
529 lines
18 KiB
Rust
529 lines
18 KiB
Rust
//! Confique is a type-safe, layered, light-weight, `serde`-based configuration library.
|
|
//!
|
|
//! The core of the library is the [`Config`] trait and [its derive-macro][macro@Config].
|
|
//! You define your configuration value as one or more structs, each of which has
|
|
//! to `#[derive(Config)]`. Then you can use different ways of loading an instancce
|
|
//! of your root configuration struct.
|
|
//!
|
|
//!
|
|
//! # How to use
|
|
//!
|
|
//! ## Defining your configuration with structs
|
|
//!
|
|
//! First, define some structs that describe all your configuration values. Use
|
|
//! the types you want to use in your code. For example, if you have a `port`
|
|
//! config and your code needs that value, it should be of type `u16`,
|
|
//! and *not* `Option<u16>` or `String`. That way, the code using that value is
|
|
//! cleanest.
|
|
//!
|
|
//! Small example:
|
|
//!
|
|
//! ```
|
|
//! use confique::Config;
|
|
//!
|
|
//! #[derive(Config)]
|
|
//! struct Conf {
|
|
//! // A required value. Since it's not `Option<_>`, it has to be specified when
|
|
//! // loading the configuration, or else loading returns an error.
|
|
//! username: String,
|
|
//!
|
|
//! // An optional value.
|
|
//! welcome_message: Option<String>,
|
|
//!
|
|
//! // A required value with default value. If no other value is specified
|
|
//! // (e.g. in a config file), the default value is used.
|
|
//! #[config(default = 8080)]
|
|
//! port: u16,
|
|
//! }
|
|
//! # fn main() {}
|
|
//! ```
|
|
//!
|
|
//! As your application grows, oftentimes you want to split the configuration
|
|
//! into multiple structs. This has the added benefit that your config files
|
|
//! are somewhat structured or have sections. You can do that by including
|
|
//! other types that implement `Config` with `#[config(nested)]`.
|
|
//!
|
|
//! ```
|
|
//! use std::path::PathBuf;
|
|
//! use confique::Config;
|
|
//!
|
|
//! #[derive(Config)]
|
|
//! struct Conf {
|
|
//! username: String,
|
|
//!
|
|
//! #[config(nested)]
|
|
//! log: LogConf,
|
|
//!
|
|
//! #[config(nested)]
|
|
//! db: DbConf,
|
|
//! }
|
|
//!
|
|
//! #[derive(Config)]
|
|
//! struct LogConf {
|
|
//! #[config(default = true)]
|
|
//! stdout: bool,
|
|
//!
|
|
//! file: Option<PathBuf>,
|
|
//! }
|
|
//!
|
|
//! #[derive(Config)]
|
|
//! struct DbConf {
|
|
//! // ...
|
|
//! }
|
|
//! # fn main() {}
|
|
//! ```
|
|
//!
|
|
//! You can also attach some other attributes to fields. For example, with
|
|
//! `#[config(env = "KEY")]`, you can load a value from an environment variable.
|
|
//! For more information, see the [docs for the derive macro][macro@Config].
|
|
//!
|
|
//!
|
|
//! ## Loading the configuration
|
|
//!
|
|
//! Here, you have multiple options. Most of the time, you can probably use the
|
|
//! provided high-level methods of [`Config`], like [`Config::from_file`] and
|
|
//! [`Config::builder`].
|
|
//!
|
|
//! ```
|
|
//! use confique::Config;
|
|
//!
|
|
//! # #[derive(Config)]
|
|
//! # struct Conf {}
|
|
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! // Load from a single file only.
|
|
//! # #[cfg(feature = "toml")]
|
|
//! let config = Conf::from_file("config.toml")?;
|
|
//!
|
|
//! // Or load from multiple sources (higher priority sources are listed first).
|
|
//! # #[cfg(feature = "toml")]
|
|
//! let config = Conf::builder()
|
|
//! .env()
|
|
//! .file("config.toml")
|
|
//! .file("/etc/myapp/config.toml")
|
|
//! .load()?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! But you can also assemble your configuration yourself. That's what
|
|
//! the *partial* types are for (i.e. [`Config::Partial`]). These implement
|
|
//! `serde::Deserialize` and can thus be loaded from a vast number of sources.
|
|
//! One of those sources is the built-in [`File`] which gives you a bit more
|
|
//! control when loading configuration from files. And you can always simply
|
|
//! create an instance of the partial type by writing all values in Rust code
|
|
//! with struct initializer syntax!
|
|
//!
|
|
//! Once you have all your layers (partial types) collected, you have to combine
|
|
//! them via [`Partial::with_fallback`] and convert them to the actual config
|
|
//! type via [`Config::from_partial`]. And you probably also want to use
|
|
//! [`Partial::default_values`] as the last layer.
|
|
//!
|
|
//! ```no_run
|
|
//! # #[cfg(not(feature = "toml"))]
|
|
//! # fn main() {}
|
|
//! # #[cfg(feature = "toml")]
|
|
//! # fn main() -> Result<(), confique::Error> {
|
|
//! use confique::{Config, File, FileFormat, Partial};
|
|
//!
|
|
//! #[derive(Config)]
|
|
//! struct Conf {
|
|
//! foo: f32,
|
|
//! }
|
|
//!
|
|
//! type PartialConf = <Conf as Config>::Partial;
|
|
//! let from_file: PartialConf = File::with_format("/etc/foo/config", FileFormat::Toml)
|
|
//! .required()
|
|
//! .load()?;
|
|
//! let from_file: PartialConf = todo!();
|
|
//! let manual = PartialConf {
|
|
//! // Remember: all fields in the partial types are `Option`s!
|
|
//! foo: Some(3.14),
|
|
//! };
|
|
//! let defaults = PartialConf::default_values();
|
|
//!
|
|
//! let merged = from_file.with_fallback(manual).with_fallback(defaults);
|
|
//! let config = Conf::from_partial(merged)?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! ## Using your configuration
|
|
//!
|
|
//! Well, this is the simple part: the loaded configuration is just an instance
|
|
//! of your struct. And you already know how to access fields of structs!
|
|
//!
|
|
//!
|
|
//! # Cargo features
|
|
//!
|
|
//! This crate has a Cargo feature for each supported file format which are all
|
|
//! enabled by default to get started easily. If you don't need certain
|
|
//! features, remember to disable them to save compile time:
|
|
//!
|
|
//! ```toml
|
|
//! confique = { version = "...", default-features = false, features = ["toml"] }
|
|
//! ```
|
|
//!
|
|
//! All crate features:
|
|
//!
|
|
//! - `toml`: enables TOML support and adds the `toml` dependency.
|
|
//! - `yaml`: enables YAML support and adds the `serde_yaml` dependency.
|
|
//! - `json5`: enables YAML support and adds the `json5` dependency.
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[doc(hidden)]
|
|
pub mod internal;
|
|
|
|
mod builder;
|
|
pub mod env;
|
|
mod error;
|
|
pub mod meta;
|
|
|
|
#[cfg(any(feature = "toml", feature = "yaml", feature = "json5"))]
|
|
mod file;
|
|
|
|
#[cfg(any(feature = "toml", feature = "yaml", feature = "json5"))]
|
|
mod template;
|
|
|
|
#[cfg(feature = "json5")]
|
|
pub mod json5;
|
|
|
|
#[cfg(feature = "toml")]
|
|
pub mod toml;
|
|
|
|
#[cfg(feature = "yaml")]
|
|
pub mod yaml;
|
|
|
|
#[cfg(test)]
|
|
mod test_utils;
|
|
|
|
|
|
pub use serde;
|
|
pub use self::{
|
|
builder::Builder,
|
|
error::Error,
|
|
};
|
|
|
|
#[cfg(any(feature = "toml", feature = "yaml", feature = "json5"))]
|
|
pub use crate::{
|
|
file::{File, FileFormat},
|
|
template::FormatOptions,
|
|
};
|
|
|
|
|
|
/// Derives (automatically implements) [`Config`] for a struct.
|
|
///
|
|
/// This only works for structs with named fields, but not for tuple structs,
|
|
/// unit structs, enums, or unions. This macro only works sometimes inside of
|
|
/// functions (as it generates a module and symbol resolution is weird in that
|
|
/// case); if you get weird errors "symbol not found", just move the struct
|
|
/// definition outside of the function.
|
|
///
|
|
/// # Quick example
|
|
///
|
|
/// ```
|
|
/// use confique::Config;
|
|
/// use std::net::IpAddr;
|
|
///
|
|
/// #[derive(Config)]
|
|
/// struct Conf {
|
|
/// color: Option<String>,
|
|
///
|
|
/// #[config(nested)]
|
|
/// http: HttpConf,
|
|
/// }
|
|
///
|
|
/// #[derive(Config)]
|
|
/// struct HttpConf {
|
|
/// #[config(env = "APP_PORT")]
|
|
/// port: u16,
|
|
///
|
|
/// #[config(default = "127.0.0.1")]
|
|
/// bind: IpAddr,
|
|
///
|
|
/// #[config(default = ["x-user", "x-password"])]
|
|
/// headers: Vec<String>,
|
|
/// }
|
|
/// # fn main() {}
|
|
/// ```
|
|
///
|
|
/// This derives `Config` for the two structs.
|
|
///
|
|
/// - `HttpConf::port` can be loaded from the environment variable `APP_PORT`.
|
|
/// - `HttpConf::bind` has a default value of `127.0.0.1` (the string is turned
|
|
/// into the `IpAddr` via its `Deserialize` impl). Thus a value for this
|
|
/// field does not need to be present when loading configuration.
|
|
/// - `Conf::color` is optional and does not need to be present when loading the
|
|
/// configuration.
|
|
///
|
|
///
|
|
/// # How to use
|
|
///
|
|
/// There are two types of fields distinguished by this macro: nested and leaf
|
|
/// fields.
|
|
///
|
|
/// - **Nested fields**: they have to be annotated with `#[config(nested)]` and
|
|
/// contain a nested configuration object. The type of this field must
|
|
/// implement `Config`. As implied by the previous statement, `Option<_>` as
|
|
/// type for nested fields is not allowed.
|
|
///
|
|
/// - **Leaf fields**: all fields *not* annotated with `#[config(nested)]`,
|
|
/// these contain your actual values. The type of such a field has to
|
|
/// implement `serde::Deserialize` or you have to add a `deserialize_with`
|
|
/// attribute.
|
|
///
|
|
/// Doc comments on the struct and the individual fields are interpreted and
|
|
/// stored in [`Meta`][meta::Meta]. They are used in the formatting functions
|
|
/// (e.g. `toml::format`).
|
|
///
|
|
///
|
|
/// ## Attributes
|
|
///
|
|
/// This macro currently recognizes the following attributes for leaf fields:
|
|
///
|
|
/// - **`#[config(default = ...)]`**: sets a default value for this field. This
|
|
/// is returned by [`Partial::default_values`] and, in most circumstances,
|
|
/// used as a last "layer" to pull values from that have not been set in a
|
|
/// layer of higher-priority. Currently, the following expressions are
|
|
/// allowed:
|
|
///
|
|
/// - Booleans, e.g. `default = true`
|
|
/// - Integers, e.g. `default = 900`
|
|
/// - Floats, e.g. `default = 3.14`
|
|
/// - Strings, e.g. `default = "fox"`
|
|
/// - Arrays, e.g. `default = ["foo", "bar"]`
|
|
/// - Key value maps, e.g. `default = { "cat": 3.14, "bear": 9.0 }`
|
|
///
|
|
/// Map keys can be Booleans, integers, floats, and strings. For array and map
|
|
/// values, you can use any of the expressions in the list above (i.e. you
|
|
/// can nest arrays/maps).
|
|
///
|
|
/// The field value is deserialized from the specified default value
|
|
/// (via `serde::de::IntoDeserializer`). So the expression after `default =`
|
|
/// is often not the same Rust type as your field. For example, you can have
|
|
/// `#[config(default = "/foo/bar")]` on the field `path: PathBuf`. This
|
|
/// works fine as `PathBuf` can be deserialized from a string. (Also see the
|
|
/// `IpAddr` field in the example above.)
|
|
///
|
|
/// If you use an integer or float literal without type suffix, `confique` has
|
|
/// to infer the exact type from the type of the field. This should work in
|
|
/// most cases (`u8`, `f32`, `Vec<i16>`, `[f64; 3]`, ...), but this type
|
|
/// inference is very basic, not even close to what Rust can do. If confique
|
|
/// cannot figure out the type, it defaults to `i32` for integers and `f64`
|
|
/// for floats (like Rust does). If that causes problems for you, just add a
|
|
/// type suffix, e.g. `default = 800u32`.
|
|
///
|
|
/// - **`#[config(env = "KEY")]`**: assigns an environment variable to this
|
|
/// field. In [`Partial::from_env`], the variable is checked and
|
|
/// deserialized into the field if present.
|
|
///
|
|
/// - **`#[config(deserialize_with = path::to::function)]`**: like
|
|
/// [serde's `deserialize_with` attribute][serde-deser].
|
|
///
|
|
/// - **`#[config(parse_env = path::to::function)]`**: function used to parse
|
|
/// environment variables. Mostly useful if you need to parse lists or
|
|
/// other complex objects from env vars. Function needs signature
|
|
/// `fn(&str) -> Result<T, impl std::error::Error>` where `T` is the type of
|
|
/// the field.. Can only be present if the `env` attribute is present. Also
|
|
/// see [`env::parse`].
|
|
///
|
|
/// [serde-deser]: https://serde.rs/field-attrs.html#deserialize_with
|
|
///
|
|
/// ## Special types for leaf fields
|
|
///
|
|
/// These types give a different meaning/semantic to the field. Please note that
|
|
/// due to the limitations of derive macros, the type is checked *literally*.
|
|
/// So it won't work if you rename symbols or used full paths.
|
|
///
|
|
/// - **`Option<T>`**: this marks the field as an optional field. All other
|
|
/// fields are non-optional and will raise an error if while loading the
|
|
/// configuration, no value has been set for them. Optional fields cannot have
|
|
/// a `#[config(default = ...)]` attribute as that would not make sense.
|
|
///
|
|
///
|
|
///
|
|
/// # What the macro generates
|
|
///
|
|
/// This macro emits one `impl confique::Config for … { … }` block. But in order
|
|
/// to implement that trait, a *partial type* of your struct is also generated.
|
|
/// That partial type lives in its own module and derives
|
|
/// `serde::Deserialize`.
|
|
///
|
|
/// The example in the "Quick example" section above would expand to something
|
|
/// like this:
|
|
///
|
|
/// ```ignore
|
|
/// // ----- Generated for `Conf` -----
|
|
/// impl confique::Config for Conf {
|
|
/// type Partial = confique_partial_conf::PartialConf;
|
|
/// ...
|
|
/// }
|
|
/// mod confique_partial_conf {
|
|
/// #[derive(serde::Deserialize)]
|
|
/// pub(super) struct PartialConf {
|
|
/// pub(super) color: Option<String>,
|
|
///
|
|
/// #[serde(default = "confique::Partial::empty")]
|
|
/// pub(super) http: <HttpConf as confique::Config>::Partial,
|
|
/// }
|
|
///
|
|
/// impl confique::Partial for PartialConf { ... }
|
|
/// }
|
|
///
|
|
/// // ----- Generated for `HttpConf` -----
|
|
/// impl confique::Config for HttpConf {
|
|
/// type Partial = confique_partial_http_conf::PartialHttpConf;
|
|
/// ...
|
|
/// }
|
|
/// mod confique_partial_http_conf {
|
|
/// #[derive(serde::Deserialize)]
|
|
/// pub(super) struct PartialHttpConf {
|
|
/// pub(super) port: Option<u16>,
|
|
/// pub(super) bind: Option<IpAddr>,
|
|
/// pub(super) headers: Option<Vec<String>>,
|
|
/// }
|
|
///
|
|
/// impl confique::Partial for PartialHttpConf { ... }
|
|
/// }
|
|
/// ```
|
|
pub use confique_macro::Config;
|
|
|
|
|
|
/// 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_file`][Self::from_file] or [`builder`](Self::builder).
|
|
///
|
|
/// # Deriving
|
|
///
|
|
/// This trait is usually derived as implementing it manually usually entails
|
|
/// writing some repetitive boilerplate code, that goes against the "don't
|
|
/// repeat yourself" principle. See [the documentation of the derive
|
|
/// macro][macro@Config] for more information!
|
|
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;
|
|
|
|
/// A description of this configuration.
|
|
///
|
|
/// This is a runtime representation from the struct definition of your
|
|
/// configuration type.
|
|
const META: meta::Meta;
|
|
|
|
/// 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<Self, Error>;
|
|
|
|
/// Convenience builder to configure, load and merge multiple configuration
|
|
/// sources. **Sources specified earlier have a higher priority**; later
|
|
/// sources only fill in the gaps. After all sources have been loaded, the
|
|
/// default values (usually specified with `#[default = ...]`) are merged
|
|
/// (with the lowest priority).
|
|
///
|
|
/// # Example
|
|
///
|
|
/// In the following example, configuration is first loaded from environment
|
|
/// variables, then from `app.toml`, then from `/etc/app/config.toml` and
|
|
/// finally from the configured default values. Values found earlier in
|
|
/// this list have precedence.
|
|
///
|
|
/// ```
|
|
/// use confique::Config;
|
|
///
|
|
/// #[derive(Config)]
|
|
/// struct Conf {
|
|
/// #[config(env = "APP_PORT", default = 8080)]
|
|
/// port: u16,
|
|
/// }
|
|
///
|
|
/// #[cfg(feature = "toml")]
|
|
/// let conf = Conf::builder()
|
|
/// .env()
|
|
/// .file("app.toml")
|
|
/// .file("/etc/app/config.toml")
|
|
/// .load();
|
|
/// ```
|
|
fn builder() -> Builder<Self> {
|
|
Builder::new()
|
|
}
|
|
|
|
|
|
/// Load the configuration from a single file.
|
|
///
|
|
/// If you rather want to load from multiple sources, use
|
|
/// [`Config::builder`]. Infers the file format from the file extension.
|
|
/// Returns an error in these cases:
|
|
///
|
|
/// - The path does not have a known file extension.
|
|
/// - Loading the file fails.
|
|
/// - The file does not specify all required configuration values.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use confique::Config;
|
|
///
|
|
/// #[derive(Config)]
|
|
/// struct Conf {
|
|
/// port: u16,
|
|
/// }
|
|
///
|
|
/// let conf = Conf::from_file("config.toml");
|
|
/// ```
|
|
#[cfg(any(feature = "toml", feature = "yaml", feature = "json5"))]
|
|
fn from_file(path: impl Into<std::path::PathBuf>) -> Result<Self, Error> {
|
|
let default_values = Self::Partial::default_values();
|
|
let mut file = File::new(path)?;
|
|
if !default_values.is_complete() {
|
|
file = file.required();
|
|
}
|
|
|
|
Self::from_partial(file.load::<Self::Partial>()?.with_fallback(default_values))
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
|
|
/// Loads values from environment variables. This is only relevant for
|
|
/// fields annotated with `#[config(env = "...")]`: all fields not
|
|
/// annotated `env` will be `None`.
|
|
///
|
|
/// If the env variable corresponding to a field is not set, that field is
|
|
/// `None`. If it is set but it failed to deserialize into the target type,
|
|
/// an error is returned.
|
|
fn from_env() -> Result<Self, Error>;
|
|
|
|
/// 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;
|
|
|
|
/// Returns `true` if all values are unspecified/`None`.
|
|
fn is_empty(&self) -> bool;
|
|
|
|
/// Returns `true` if all required (non-optional) values in this
|
|
/// configuration are set. If this returns `true`, `Config::from_partial`
|
|
/// will not return an error.
|
|
fn is_complete(&self) -> bool;
|
|
}
|