mirror of
https://github.com/Steffo99/micronfig.git
synced 2024-11-27 18:34:23 +00:00
Complete the library
This commit is contained in:
parent
1a08c9638e
commit
b31d6841ee
4 changed files with 307 additions and 37 deletions
|
@ -6,3 +6,6 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.5.0"
|
||||||
|
|
91
src/file.rs
Normal file
91
src/file.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
//! Module defining a function retrieving a configuration value from a file at a path specified in the environment.
|
||||||
|
|
||||||
|
|
||||||
|
/// Get a value of the requested type from the file at the path contained in the environment variable with the given name.
|
||||||
|
pub fn get<Key, Type>(name: Key) -> Result<Type>
|
||||||
|
where Key: AsRef<std::ffi::OsStr>,
|
||||||
|
Type: std::str::FromStr,
|
||||||
|
{
|
||||||
|
let path = std::env::var(name)
|
||||||
|
.map_err(Error::CannotReadEnvVar)?;
|
||||||
|
let path = std::ffi::OsString::from(path);
|
||||||
|
let path = std::path::PathBuf::from(path);
|
||||||
|
|
||||||
|
let mut file = std::fs::File::open(path)
|
||||||
|
.map_err(Error::CannotOpenFile)?;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
let mut data = String::new();
|
||||||
|
file.read_to_string(&mut data)
|
||||||
|
.map_err(Error::CannotReadFile)?;
|
||||||
|
|
||||||
|
let value = Type::from_str(&data)
|
||||||
|
.map_err(Error::CannotConvertValue)?;
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A possible error encountered by [`get`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<ConversionError>
|
||||||
|
{
|
||||||
|
/// The environment variable could not be read.
|
||||||
|
CannotReadEnvVar(std::env::VarError),
|
||||||
|
|
||||||
|
/// The specified file could not be opened. (Probably it doesn't exist.)
|
||||||
|
CannotOpenFile(std::io::Error),
|
||||||
|
|
||||||
|
/// The specified file could not be read.
|
||||||
|
CannotReadFile(std::io::Error),
|
||||||
|
|
||||||
|
/// The value could not be converted to the desired type.
|
||||||
|
CannotConvertValue(ConversionError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A possible error encountered by [`get`].
|
||||||
|
pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>;
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::tests::tempfile_fixture;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let file = tempfile_fixture("1");
|
||||||
|
std::env::set_var("NUMBER_FILE", file.as_os_str());
|
||||||
|
|
||||||
|
let number = get::<&str, u32>("NUMBER_FILE").unwrap();
|
||||||
|
assert_eq!(number, 1u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_envvar() {
|
||||||
|
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST_FILE") {
|
||||||
|
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
|
||||||
|
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_file() {
|
||||||
|
std::env::set_var("NUMBER_FILE", "/this/file/does/not/exist");
|
||||||
|
|
||||||
|
match get::<&str, u32>("NUMBER_FILE") {
|
||||||
|
Err(Error::CannotOpenFile(_)) => {},
|
||||||
|
_ => panic!("expected Err(Error::CannotOpenFile(_))"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_a_number() {
|
||||||
|
let file = tempfile_fixture("XYZ");
|
||||||
|
std::env::set_var("NUMBER_FILE", file.as_os_str());
|
||||||
|
|
||||||
|
match get::<&str, u32>("NUMBER_FILE") {
|
||||||
|
Err(Error::CannotConvertValue(_)) => {},
|
||||||
|
_ => panic!("expected Err(Error::CannotConvertValue(_))"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
179
src/lib.rs
179
src/lib.rs
|
@ -1,50 +1,163 @@
|
||||||
use std::env;
|
//! A tiny crate for [twelve-factor app configuration](https://12factor.net/config).
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::fs::File;
|
|
||||||
|
|
||||||
/// An error that occurred while getting a configuration value.
|
pub mod var;
|
||||||
pub enum GetFileError<TargetType> where TargetType: TryFrom<String> {
|
pub mod file;
|
||||||
CannotReadEnvVar(env::VarError),
|
|
||||||
CannotOpenFile(std::io::Error),
|
|
||||||
CannotReadFile(std::io::Error),
|
/// Get a value of the requested type, trying the following sources in order:
|
||||||
CannotConvertValue(<TargetType as TryFrom<String>>::Error),
|
///
|
||||||
|
/// 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),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of an attempt at getting a configuration value.
|
let v = file::get(format!("{name}_FILE"));
|
||||||
pub type GetFileResult<TargetType> = Result<TargetType, GetFileError<TargetType>>;
|
|
||||||
|
|
||||||
/// Get a configuration value from a file specified in the given environment variable.
|
match v {
|
||||||
///
|
Err(file::Error::CannotReadEnvVar(_)) => {},
|
||||||
///
|
_ => return Source::File(v),
|
||||||
/// # Parameters
|
}
|
||||||
///
|
|
||||||
/// - `name`: the name of the environment variable containing the location of the file containing the configuration value.
|
|
||||||
/// - `TargetType`: the struct the contents of the file are to be converted into using [`TryFrom`] [`String`].
|
|
||||||
///
|
|
||||||
pub fn get_file<TargetType>(name: &str) -> GetFileResult<TargetType> where TargetType: TryFrom<String> {
|
|
||||||
let path = env::var(name).map_err(GetFileError::CannotReadEnvVar)?;
|
|
||||||
let path = OsString::from(path);
|
|
||||||
let path = PathBuf::from(path);
|
|
||||||
|
|
||||||
let mut file = File::open(path).map_err(GetFileError::CannotOpenFile)?;
|
Source::NotFound
|
||||||
|
}
|
||||||
|
|
||||||
let mut data = String::new();
|
|
||||||
file.read_to_string(&mut data).map_err(GetFileError::CannotReadFile)?;
|
|
||||||
|
|
||||||
let value = TargetType::try_from(data).map_err(GetFileError::CannotConvertValue)?;
|
#[non_exhaustive]
|
||||||
|
pub enum Source<Type>
|
||||||
|
where Type: std::str::FromStr,
|
||||||
|
{
|
||||||
|
Var(var::Result<Type>),
|
||||||
|
File(file::Result<Type>),
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
Ok(value)
|
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)]
|
||||||
mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn it_works_var() {
|
||||||
todo!()
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
63
src/var.rs
Normal file
63
src/var.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
//! Module defining a function retrieving a configuration value from the environment.
|
||||||
|
|
||||||
|
|
||||||
|
/// Get a value of the requested type from the environment variable with the given name.
|
||||||
|
pub fn get<Key, Type>(name: Key) -> Result<Type>
|
||||||
|
where Key: AsRef<std::ffi::OsStr>,
|
||||||
|
Type: std::str::FromStr,
|
||||||
|
{
|
||||||
|
let data = std::env::var(name)
|
||||||
|
.map_err(Error::CannotReadEnvVar)?;
|
||||||
|
|
||||||
|
let value = Type::from_str(&data)
|
||||||
|
.map_err(Error::CannotConvertValue)?;
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A possible error encountered by [`get`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error<ConversionError> {
|
||||||
|
/// The environment variable could not be read.
|
||||||
|
CannotReadEnvVar(std::env::VarError),
|
||||||
|
|
||||||
|
/// The value could not be converted to the desired type.
|
||||||
|
CannotConvertValue(ConversionError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The result of [`get`].
|
||||||
|
pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromStr>::Err>>;
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
std::env::set_var("NUMBER", "1");
|
||||||
|
|
||||||
|
let number = get::<&str, u32>("NUMBER").unwrap();
|
||||||
|
assert_eq!(number, 1u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_envvar() {
|
||||||
|
match get::<&str, String>("THIS_ENVVAR_DOES_NOT_EXIST") {
|
||||||
|
Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent)) => {},
|
||||||
|
_ => panic!("expected Err(Error::CannotReadEnvVar(std::env::VarError::NotPresent))"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_a_number() {
|
||||||
|
std::env::set_var("NUMBER", "XYZ");
|
||||||
|
|
||||||
|
match get::<&str, u32>("NUMBER") {
|
||||||
|
Err(Error::CannotConvertValue(_)) => {},
|
||||||
|
_ => panic!("expected Error::CannotConvertValue(_)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue