diff --git a/.idea/micronfig.iml b/.idea/micronfig.iml index cd3603f..cbb3e12 100644 --- a/.idea/micronfig.iml +++ b/.idea/micronfig.iml @@ -3,15 +3,20 @@ - + + + + + + diff --git a/Cargo.toml b/Cargo.toml index a2a3952..a62cb49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "micronfig" -version = "0.1.2" +version = "0.2.0" authors = ["Stefano Pigozzi "] edition = "2021" description = "Tiny crate for simple configuration management" @@ -9,7 +9,23 @@ license = "MIT OR Apache-2.0" keywords = ["12-factor-app", "configuration", "config", "environment", "envvar"] categories = ["config"] -[dependencies] -[dev-dependencies] -tempfile = "3.5.0" +[package.metadata.docs.rs] +all-features = true +cargo-args = ["--bins"] +rustdoc-args = ["--document-private-items"] + + +[features] +default = ["single_envvars", "single_envfiles", "multi", "handle", "macros"] +single_envvars = [] +single_envfiles = [] +multi = ["single_envvars", "single_envfiles"] +handle = ["multi"] +macros = ["lazy_static", "handle"] +testing = ["tempfile"] + + +[dependencies] +lazy_static = { version = "1.4.0", optional = true } +tempfile = { version = "3.5.0", optional = true } diff --git a/README.md b/README.md index 8a25b2b..34cb8b4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Tiny crate for simple configuration management. ```rust -let ip_addr: std::net::IpAddr = micronfig::required("IP_ADDRESS"); +micronfig::required!(IP_ADDRESS, std::net::IpAddr); ``` ## Links diff --git a/examples/e01_the_cave/src/main.rs b/examples/e01_the_cave/src/main.rs index 569d7bc..9276e98 100644 --- a/examples/e01_the_cave/src/main.rs +++ b/examples/e01_the_cave/src/main.rs @@ -2,8 +2,9 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; -fn main() { - let echo: String = micronfig::required("ECHO"); +micronfig::required!(ECHO, String); - println!("ECHOing back: {echo}"); + +fn main() { + println!("{}", *ECHO); } diff --git a/examples/e02_quick_math/src/main.rs b/examples/e02_quick_math/src/main.rs index 8288d8b..3784921 100644 --- a/examples/e02_quick_math/src/main.rs +++ b/examples/e02_quick_math/src/main.rs @@ -2,19 +2,20 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; -fn main() { - let first: u64 = micronfig::required("FIRST"); - let second: u64 = micronfig::required("SECOND"); - let operator: Operator = micronfig::required("OPERATOR"); +micronfig::required!(FIRST, u64); +micronfig::required!(SECOND, u64); +micronfig::required!(OPERATOR, Operator); - let result = match operator { - Operator::Sum => first + second, - Operator::Subtraction => first - second, - Operator::Multiplication => first * second, - Operator::Division => first / second, + +fn main() { + let result = match *OPERATOR { + Operator::Sum => (*FIRST) + (*SECOND), + Operator::Subtraction => (*FIRST) - (*SECOND), + Operator::Multiplication => (*FIRST) * (*SECOND), + Operator::Division => (*FIRST) / (*SECOND), }; - println!("{first} {operator} {second} = {result}") + println!("{} {} {} = {}", *FIRST, *OPERATOR, *SECOND, result) } diff --git a/examples/e03_order_a_pizza/src/main.rs b/examples/e03_order_a_pizza/src/main.rs index f0c70b4..c3a3122 100644 --- a/examples/e03_order_a_pizza/src/main.rs +++ b/examples/e03_order_a_pizza/src/main.rs @@ -2,40 +2,44 @@ use std::fmt::Formatter; use std::net::IpAddr; use std::str::FromStr; + +// The name of the person who ordered the pizza. +micronfig::required!(FULLNAME, String); + +// The (IP) address the pizza should be delivered to. +micronfig::required!(DESTINATION, IpAddr); + +// The base of the pizza to add toppings on. +micronfig::required!(PIZZABASE, PizzaBase); + +// The toppings to add to the pizza. +micronfig::optional!(PIZZATOPPINGS, PizzaToppingsList); +// A pizza with no toppings, to use as fallback. +const PIZZATOPPINGS_NONE: PizzaToppingsList = PizzaToppingsList{ list: vec![] }; + + fn main() { - // The name of the person who ordered the pizza. - let full_name: String = micronfig::required("FULLNAME"); - - // The (IP) address the pizza should be delivered to. - let destination: IpAddr = micronfig::required("DESTINATION"); - - // The base of the pizza to add toppings on. - let pizza_base: PizzaBase = micronfig::required("PIZZABASE"); - - // The toppings to add to the pizza. - let pizza_toppings: PizzaToppingsList = micronfig::optional("PIZZATOPPINGS") - .unwrap_or_else(|| PizzaToppingsList{ list: vec![] }); - // Let's print the order! println!("Pizza Order"); println!("==========="); println!(); println!("Base:"); - println!("- {}", &pizza_base); + println!("- {}", *PIZZABASE); println!(); println!("Toppings:"); - for topping in pizza_toppings.list { + for topping in &(*PIZZATOPPINGS).as_ref().unwrap_or(&PIZZATOPPINGS_NONE).list { println!("- {}", &topping); }; println!(); println!("Deliver to:"); - println!("{} @ {}", &full_name, &destination) + println!("{} @ {}", *FULLNAME, *DESTINATION) } /// A possible base of pizza. +#[derive(Clone, Copy, Debug)] enum PizzaBase { - /// Just the pizza dough, with nothing else on top of it. + /// Just the pizza dough, with nothing else on top f it. Blank, /// Pizza dough with tomato on top. Red, @@ -88,6 +92,7 @@ impl std::fmt::Display for PizzaBase { } /// The toppings +#[derive(Clone, Debug)] struct PizzaToppingsList { pub list: Vec } diff --git a/src/any.rs b/src/any.rs deleted file mode 100644 index 5447191..0000000 --- a/src/any.rs +++ /dev/null @@ -1,267 +0,0 @@ -//! Module defining the [`get`] low-level function, and its associated [`Source`] type. - -use std::ffi::OsString; -use crate::var; -use crate::file; - - -/// Get a value from the first available source and convert it to the given `Type`, additionally returning information about how the value was retrieved. -/// -/// # Process -/// -/// This function tries to get a configuration value: -/// -/// 1. with [`var::get`] using `key`, returning a [`Source::Var`] -/// 2. with [`file::get`] using `key + key_suffix_file`, returning a [`Source::File`] -/// -/// If none of these options successfully resulted in the successful retrieval of the configuration value, [`Source::NotFound`] is returned instead. -/// -/// # Errors -/// -/// All errors are bubbled up, except the ones surfacing because of the total absence of a configuration value, which make the function try the next available source. -/// -/// Currently, those are: -/// - [`var::Error::CannotReadEnvVar`] -/// - [`file::Error::CannotReadEnvVar`] -/// -/// # Examples -/// -/// ``` -/// use micronfig::any::get; -/// use micronfig::any::Source; -/// -/// // The NUMBER envvar has been previously set to "1". -/// # std::env::set_var("NUMBER", "1"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value = get::<&str, &str, u32>("NUMBER", "_FILE"); -/// if let Source::Var(Ok(1)) = value {} else { panic!() } -/// ``` -/// -/// ``` -/// use micronfig::any::get; -/// use micronfig::any::Source; -/// -/// // The NUMBER and NUMBER_FILE envvars have not been set. -/// # std::env::remove_var("NUMBER"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value = get::<&str, &str, u32>("NUMBER", "_FILE"); -/// if let Source::NotFound = value {} else { panic!() } -/// ``` -/// -pub fn get(key: Key, key_suffix_file: KeySuffixFile) -> Source - where Key: AsRef, - KeySuffixFile: AsRef, - Type: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - let v = var::get(&key); - - match v { - Err(var::Error::CannotReadEnvVar(_)) => {}, - _ => return Source::Var(v), - } - - let mut key_file = OsString::new(); - key_file.push(key); - key_file.push(key_suffix_file); - let v = file::get(key_file); - - match v { - Err(file::Error::CannotReadEnvVar(_)) => {}, - _ => return Source::File(v), - } - - Source::NotFound -} - - -/// The way the result returned by [`get`] was obtained. -/// -/// Since more sources might be added in the future, this function is `non_exaustive`. -#[non_exhaustive] -pub enum Source - where Type: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - /// The result was not obtained, since the configuration value was not defined anywhere. - NotFound, - - /// The result was obtained by [`var::get`]. - Var(var::Result), - - /// The result was obtained by [`file::get`]. - File(file::Result), -} - -impl Source - where Type: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - /// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside. - /// - /// # Panics - /// - /// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`]. - /// - /// The panic message is the `msg` given. - /// - /// # See also - /// - /// Similar to [`Result::expect`]. - /// - /// Used by [`Self::unwrap`]. - /// - /// # Examples - /// - /// ``` - /// use micronfig::any::Source; - /// - /// let value = Source::::File(Ok(1)).expect("value to be present"); - /// assert_eq!(value, 1) - /// ``` - /// - /// ```should_panic - /// use micronfig::any::Source; - /// use micronfig::file::Error as FileError; - /// - /// let value = Source::::File(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).expect("value to be present"); - /// // Panic! - /// ``` - pub fn expect(self, msg: &str) -> Type { - match self { - Self::Var(Ok(v)) => v, - Self::File(Ok(v)) => v, - _ => panic!("{}", msg), - } - } - - /// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside. - /// - /// # Panics - /// - /// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`]. - /// - /// # See also - /// - /// Similar to [`Result::unwrap`]. - /// - /// Internally, it uses [`Self::expect`]. - /// - /// # Examples - /// - /// ``` - /// use micronfig::any::Source; - /// - /// let value = Source::::File(Ok(1)).unwrap(); - /// assert_eq!(value, 1) - /// ``` - /// - /// ```should_panic - /// use micronfig::any::Source; - /// use micronfig::file::Error as FileError; - /// - /// let value = Source::::File(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); - /// // Panic! - /// ``` - pub fn unwrap(self) -> Type - { - self.expect("called `Source::unwrap()` on an invalid variant, such as `NotFound` or `_(Err(_))`") - } -} - - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::tests::tempfile_fixture; - - #[test] - fn it_works_var() { - std::env::set_var("NUMBER", "1"); - std::env::remove_var("NUMBER_FILE"); - - match get::<&str, &str, u32>("NUMBER", "_FILE") { - Source::Var(Ok(1u32)) => {}, - _ => panic!("expected Source::Var(Ok(1u32))") - } - } - - #[test] - fn it_works_file() { - let file = tempfile_fixture("1"); - std::env::remove_var("NUMBER"); - std::env::set_var("NUMBER_FILE", file.as_os_str()); - - let n = get::<&str, &str, u32>("NUMBER", "_FILE"); - match n { - Source::File(Ok(1u32)) => {}, - _ => panic!("expected Source::File(Ok(1u32))") - } - } - - #[test] - fn missing_envvar() { - match get::<&str, &str, String>("MISSING_ENVVAR", "_FILE") { - Source::NotFound => {}, - _ => panic!("expected Source::NotFound"), - } - } - - #[test] - fn missing_file() { - std::env::remove_var("NUMBER"); - std::env::set_var("NUMBER_FILE", "/this/file/does/not/exist"); - - match get::<&str, &str, u32>("NUMBER", "_FILE") { - Source::File(Err(file::Error::CannotOpenFile(_))) => {}, - _ => panic!("expected Source::File(Err(file::Error::CannotOpenFile(_)))"), - } - } - - #[test] - fn not_a_number_var() { - std::env::set_var("NUMBER", "XYZ"); - std::env::remove_var("NUMBER_FILE"); - - match get::<&str, &str, u32>("NUMBER", "_FILE") { - Source::Var(Err(var::Error::CannotConvertValue(_))) => {}, - _ => panic!("expected Source::Var(Err(var::Error::CannotConvertValue(_)))"), - } - } - - #[test] - fn not_a_number_file() { - let file = tempfile_fixture("XYZ"); - std::env::set_var("NUMBER_FILE", file.as_os_str()); - std::env::remove_var("NUMBER"); - - match get::<&str, &str, u32>("NUMBER", "_FILE") { - Source::File(Err(file::Error::CannotConvertValue(_))) => {}, - _ => panic!("expected Source::File(Err(file::Error::CannotConvertValue(_)))"), - } - } - - #[test] - fn unwrap_var_ok() { - Source::Var(Ok("ok".to_string())).unwrap(); - } - - #[test] - fn unwrap_file_ok() { - Source::File(Ok("ok".to_string())).unwrap(); - } - - #[test] - #[should_panic] - fn unwrap_var_err() { - Source::::Var(Err(var::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); - } - - #[test] - #[should_panic] - fn unwrap_file_err() { - Source::::File(Err(file::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); - } -} \ No newline at end of file diff --git a/src/handle/mod.rs b/src/handle/mod.rs new file mode 100644 index 0000000..db82c2a --- /dev/null +++ b/src/handle/mod.rs @@ -0,0 +1,153 @@ +//! High-level API — Handle errors automatically. +//! +//! It can be useful if you want to specify when configuration values are loaded in the lifecycle of your binary. + + +/// Get a value from the first available source, panicking with a human-readable message in case the value is missing or cannot be processed. +/// +/// # Process +/// +/// This function: +/// +/// 1. calls [`crate::multi::get`] with the given key and a file suffix of `_FILE` +/// 2. pattern matches errors and [`panic`]s if an error is caught. +/// +/// # Panics +/// +/// Any error encountered by this function causes a panic with a message describing what went wrong. +/// +/// The same thing happens if the configuration value could not be retrieved by any source. +/// +/// # Examples +/// +/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]: +/// ``` +/// use micronfig::handle::get_required; +/// # +/// # std::env::set_var("USER", "steffo"); +/// # std::env::remove_var("USER_FILE"); +/// +/// let user: String = get_required("USER"); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// use micronfig::handle::get_required; +/// # +/// # std::env::set_var("IP_ADDRESS", "192.168.1.1"); +/// # std::env::remove_var("IP_ADDRESS_FILE"); +/// +/// let ip_addr: IpAddr = get_required("IP_ADDRESS"); +/// ``` +/// +/// # See also +/// +/// [`get_optional`], which has the same behaviour but does not panic if the value is not found, instead returning [`None`]. +/// +/// # Possible future improvements +/// +/// Possibly refactor this to a method of [`crate::multi::Source`]. +/// +pub fn get_required(key: &str) -> Type + where Type: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + use crate::multi::{get, Source}; + use crate::single::{envvars, envfiles}; + + match get(key, "_FILE") { + Source::EnvVar(Ok(v)) => v, + Source::EnvVar(Err(envvars::Error::CannotConvertValue(err))) => + panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &key, &std::any::type_name::(), &err), + Source::EnvVar(Err(envvars::Error::CannotReadEnvVar(_))) => + panic!("Something unexpected happened in micronfig. Please report this as a bug!"), + + Source::EnvFile(Ok(v)) => v, + Source::EnvFile(Err(envfiles::Error::CannotConvertValue(err))) => + panic!("The contents of the file at {} could not be converted to a {}: {:?}", &key, &std::any::type_name::(), &err), + Source::EnvFile(Err(envfiles::Error::CannotOpenFile(err))) => + panic!("The file at {} could not be opened: {}", &key, &err), + Source::EnvFile(Err(envfiles::Error::CannotReadFile(err))) => + panic!("The contents of the file at {} could not be read: {}", &key, &err), + Source::EnvFile(Err(envfiles::Error::CannotReadEnvVar(_))) => + panic!("Something unexpected happened in micronfig. Please report this as a bug!"), + + Source::NotFound => + panic!("The configuration value {} is not defined.", &key), + } +} + + +/// Try to get a value from the first available source, panicking with a human-readable message in case it cannot be processed. +/// +/// # Process +/// +/// This function: +/// +/// 1. calls [`crate::multi::get`] with the given key and a file suffix of `_FILE` +/// 2. pattern matches errors and [`panic`]s if an error is caught. +/// +/// # Panics +/// +/// Any error encountered by this function causes a panic with a message describing what went wrong. +/// +/// # Examples +/// +/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]: +/// ``` +/// use micronfig::handle::get_optional; +/// # +/// # std::env::set_var("USER", "steffo"); +/// # std::env::remove_var("USER_FILE"); +/// +/// let user: Option = get_optional("USER"); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// use micronfig::handle::get_optional; +/// # +/// # std::env::set_var("IP_ADDRESS", "192.168.1.1"); +/// # std::env::remove_var("IP_ADDRESS_FILE"); +/// +/// let ip_addr: Option = get_optional("IP_ADDRESS"); +/// ``` +/// +/// # See also +/// +/// [`get_required`], which has the same behaviour but does panics if the value is not found. +/// +/// # Possible future improvements +/// +/// Possibly refactor this to a method of [`crate::multi::Source`]. +/// +pub fn get_optional(name: &str) -> Option + where Type: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + use crate::multi::{get, Source}; + use crate::single::{envvars, envfiles}; + + match get(name, "_FILE") { + Source::EnvVar(Ok(v)) => Some(v), + Source::EnvVar(Err(envvars::Error::CannotConvertValue(err))) => + panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &name, &std::any::type_name::(), &err), + Source::EnvVar(Err(envvars::Error::CannotReadEnvVar(_))) => + panic!("Something unexpected happened in micronfig. Please report this as a bug!"), + + Source::EnvFile(Ok(v)) => Some(v), + Source::EnvFile(Err(envfiles::Error::CannotConvertValue(err))) => + panic!("The contents of the file at {} could not be converted to a {}: {:?}", &name, &std::any::type_name::(), &err), + Source::EnvFile(Err(envfiles::Error::CannotOpenFile(err))) => + panic!("The file at {} could not be opened: {}", &name, &err), + Source::EnvFile(Err(envfiles::Error::CannotReadFile(err))) => + panic!("The contents of the file at {} could not be read: {}", &name, &err), + Source::EnvFile(Err(envfiles::Error::CannotReadEnvVar(_))) => + panic!("Something unexpected happened in micronfig. Please report this as a bug!"), + + Source::NotFound => None, + + } +} diff --git a/src/lib.rs b/src/lib.rs index b8f603e..8388831 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ //! Tiny crate for simple configuration management. //! +//! > **Unstable**; I haven't fully committed to the API yet, so it might change wildly in the following minor versions (`0.x.0`). +//! //! # Features //! //! This crate handles: @@ -10,231 +12,40 @@ //! //! # Usage //! -//! Each configurable property of the dependent binary must have an arbitrary *key*, a name used to define its value, usually in `SCREAMING_SNAKE_CASE`. +//! This crate has four levels of abstraction, each one with a different usage method. //! -//! For example, some keys may be: +//! In order from the highest to the lowest, they are: //! -//! - `TELEGRAM_API_KEY` -//! - `OAUTH2_CLIENT_SECRET` -//! - `SCREEN_RESOLUTION` +//! 1. **Recommended**: [`required`] and [`optional`], macros which allow you to define global, lazily-evaluated, configuration values; +//! 2. [`handle::get_required`] and [`handle::get_optional`], functions which allow you to get a configuration value in a specific moment, without having to consider handling errors; +//! 3. [`multi::get`], function which behaves in the same way as the previous two, but returns [`multi::Source`] instead, allowing you to handle errors how you prefer; +//! 4. [`single`], module containing submodules allowing the retrieval of configuration values from a single source, returning a source-specific [`Result`]. //! -//! ## High-level API +//! ## Examples //! -//! The recommended usage of this crate is via the high-level API, which comprises the [`required`] and [`optional`] functions. -//! -//! They automatically try to retrieve a value from the following sources, in this order, returning as soon as one is found: -//! -//! 1. the contents of the environment variable `{key}`; -//! 2. the contents of the file located at path specified in the environment variable `{key}_FILE`. -//! -//! If no value is found, or if an error occurred while trying to retrieve it, the function panics with a human-readable error message. -//! -//! Additionally, they try to parse the value into the requested Rust type using its [`std::str::FromStr`] trait. -//! -//! If the conversion fails, the function panics, again providing a human-readable error message. -//! -//! ### Examples -//! -//! To require a `IP_ADDRESS` property to be configured, and to parse it as an [`std::net::IpAddr`], you may write the following code: -//! -//! ``` -//! use std::net::IpAddr; -//! -//! # std::env::set_var("IP_ADDRESS", "192.168.1.1"); -//! let ip_addr: IpAddr = micronfig::required("IP_ADDRESS"); -//! ``` -//! -//! To allow the user to not specify it, and provide a default, you may write: -//! -//! ``` -//! use std::net::{IpAddr, Ipv4Addr}; -//! -//! # std::env::remove_var("IP_ADDRESS"); -//! let ip_addr: IpAddr = micronfig::optional("IP_ADDRESS").unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); -//! ``` -//! -//! ## Middle-level API -//! -//! If you want more control on how errors are handled or on how the key is manipulated to access values, you can use the middle-level API, comprised of the [`any::get`] function and the [`any::Source`] enum. -//! -//! [`any::get`] works similarly to the [`required`] and [`optional`] functions, but returns a [`any::Source`] enum variant instead, which denotes the source a result was obtained from, and contains the raw [`Result`] of the operation. -//! -//! ### Example -//! -//! To customize the handling of the same `IP_ADDRESS` as earlier, so that something is printed instead of the binary panicking, you may write the following code: -//! -//! ``` -//! use std::net::IpAddr; -//! use micronfig::any::{get, Source}; -//! -//! let ip_addr: Source = get("IP_ADDRESS", "_FILE"); -//! -//! match ip_addr { -//! Source::Var(Ok(addr)) | Source::File(Ok(addr)) => println!("Success! · {}", &addr), -//! _ => println!("Failure..."), -//! } -//! ``` -//! -//! ## Low-level API -//! -//! Finally, if you want to override the accessed sources, you may use the low level API directly, comprised of the following modules: -//! -//! - [`micronfig::var`] for accessing environment variable -//! - [`micronfig::file`] for accessing files with the path defined in environment variables -//! -//! ### Example -//! -//! To retrieve the `IP_ADDRESS` only from the environment variable, and ignoring other sources: -//! -//! ``` -//! use std::net::IpAddr; -//! use micronfig::var::get; -//! -//! # std::env::set_var("IP_ADDRESS", "192.168.1.1"); -//! let ip_addr: IpAddr = get("IP_ADDRESS").expect("IP_ADDRESS envvar to be defined"); -//! ``` -//! -//! # More examples -//! -//! Other examples are provided in the crate source, [inside the `examples/` directory](https://github.com/Steffo99/micronfig/tree/main/examples). +//! Some examples are provided in the crate source, [inside the `examples/` directory](https://github.com/Steffo99/micronfig/tree/main/examples). -pub mod any; -pub mod var; -pub mod file; - -/// Get the configuration value with the given `key` and convert it to the given `Type`. -/// -/// # Panics -/// -/// Any error encountered by this function causes a panic with a message describing what went wrong. -/// -/// The same thing happens if the configuration value could not be retrieved by any source. -/// -/// # Examples -/// -/// ``` -/// // The NUMBER envvar has been previously set to "1". -/// # std::env::set_var("NUMBER", "1"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value: u8 = micronfig::required("NUMBER"); -/// assert_eq!(value, 1u8); -/// ``` -/// -/// ```should_panic -/// // The NUMBER envvar has not been set. -/// # std::env::remove_var("NUMBER"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value: u8 = micronfig::required("NUMBER"); -/// // Panic: The configuration value NUMBER is not defined. -/// ``` -/// -/// # See also -/// -/// [`any::get`], the function called by this one to get the configuration value. -/// -pub fn required(key: &str) -> Type - where Type: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - use crate::any::{get, Source}; - - match get(key, "_FILE") { - Source::Var(Ok(v)) => v, - Source::Var(Err(var::Error::CannotConvertValue(err))) => - panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &key, &std::any::type_name::(), &err), - Source::Var(Err(var::Error::CannotReadEnvVar(_))) => - panic!("Something unexpected happened in micronfig. Please report this as a bug!"), - - Source::File(Ok(v)) => v, - Source::File(Err(file::Error::CannotConvertValue(err))) => - panic!("The contents of the file at {} could not be converted to a {}: {:?}", &key, &std::any::type_name::(), &err), - Source::File(Err(file::Error::CannotOpenFile(err))) => - panic!("The file at {} could not be opened: {}", &key, &err), - Source::File(Err(file::Error::CannotReadFile(err))) => - panic!("The contents of the file at {} could not be read: {}", &key, &err), - Source::File(Err(file::Error::CannotReadEnvVar(_))) => - panic!("Something unexpected happened in micronfig. Please report this as a bug!"), - - Source::NotFound => - panic!("The configuration value {} is not defined.", &key), - } -} +#![warn(missing_docs)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/Steffo99/micronfig/main/icon.png")] -/// Get the configuration value with the given `name` and convert it to the given `Type`, if it was defined somewhere. -/// -/// # Panics -/// -/// Any error encountered by this function causes a panic with a message describing what went wrong. -/// -/// # Examples -/// -/// ``` -/// // The NUMBER envvar has been previously set to "1". -/// # std::env::set_var("NUMBER", "1"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value: Option = micronfig::optional("NUMBER"); -/// assert_eq!(value, Some(1u8)); -/// ``` -/// -/// ``` -/// // The NUMBER envvar has not been set. -/// # std::env::remove_var("NUMBER"); -/// # std::env::remove_var("NUMBER_FILE"); -/// -/// let value: Option = micronfig::optional("NUMBER"); -/// assert_eq!(value, None); -/// ``` -/// -/// # See also -/// -/// [`any::get`], the function called by this one to get the configuration value. -/// -pub fn optional(name: &str) -> Option - where Type: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - use crate::any::{get, Source}; +pub mod single; - match get(name, "_FILE") { - Source::Var(Ok(v)) => Some(v), - Source::Var(Err(var::Error::CannotConvertValue(err))) => - panic!("The contents of the {} environment variable could not be converted to a {}: {:?}", &name, &std::any::type_name::(), &err), - Source::Var(Err(var::Error::CannotReadEnvVar(_))) => - panic!("Something unexpected happened in micronfig. Please report this as a bug!"), +#[cfg(feature = "multi")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "multi")))] +pub mod multi; - Source::File(Ok(v)) => Some(v), - Source::File(Err(file::Error::CannotConvertValue(err))) => - panic!("The contents of the file at {} could not be converted to a {}: {:?}", &name, &std::any::type_name::(), &err), - Source::File(Err(file::Error::CannotOpenFile(err))) => - panic!("The file at {} could not be opened: {}", &name, &err), - Source::File(Err(file::Error::CannotReadFile(err))) => - panic!("The contents of the file at {} could not be read: {}", &name, &err), - Source::File(Err(file::Error::CannotReadEnvVar(_))) => - panic!("Something unexpected happened in micronfig. Please report this as a bug!"), +#[cfg(feature = "handle")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "handle")))] +pub mod handle; - Source::NotFound => None, +#[cfg(feature = "macros")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "macros")))] +pub use lazy_static; +#[cfg(feature = "macros")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "macros")))] +pub mod macros; - } -} - - -#[cfg(test)] -pub(crate) mod tests { - /// Create a temporary file and write `content` inside it. - /// - /// The file will be deleted as soon as the [`tempfile::TempPath`] is dropped. - pub(crate) fn tempfile_fixture(content: &str) -> tempfile::TempPath { - use std::io::Write; - - let mut file = tempfile::NamedTempFile::new() - .expect("the tempfile fixture to be created successfully"); - write!(file, "{}", content) - .expect("to be able to write into the tempfile fixture"); - - file.into_temp_path() - } -} \ No newline at end of file +#[cfg(feature = "testing")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "testing")))] +pub mod testing; \ No newline at end of file diff --git a/src/macros/mod.rs b/src/macros/mod.rs new file mode 100644 index 0000000..759b50e --- /dev/null +++ b/src/macros/mod.rs @@ -0,0 +1,82 @@ +//! Highest-level API — Define lazy statics. +//! +//! The recommended way to use the library. + + +/// Define a required configuration value with a certain type. +/// +/// # Process +/// +/// This macro: +/// +/// 1. uses [`lazy_static::lazy_static`] to define a new static variable (and associated struct) +/// 2. uses [`crate::handle::get_required`] to get the configuration value and handle eventual errors +/// +/// # Examples +/// +/// Define a configuration value with the `USER` key, a [`String`]: +/// ``` +/// # std::env::set_var("USER", "steffo"); +/// # std::env::remove_var("USER_FILE"); +/// +/// micronfig::required!(USER, String); +/// println!("{:?}", *USER); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// # +/// # std::env::set_var("IP_ADDRESS", "192.168.1.1"); +/// # std::env::remove_var("IP_ADDRESS_FILE"); +/// +/// micronfig::required!(IP_ADDRESS, IpAddr); +/// println!("{:?}", *IP_ADDRESS); +/// ``` +#[macro_export] +macro_rules! required { + ($identifier:ident, $kind:ty) => { + $crate::lazy_static::lazy_static! { + pub(crate) static ref $identifier: $kind = $crate::handle::get_required::<$kind>(stringify!($identifier)); + } + }; +} + +/// Define a optional configuration value with a certain type. +/// +/// # Process +/// +/// This macro: +/// +/// 1. uses [`lazy_static::lazy_static`] to define a new static variable (and associated struct) +/// 2. uses [`crate::handle::get_optional`] to get the configuration value and handle eventual errors +/// +/// # Examples +/// +/// Define a configuration value with the `USER` key, a [`String`]: +/// ``` +/// # std::env::set_var("USER", "steffo"); +/// # std::env::remove_var("USER_FILE"); +/// +/// micronfig::optional!(USER, String); +/// println!("{:?}", *USER); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// # +/// # std::env::set_var("IP_ADDRESS", "192.168.1.1"); +/// # std::env::remove_var("IP_ADDRESS_FILE"); +/// +/// micronfig::required!(IP_ADDRESS, IpAddr); +/// println!("{:?}", *IP_ADDRESS); +/// ``` +#[macro_export] +macro_rules! optional { + ($identifier:ident, $kind:ty) => { + $crate::lazy_static::lazy_static! { + pub(crate) static ref $identifier: Option<$kind> = $crate::handle::get_optional::<$kind>(stringify!($identifier)); + } + } +} diff --git a/src/multi/mod.rs b/src/multi/mod.rs new file mode 100644 index 0000000..a7a0aca --- /dev/null +++ b/src/multi/mod.rs @@ -0,0 +1,293 @@ +//! Middle-level API — Use all available configuration sources. +//! +//! It can be useful if you want more control on how errors are handled or on how the key is passed to the [`crate::single`] sources. + + +use std::ffi::OsString; +#[cfg(feature = "single_envvars")] use crate::single::envvars; +#[cfg(feature = "single_envfiles")] use crate::single::envfiles; + + +/// Get a value from the first available source, additionally returning information about how the value was retrieved. +/// +/// # Process +/// +/// This function tries to `get` a configuration value: +/// +/// 1. with [`envvars::get`], using `key`, returning a [`Source::EnvVar`] +/// 2. with [`envfiles::get`], using `key + key_suffix_file`, returning a [`Source::EnvFile`] +/// +/// If none of these options successfully resulted in the successful retrieval of the configuration value, [`Source::NotFound`] is returned instead. +/// +/// # Errors +/// +/// All errors encountered are bubbled up, except the ones surfacing because of the total absence of a configuration value, which make the function immediately try the next available source. +/// +/// Currently, those errors are: +/// - [`envvars::Error::CannotReadEnvVar`] +/// - [`envfiles::Error::CannotReadEnvVar`] +/// +/// # Examples +/// +/// Retrieve a configuration value from either the `USER` environment variable or the `USER_FILE` file, maintaining it as a [`String`]: +/// ``` +/// use micronfig::multi::get; +/// use micronfig::multi::Source; +/// # +/// # std::env::set_var("USER", "steffo"); +/// # std::env::remove_var("USER_FILE"); +/// +/// let user: Source = get("USER", "_FILE"); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS` environment variable or the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// use micronfig::multi::get; +/// use micronfig::multi::Source; +/// # +/// # std::env::set_var("IP_ADDRESS", "192.168.1.1"); +/// # std::env::remove_var("IP_ADDRESS_FILE"); +/// +/// let ip_addr: Source = get("IP_ADDRESS", "_FILE"); +/// ``` +/// +pub fn get(key: Key, key_suffix_file: KeySuffixFile) -> Source + where Key: AsRef, + KeySuffixFile: AsRef, + Type: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + + if cfg!(feature = "single_envvars") { + let v = envvars::get(&key); + + match v { + Err(envvars::Error::CannotReadEnvVar(_)) => {}, + _ => return Source::EnvVar(v), + } + } + + if cfg!(feature = "single_envfiles") { + let mut key_file = OsString::new(); + key_file.push(key); + key_file.push(key_suffix_file); + let v = envfiles::get(key_file); + + match v { + Err(envfiles::Error::CannotReadEnvVar(_)) => {}, + _ => return Source::EnvFile(v), + } + } + + Source::NotFound +} + + +/// The way the result returned by [`get`] was obtained. +/// +/// Since more sources might be added in the future, this function is `non_exaustive`. +#[non_exhaustive] +#[derive(Debug)] +pub enum Source + where Type: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + /// The result was not obtained, since the configuration value was not defined anywhere. + NotFound, + + /// The result was obtained by [`envvars::get`]. + #[cfg(feature = "single_envvars")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "single_envvars")))] + EnvVar(envvars::Result), + + /// The result was obtained by [`envfiles::get`]. + #[cfg(feature = "single_envfiles")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "single_envfiles")))] + EnvFile(envfiles::Result), +} + +impl Source + where Type: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + /// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside. + /// + /// # Panics + /// + /// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`]. + /// + /// The panic message is the `msg` given. + /// + /// # See also + /// + /// Similar to [`Result::expect`]. + /// + /// Used by [`Self::unwrap`]. + /// + /// # Examples + /// + /// ``` + /// use micronfig::multi::Source; + /// + /// let value = Source::::EnvFile(Ok(1)).expect("value to be present"); + /// assert_eq!(value, 1) + /// ``` + /// + /// ```should_panic + /// use micronfig::multi::Source; + /// use micronfig::single::envfiles::Error as FileError; + /// + /// let value = Source::::EnvFile(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).expect("value to be present"); + /// // Panic! + /// ``` + pub fn expect(self, msg: &str) -> Type { + match self { + #[cfg(feature = "single_envvars")] + Self::EnvVar(Ok(v)) => v, + + #[cfg(feature = "single_envfiles")] + Self::EnvFile(Ok(v)) => v, + + _ => panic!("{}", msg), + } + } + + /// Returns any contained [`Ok`] value, consuming both `self` and the [`Source`] inside. + /// + /// # Panics + /// + /// This function panics if `self` is a [`Source::NotFound`], or if the contained value is a [`Err`]. + /// + /// # See also + /// + /// Similar to [`Result::unwrap`]. + /// + /// Internally, it uses [`Self::expect`]. + /// + /// # Examples + /// + /// ``` + /// use micronfig::multi::Source; + /// + /// let value = Source::::EnvFile(Ok(1)).unwrap(); + /// assert_eq!(value, 1) + /// ``` + /// + /// ```should_panic + /// use micronfig::multi::Source; + /// use micronfig::single::envfiles::Error as FileError; + /// + /// let value = Source::::EnvFile(Err(FileError::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); + /// // Panic! + /// ``` + pub fn unwrap(self) -> Type + { + self.expect("called `Source::unwrap()` on an invalid variant, such as `NotFound` or `_(Err(_))`") + } +} + + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::testing::tempfile_fixture; + + #[test] + #[cfg(feature = "single_envvars")] + fn it_works_var() { + std::env::set_var("NUMBER", "1"); + std::env::remove_var("NUMBER_FILE"); + + match get::<&str, &str, u32>("NUMBER", "_FILE") { + Source::EnvVar(Ok(1u32)) => {}, + _ => panic!("expected Source::EnvVar(Ok(1u32))") + } + } + + #[test] + #[cfg(feature = "single_envfiles")] + fn it_works_file() { + let file = tempfile_fixture("1"); + std::env::remove_var("NUMBER"); + std::env::set_var("NUMBER_FILE", file.as_os_str()); + + let n = get::<&str, &str, u32>("NUMBER", "_FILE"); + match n { + Source::EnvFile(Ok(1u32)) => {}, + _ => panic!("expected Source::EnvFile(Ok(1u32))") + } + } + + #[test] + #[cfg(feature = "single_envvars")] + fn missing_envvar() { + match get::<&str, &str, String>("MISSING_ENVVAR", "_FILE") { + Source::NotFound => {}, + _ => panic!("expected Source::NotFound"), + } + } + + #[test] + #[cfg(feature = "single_envfiles")] + fn missing_file() { + std::env::remove_var("NUMBER"); + std::env::set_var("NUMBER_FILE", "/this/file/does/not/exist"); + + match get::<&str, &str, u32>("NUMBER", "_FILE") { + Source::EnvFile(Err(envfiles::Error::CannotOpenFile(_))) => {}, + _ => panic!("expected Source::EnvFile(Err(envfiles::Error::CannotOpenFile(_)))"), + } + } + + #[test] + #[cfg(feature = "single_envvars")] + fn not_a_number_var() { + std::env::set_var("NUMBER", "XYZ"); + std::env::remove_var("NUMBER_FILE"); + + match get::<&str, &str, u32>("NUMBER", "_FILE") { + Source::EnvVar(Err(envvars::Error::CannotConvertValue(_))) => {}, + _ => panic!("expected Source::EnvVar(Err(envvars::Error::CannotConvertValue(_)))"), + } + } + + #[test] + #[cfg(feature = "single_envfiles")] + fn not_a_number_file() { + let file = tempfile_fixture("XYZ"); + std::env::set_var("NUMBER_FILE", file.as_os_str()); + std::env::remove_var("NUMBER"); + + match get::<&str, &str, u32>("NUMBER", "_FILE") { + Source::EnvFile(Err(envfiles::Error::CannotConvertValue(_))) => {}, + _ => panic!("expected Source::EnvFile(Err(envfiles::Error::CannotConvertValue(_)))"), + } + } + + #[test] + #[cfg(feature = "single_envvars")] + fn unwrap_var_ok() { + Source::EnvVar(Ok("ok".to_string())).unwrap(); + } + + #[test] + #[cfg(feature = "single_envfiles")] + fn unwrap_file_ok() { + Source::EnvFile(Ok("ok".to_string())).unwrap(); + } + + #[test] + #[should_panic] + #[cfg(feature = "single_envvars")] + fn unwrap_var_err() { + Source::::EnvVar(Err(envvars::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); + } + + #[test] + #[should_panic] + #[cfg(feature = "single_envfiles")] + fn unwrap_file_err() { + Source::::EnvFile(Err(envfiles::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap(); + } +} \ No newline at end of file diff --git a/src/file.rs b/src/single/envfiles.rs similarity index 65% rename from src/file.rs rename to src/single/envfiles.rs index 5c819f7..279e897 100644 --- a/src/file.rs +++ b/src/single/envfiles.rs @@ -1,7 +1,39 @@ -//! Module defining the [`get`] low-level function for environment files, and its [`Error`] and [`Result`] associated types. +//! Contents of files at paths defined by environment variables. -/// Get a configuration value from the file at the path contained in the environment variable with the given `key`, and convert it to the desired `Type`. +/// Get a configuration value from the source. +/// +/// # Process +/// +/// This function: +/// +/// 1. tries to access the environment variable with the given name using [`std::env::var`] +/// 2. tries to interpret the contents of the environment variable as a [`std::path::PathBuf`] +/// 3. tries to [`std::fs::File::open`] the file at that path +/// 4. tries to [`std::io::Read::read_to_string`] the contents of the opened file +/// 5. tries to convert the obtained value to another of the given type using [`std::str::FromStr::from_str`] +/// +/// # Examples +/// +/// Retrieve a configuration value from the `USER_FILE` file, maintaining it as a [`String`]: +/// ``` +/// use micronfig::single::envfiles::get; +/// +/// # let filename = micronfig::testing::tempfile_fixture("steffo"); +/// # std::env::set_var("USER_FILE", filename.as_os_str()); +/// let user: String = get("USER_FILE").expect("USER_FILE envvar to be defined"); +/// ``` +/// +/// Retrieve a configuration value from the `IP_ADDRESS_FILE` file, then try to convert it to a [`std::net::IpAddr`]: +/// ``` +/// use std::net::IpAddr; +/// use micronfig::single::envfiles::get; +/// +/// # let filename = micronfig::testing::tempfile_fixture("192.168.1.1"); +/// # std::env::set_var("IP_ADDRESS_FILE", filename.as_os_str()); +/// let ip_addr: IpAddr = get("IP_ADDRESS_FILE").expect("IP_ADDRESS_FILE envvar to be defined"); +/// ``` +/// pub fn get(key: Key) -> Result where Key: AsRef, Type: std::str::FromStr, @@ -49,7 +81,7 @@ pub enum Error /// The value could not be converted to the desired type. /// - /// Encountered when the call to [`FromStr::from_str`] fails. + /// Encountered when the call to [`std::str::FromStr::from_str`] fails. CannotConvertValue(ConversionError), } @@ -61,7 +93,7 @@ pub type Result = std::result::Result(key: Key) -> Result where Key: AsRef, Type: std::str::FromStr, @@ -29,7 +56,7 @@ pub enum Error /// The value could not be converted to the desired type. /// - /// Encountered when the call to [`FromStr::from_str`] fails. + /// Encountered when the call to [`std::str::FromStr::from_str`] fails. CannotConvertValue(ConversionError), } diff --git a/src/single/mod.rs b/src/single/mod.rs new file mode 100644 index 0000000..13ebf77 --- /dev/null +++ b/src/single/mod.rs @@ -0,0 +1,15 @@ +//! Lowest-level API — Manually select configuration sources. +//! +//! It can be useful if you want to specify manually the sources to access when retrieving configuration values. +//! +//! Each possible source has an associated module, and a feature named `single_{MODULENAME}` enabling it; see the list of modules below to see what sources are available! + + +#[cfg(feature = "single_envvars")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envvars")))] +pub mod envvars; + +#[cfg(feature = "single_envfiles")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envfiles")))] +pub mod envfiles; + diff --git a/src/testing/mod.rs b/src/testing/mod.rs new file mode 100644 index 0000000..9a6b778 --- /dev/null +++ b/src/testing/mod.rs @@ -0,0 +1,17 @@ +//! Fixtures for testing. +//! +//! **Unstable**; not supposed to be used outside this crate; do not add `pub(crate)` or doctests will stop working. + +/// Create a temporary file and write `content` inside it. +/// +/// The file will be deleted as soon as the [`tempfile::TempPath`] is dropped. +pub fn tempfile_fixture(content: &str) -> tempfile::TempPath { + use std::io::Write; + + let mut file = tempfile::NamedTempFile::new() + .expect("the tempfile fixture to be created successfully"); + write!(file, "{}", content) + .expect("to be able to write into the tempfile fixture"); + + file.into_temp_path() +} diff --git a/tests/integration/macros.rs b/tests/integration/macros.rs new file mode 100644 index 0000000..6dafe09 --- /dev/null +++ b/tests/integration/macros.rs @@ -0,0 +1,17 @@ +use std::env; + +micronfig::required!(PLAYER_NAME, String); +micronfig::required!(PLAYER_ID, u64); +micronfig::optional!(IS_SUS, bool); + + +#[test] +fn test_macros() { + env::set_var("PLAYER_NAME", "Steffo"); + env::set_var("PLAYER_ID", "1234"); + env::remove_var("IS_SUS"); + + assert_eq!(*PLAYER_NAME, "Steffo"); + assert_eq!(*PLAYER_ID, 1234u64); + assert_eq!(*IS_SUS, None); +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs new file mode 100644 index 0000000..eda363d --- /dev/null +++ b/tests/integration/mod.rs @@ -0,0 +1 @@ +pub mod macros;