mirror of
https://github.com/Steffo99/micronfig.git
synced 2024-11-21 15:44:20 +00:00
Improve documentation and tests
This commit is contained in:
parent
546c1e26aa
commit
e081fc693c
6 changed files with 315 additions and 150 deletions
|
@ -5,8 +5,10 @@
|
|||
<content url="file://$MODULE_DIR$">
|
||||
<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/e02_quick_math/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/e02_quick_math/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# micronfig
|
||||
|
||||
Tiny Rust crate for twelve-factor app configuration
|
||||
|
|
256
src/any.rs
Normal file
256
src/any.rs
Normal 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();
|
||||
}
|
||||
}
|
18
src/file.rs
18
src/file.rs
|
@ -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>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
|
@ -25,29 +25,39 @@ pub fn get<Key, Type>(name: Key) -> Result<Type>
|
|||
Ok(value)
|
||||
}
|
||||
|
||||
|
||||
/// A possible error encountered by [`get`].
|
||||
#[derive(Debug)]
|
||||
pub enum Error<ConversionError>
|
||||
{
|
||||
/// The environment variable could not be read.
|
||||
///
|
||||
/// Encountered when the call to [`std::env::var`] fails.
|
||||
CannotReadEnvVar(std::env::VarError),
|
||||
|
||||
/// 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),
|
||||
|
||||
/// The specified file could not be read.
|
||||
///
|
||||
/// Encountered when the call to [`std::io::Read::read_to_string`] fails.
|
||||
CannotReadFile(std::io::Error),
|
||||
|
||||
/// The value could not be converted to the desired type.
|
||||
///
|
||||
/// Encountered when the call to [`FromStr::from_str`] fails.
|
||||
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 {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::tests::tempfile_fixture;
|
||||
|
||||
|
@ -62,6 +72,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn missing_envvar() {
|
||||
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST_FILE");
|
||||
|
||||
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))"),
|
||||
|
|
176
src/lib.rs
176
src/lib.rs
|
@ -1,67 +1,43 @@
|
|||
//! 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 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)]
|
||||
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 {
|
||||
use std::io::Write;
|
||||
|
||||
|
@ -72,92 +48,4 @@ pub(crate) mod tests {
|
|||
|
||||
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();
|
||||
}
|
||||
}
|
12
src/var.rs
12
src/var.rs
|
@ -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>
|
||||
where Key: AsRef<std::ffi::OsStr>,
|
||||
Type: std::str::FromStr,
|
||||
|
@ -20,9 +20,13 @@ pub fn get<Key, Type>(name: Key) -> Result<Type>
|
|||
#[derive(Debug)]
|
||||
pub enum Error<ConversionError> {
|
||||
/// The environment variable could not be read.
|
||||
///
|
||||
/// Encountered when the call to [`std::env::var`] fails.
|
||||
CannotReadEnvVar(std::env::VarError),
|
||||
|
||||
/// The value could not be converted to the desired type.
|
||||
///
|
||||
/// Encountered when the call to [`FromStr::from_str`] fails.
|
||||
CannotConvertValue(ConversionError),
|
||||
}
|
||||
|
||||
|
@ -32,7 +36,7 @@ pub type Result<Type> = std::result::Result<Type, Error<<Type as std::str::FromS
|
|||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -45,6 +49,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn missing_envvar() {
|
||||
std::env::remove_var("THIS_ENVVAR_DOES_NOT_EXIST");
|
||||
|
||||
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))"),
|
||||
|
|
Loading…
Reference in a new issue