1
Fork 0
mirror of https://github.com/Steffo99/micronfig.git synced 2024-10-16 14:37:29 +00:00

Improve documentation and tests

This commit is contained in:
Steffo 2023-04-28 16:53:16 +02:00
parent 546c1e26aa
commit e081fc693c
Signed by: steffo
GPG key ID: 2A24051445686895
6 changed files with 315 additions and 150 deletions

View file

@ -5,8 +5,10 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/examples/e01_the_cave/src" isTestSource="false" /> <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$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/examples/e01_the_cave/target" /> <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$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View file

@ -1,2 +1,3 @@
# micronfig # micronfig
Tiny Rust crate for twelve-factor app configuration Tiny Rust crate for twelve-factor app configuration

256
src/any.rs Normal file
View file

@ -0,0 +1,256 @@
//! Module defining the general [`get`] low-level function and its associated [`Source`] type.
use crate::var;
use crate::file;
/// Get a configuration value, maintaining information about how the value was retrieved.
///
/// This function tries to get a configuration value:
///
/// 1. with [`var::get`] using `name_var`, returning a [`Source::Var`]
/// 2. with [`file::get`] using `name_file`, returning a [`Source::File`]
///
/// If none of these options successfully resulted in the successful retrieval of the configuration value, [`Source::NotFound`] is returned.
///
/// All errors are bubbled up, except the ones surfacing because of the total absence of a configuration value, currently:
/// - [`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", "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", "NUMBER_FILE");
/// if let Source::NotFound = value {} else { panic!() }
/// ```
///
pub fn get<KeyVar, KeyFile, Type>(name_var: KeyVar, name_file: KeyFile) -> Source<Type>
where KeyVar: AsRef<std::ffi::OsStr>,
KeyFile: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr,
{
let v = var::get(name_var);
match v {
Err(var::Error::CannotReadEnvVar(_)) => {},
_ => return Source::Var(v),
}
let v = file::get(name_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,
{
/// 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,
{
/// 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`].
///
/// The panic message is the `msg` given.
///
/// # 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", "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", "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", "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", "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", "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", "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();
}
}

View file

@ -1,7 +1,7 @@
//! Module defining a function retrieving a configuration value from a file at a path specified in the environment. //! Module defining the [`get`] low-level function for environment files, and its associated types.
/// Get a value of the requested type from the file at the path contained in the environment variable with the given name. /// Get a configuration value from the file at the path contained in the environment variable with the given `name`, and convert it to the desired `Type`.
pub fn get<Key, Type>(name: Key) -> Result<Type> pub fn get<Key, Type>(name: Key) -> Result<Type>
where Key: AsRef<std::ffi::OsStr>, where Key: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr, Type: std::str::FromStr,
@ -25,29 +25,39 @@ pub fn get<Key, Type>(name: Key) -> Result<Type>
Ok(value) Ok(value)
} }
/// A possible error encountered by [`get`]. /// A possible error encountered by [`get`].
#[derive(Debug)] #[derive(Debug)]
pub enum Error<ConversionError> pub enum Error<ConversionError>
{ {
/// The environment variable could not be read. /// The environment variable could not be read.
///
/// Encountered when the call to [`std::env::var`] fails.
CannotReadEnvVar(std::env::VarError), CannotReadEnvVar(std::env::VarError),
/// The specified file could not be opened. (Probably it doesn't exist.) /// The specified file could not be opened. (Probably it doesn't exist.)
///
/// Encountered when the call to [`std::fs::File::open`] fails.
CannotOpenFile(std::io::Error), CannotOpenFile(std::io::Error),
/// The specified file could not be read. /// The specified file could not be read.
///
/// Encountered when the call to [`std::io::Read::read_to_string`] fails.
CannotReadFile(std::io::Error), CannotReadFile(std::io::Error),
/// The value could not be converted to the desired type. /// The value could not be converted to the desired type.
///
/// Encountered when the call to [`FromStr::from_str`] fails.
CannotConvertValue(ConversionError), CannotConvertValue(ConversionError),
} }
/// A possible error encountered by [`get`]. /// A possible error encountered by [`get`].
pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>; pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>;
#[cfg(test)] #[cfg(test)]
mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::tests::tempfile_fixture; use crate::tests::tempfile_fixture;
@ -62,6 +72,8 @@ mod tests {
#[test] #[test]
fn missing_envvar() { fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST_FILE");
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST_FILE") { match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST_FILE") {
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {}, Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"), _ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),

View file

@ -1,67 +1,43 @@
//! A tiny crate for [twelve-factor app configuration](https://12factor.net/config). //! A tiny crate for [twelve-factor app configuration](https://12factor.net/config).
//!
//! # Goals
//!
//! This crate aims to simplify developing and deploying Docker-compatible services in Rust.
//!
//! # Features
//!
//! This crate handles:
//!
//! 1. Retrieval of configuration values from multiple sources ([`any::get`])
//! 1. The environment ([`var::get`])
//! 2. Files specified in the environment ([`file::get`])
//! 2. Conversion to a value of an arbitrary type ([`std::str::FromStr`])
//! 3. Displaying a operator-friendly error in case
//!
//! # Usage
//!
//! The following example:
//!
//! 1. Tries to retrieve the value of the configuration value `THIS_ENVVAR_CONTAINS_ONE`
//! 1. From the `THIS_ENVVAR_CONTAINS_ONE` environment variable
//! 2. From the contents of the file specified in the `THIS_ENVVAR_CONTAINS_ONE_FILE` environment variable
//! 2. It converts the value to a [`u8`]
//! 3. Panics with a operator-friendly error if any of these steps failed
//!
//! ```
//! todo!()
//! ```
pub mod any;
pub mod var; pub mod var;
pub mod file; pub mod file;
/// Get a value of the requested type, trying the following sources in order:
///
/// 1. the environment variable `{name}` (see [`var::get`])
/// 2. the contents of the file at the path specified at the environment variable `{name}_FILE` (see [`file::get`])
///
pub fn get<Type>(name: &str) -> Source<Type>
where Type: std::str::FromStr,
{
let v = var::get(name);
match v {
Err(var::Error::CannotReadEnvVar(_)) => {},
_ => return Source::Var(v),
}
let v = file::get(format!("{name}_FILE"));
match v {
Err(file::Error::CannotReadEnvVar(_)) => {},
_ => return Source::File(v),
}
Source::NotFound
}
#[non_exhaustive]
pub enum Source<Type>
where Type: std::str::FromStr,
{
Var(var::Result<Type>),
File(file::Result<Type>),
NotFound,
}
impl<Type> Source<Type>
where Type: std::str::FromStr,
{
/// Like [`Result::expect`], but tries to access the nested [`Ok`] value.
pub fn expect(self, msg: &str) -> Type {
match self {
Self::Var(Ok(v)) => v,
Self::File(Ok(v)) => v,
_ => panic!("{}", msg),
}
}
/// Like [`Result::unwrap`], but tries to access the nested [`Ok`] value.
pub fn unwrap(self) -> Type {
self.expect("called `Source::unwrap()` on an `Err` or `NotFound` value")
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; /// 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 { pub(crate) fn tempfile_fixture(content: &str) -> tempfile::TempPath {
use std::io::Write; use std::io::Write;
@ -72,92 +48,4 @@ pub(crate) mod tests {
file.into_temp_path() file.into_temp_path()
} }
#[test]
fn it_works_var() {
std::env::set_var("NUMBER", "1");
std::env::remove_var("NUMBER_FILE");
match get::<u32>("NUMBER") {
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::<u32>("NUMBER");
match n {
Source::File(Ok(1u32)) => {},
_ => panic!("expected Source::File(Ok(1u32))")
}
}
#[test]
fn missing_envvar() {
match get::<String>("THIS_ENVVAR_DOES_NOT_EXIST") {
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::<u32>("NUMBER") {
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::<u32>("NUMBER") {
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::<u32>("NUMBER") {
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();
}
} }

View file

@ -1,7 +1,7 @@
//! Module defining a function retrieving a configuration value from the environment. //! Module defining the [`get`] low-level function for environment variables, and its associated types.
/// Get a value of the requested type from the environment variable with the given name. /// Get a configuration value from the environment variable with the given `name`, and convert it to the desired `Type`.
pub fn get<Key, Type>(name: Key) -> Result<Type> pub fn get<Key, Type>(name: Key) -> Result<Type>
where Key: AsRef<std::ffi::OsStr>, where Key: AsRef<std::ffi::OsStr>,
Type: std::str::FromStr, Type: std::str::FromStr,
@ -20,9 +20,13 @@ pub fn get<Key, Type>(name: Key) -> Result<Type>
#[derive(Debug)] #[derive(Debug)]
pub enum Error<ConversionError> { pub enum Error<ConversionError> {
/// The environment variable could not be read. /// The environment variable could not be read.
///
/// Encountered when the call to [`std::env::var`] fails.
CannotReadEnvVar(std::env::VarError), CannotReadEnvVar(std::env::VarError),
/// The value could not be converted to the desired type. /// The value could not be converted to the desired type.
///
/// Encountered when the call to [`FromStr::from_str`] fails.
CannotConvertValue(ConversionError), CannotConvertValue(ConversionError),
} }
@ -32,7 +36,7 @@ pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromS
#[cfg(test)] #[cfg(test)]
mod tests { pub(crate) mod tests {
use super::*; use super::*;
#[test] #[test]
@ -45,6 +49,8 @@ mod tests {
#[test] #[test]
fn missing_envvar() { fn missing_envvar() {
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST");
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST") { match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST") {
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {}, Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"), _ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),