1
Fork 0
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:
Steffo 2023-05-05 03:13:35 +02:00
parent 933f28a7a5
commit 412c3e98b5
Signed by: steffo
GPG key ID: 2A24051445686895
17 changed files with 736 additions and 527 deletions

View file

@ -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" />

View file

@ -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 }

View file

@ -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

View file

@ -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);
}

View file

@ -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)
}

View file

@ -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>
}

View file

@ -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
View 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,
}
}

View file

@ -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
View 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
View 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();
}
}

View file

@ -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() {

View file

@ -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
View 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
View 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()
}

View 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
View file

@ -0,0 +1 @@
pub mod macros;