mirror of
https://github.com/Steffo99/micronfig.git
synced 2024-11-23 16:44:18 +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
|
||||
|
||||
[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(_))"),
|
||||
}
|
||||
}
|
||||
}
|
187
src/lib.rs
187
src/lib.rs
|
@ -1,50 +1,163 @@
|
|||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
//! A tiny crate for [twelve-factor app configuration](https://12factor.net/config).
|
||||
|
||||
/// An error that occurred while getting a configuration value.
|
||||
pub enum GetFileError<TargetType> where TargetType: TryFrom<String> {
|
||||
CannotReadEnvVar(env::VarError),
|
||||
CannotOpenFile(std::io::Error),
|
||||
CannotReadFile(std::io::Error),
|
||||
CannotConvertValue(<TargetType as TryFrom<String>>::Error),
|
||||
pub mod var;
|
||||
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
|
||||
}
|
||||
|
||||
/// The result of an attempt at getting a configuration value.
|
||||
pub type GetFileResult<TargetType> = Result<TargetType, GetFileError<TargetType>>;
|
||||
|
||||
/// Get a configuration value from a file specified in the given environment variable.
|
||||
///
|
||||
///
|
||||
/// # 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);
|
||||
#[non_exhaustive]
|
||||
pub enum Source<Type>
|
||||
where Type: std::str::FromStr,
|
||||
{
|
||||
Var(var::Result<Type>),
|
||||
File(file::Result<Type>),
|
||||
NotFound,
|
||||
}
|
||||
|
||||
let mut file = File::open(path).map_err(GetFileError::CannotOpenFile)?;
|
||||
|
||||
let mut data = String::new();
|
||||
file.read_to_string(&mut data).map_err(GetFileError::CannotReadFile)?;
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
let value = TargetType::try_from(data).map_err(GetFileError::CannotConvertValue)?;
|
||||
|
||||
Ok(value)
|
||||
/// 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)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
todo!()
|
||||
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]
|
||||
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();
|
||||
}
|
||||
}
|
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