mirror of
https://github.com/Steffo99/micronfig.git
synced 2024-11-27 18:34:23 +00:00
v0.2: Macros, better module structure
This commit is contained in:
parent
933f28a7a5
commit
412c3e98b5
17 changed files with 736 additions and 527 deletions
|
@ -3,15 +3,20 @@
|
|||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/e01_the_cave/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/e02_quick_math/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/e03_order_a_pizza/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples/e04_macros/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests/test_macros/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples/e01_the_cave/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples/e02_quick_math/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples/e03_order_a_pizza/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/examples/e04_macros/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tests/test_macros/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
24
Cargo.toml
24
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "micronfig"
|
||||
version = "0.1.2"
|
||||
version = "0.2.0"
|
||||
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
||||
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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2,40 +2,44 @@ use std::fmt::Formatter;
|
|||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn main() {
|
||||
|
||||
// The name of the person who ordered the pizza.
|
||||
let full_name: String = micronfig::required("FULLNAME");
|
||||
micronfig::required!(FULLNAME, String);
|
||||
|
||||
// The (IP) address the pizza should be delivered to.
|
||||
let destination: IpAddr = micronfig::required("DESTINATION");
|
||||
micronfig::required!(DESTINATION, IpAddr);
|
||||
|
||||
// The base of the pizza to add toppings on.
|
||||
let pizza_base: PizzaBase = micronfig::required("PIZZABASE");
|
||||
micronfig::required!(PIZZABASE, PizzaBase);
|
||||
|
||||
// The toppings to add to the pizza.
|
||||
let pizza_toppings: PizzaToppingsList = micronfig::optional("PIZZATOPPINGS")
|
||||
.unwrap_or_else(|| PizzaToppingsList{ list: vec![] });
|
||||
micronfig::optional!(PIZZATOPPINGS, PizzaToppingsList);
|
||||
// A pizza with no toppings, to use as fallback.
|
||||
const PIZZATOPPINGS_NONE: PizzaToppingsList = PizzaToppingsList{ list: vec![] };
|
||||
|
||||
|
||||
fn main() {
|
||||
// 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<String>
|
||||
}
|
||||
|
|
267
src/any.rs
267
src/any.rs
|
@ -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, KeySuffixFile, Type>(key: Key, key_suffix_file: KeySuffixFile) -> Source<Type>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
KeySuffixFile: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
<Type as 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<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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<Type>),
|
||||
|
||||
/// The result was obtained by [`file::get`].
|
||||
File(file::Result<Type>),
|
||||
}
|
||||
|
||||
impl<Type> Source<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<u8>::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::<u8>::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::<u8>::File(Ok(1)).unwrap();
|
||||
/// assert_eq!(value, 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```should_panic
|
||||
/// use micronfig::any::Source;
|
||||
/// use micronfig::file::Error as FileError;
|
||||
///
|
||||
/// let value = Source::<u8>::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::<String>::Var(Err(var::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn unwrap_file_err() {
|
||||
Source::<String>::File(Err(file::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
|
||||
}
|
||||
}
|
153
src/handle/mod.rs
Normal file
153
src/handle/mod.rs
Normal file
|
@ -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<Type>(key: &str) -> Type
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<Type>(), &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::<Type>(), &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<String> = 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<IpAddr> = 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<Type>(name: &str) -> Option<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<Type>(), &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::<Type>(), &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,
|
||||
|
||||
}
|
||||
}
|
245
src/lib.rs
245
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<IpAddr> = 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<Type>(key: &str) -> Type
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<Type>(), &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::<Type>(), &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<u8> = 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<u8> = 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<Type>(name: &str) -> Option<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<Type>(), &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::<Type>(), &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()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "testing")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "testing")))]
|
||||
pub mod testing;
|
82
src/macros/mod.rs
Normal file
82
src/macros/mod.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
293
src/multi/mod.rs
Normal file
293
src/multi/mod.rs
Normal file
|
@ -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<u32> = 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<IpAddr> = get("IP_ADDRESS", "_FILE");
|
||||
/// ```
|
||||
///
|
||||
pub fn get<Key, KeySuffixFile, Type>(key: Key, key_suffix_file: KeySuffixFile) -> Source<Type>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
KeySuffixFile: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
<Type as 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<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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<Type>),
|
||||
|
||||
/// The result was obtained by [`envfiles::get`].
|
||||
#[cfg(feature = "single_envfiles")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "single_envfiles")))]
|
||||
EnvFile(envfiles::Result<Type>),
|
||||
}
|
||||
|
||||
impl<Type> Source<Type>
|
||||
where Type: std::str::FromStr,
|
||||
<Type as 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::<u8>::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::<u8>::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::<u8>::EnvFile(Ok(1)).unwrap();
|
||||
/// assert_eq!(value, 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```should_panic
|
||||
/// use micronfig::multi::Source;
|
||||
/// use micronfig::single::envfiles::Error as FileError;
|
||||
///
|
||||
/// let value = Source::<u8>::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::<String>::EnvVar(Err(envvars::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[cfg(feature = "single_envfiles")]
|
||||
fn unwrap_file_err() {
|
||||
Source::<String>::EnvFile(Err(envfiles::Error::CannotReadEnvVar(std::env::VarError::NotPresent))).unwrap();
|
||||
}
|
||||
}
|
|
@ -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, Type>(key: Key) -> Result<Type>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
|
@ -49,7 +81,7 @@ pub enum Error<ConversionError>
|
|||
|
||||
/// 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<Type> = std::result::Result<Type, Error<<Type as std::str::FromS
|
|||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::tests::tempfile_fixture;
|
||||
use crate::testing::tempfile_fixture;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
|
@ -1,7 +1,34 @@
|
|||
//! Module defining the [`get`] low-level function for environment variables, and its [`Error`] and [`Result`] associated types.
|
||||
//! Environment variables.
|
||||
|
||||
|
||||
/// Get a configuration value from 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 convert the obtained value to another of the given type using [`std::str::FromStr::from_str`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Retrieve a configuration value from the `USER` environment variable, maintaining it as a [`String`]:
|
||||
/// ```
|
||||
/// use micronfig::single::envvars::get;
|
||||
///
|
||||
/// # std::env::set_var("USER", "steffo");
|
||||
/// let user: String = get("USER").expect("USER envvar to be defined");
|
||||
/// ```
|
||||
///
|
||||
/// Retrieve a configuration value from the `IP_ADDRESS` environment variable, then try to convert it to a [`std::net::IpAddr`]:
|
||||
/// ```
|
||||
/// use std::net::IpAddr;
|
||||
/// use micronfig::single::envvars::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");
|
||||
/// ```
|
||||
///
|
||||
pub fn get<Key, Type>(key: Key) -> Result<Type>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
|
@ -29,7 +56,7 @@ pub enum Error<ConversionError>
|
|||
|
||||
/// 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),
|
||||
}
|
||||
|
15
src/single/mod.rs
Normal file
15
src/single/mod.rs
Normal file
|
@ -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;
|
||||
|
17
src/testing/mod.rs
Normal file
17
src/testing/mod.rs
Normal file
|
@ -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()
|
||||
}
|
17
tests/integration/macros.rs
Normal file
17
tests/integration/macros.rs
Normal file
|
@ -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);
|
||||
}
|
1
tests/integration/mod.rs
Normal file
1
tests/integration/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod macros;
|
Loading…
Reference in a new issue