use crate::map::MapPropertySource;
use crate::property::*;
#[cfg(feature = "enable_log")]
use log::*;
use std::collections::HashSet;
use std::fmt::{Display, Error, Formatter};
#[cfg(test)]
#[macro_use(quickcheck)]
extern crate quickcheck_macros;
#[cfg(feature = "enable_derive")]
pub use salak_derive::FromEnvironment;
#[cfg(feature = "enable_args")]
#[macro_use]
pub mod args;
pub mod env;
mod environment;
pub mod map;
pub mod property;
#[cfg(feature = "enable_toml")]
pub mod toml;
pub use crate::environment::{PlaceholderResolver, Salak, SalakBuilder, SourceRegistry};
#[derive(Clone, Debug)]
pub enum Property {
Str(String),
Int(i64),
Float(f64),
Bool(bool),
}
#[derive(Debug, PartialEq, Eq)]
pub enum PropertyError {
NotFound(String),
ParseFail(String),
RecursiveParse(String),
}
impl Display for PropertyError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
match self {
PropertyError::NotFound(n) => write!(f, "Property {} not found.", n),
PropertyError::ParseFail(e) => write!(f, "{}", e),
PropertyError::RecursiveParse(n) => write!(f, "Property {} recursive.", &n),
}
}
}
#[doc(hidden)]
pub trait SalakStringUtil {
fn to_prefix(self) -> String;
}
impl SalakStringUtil for &str {
fn to_prefix(self) -> String {
if self.is_empty() {
self.to_owned()
} else {
format!("{}.", self)
}
}
}
pub trait PropertySource: Sync + Send {
fn name(&self) -> String;
fn get_property(&self, name: &str) -> Option<Property>;
fn contains_property(&self, name: &str) -> bool {
self.get_property(name).is_some()
}
fn is_empty(&self) -> bool;
}
pub struct EnvironmentOption {
map: MapPropertySource,
}
impl EnvironmentOption {
pub fn new() -> Self {
Self {
map: MapPropertySource::empty("environment_option_default"),
}
}
pub fn insert<P: ToProperty>(&mut self, name: String, value: P) {
self.map.insert(name, value);
}
}
pub trait Environment: Sync + Send + Sized {
fn contains(&self, name: &str) -> bool {
self.require::<Property>(name).is_ok()
}
fn require<T: FromEnvironment>(&self, name: &str) -> Result<T, PropertyError> {
self.require_with_options(name, false, &mut EnvironmentOption::new())
}
fn require_raw<T: FromEnvironment>(&self, name: &str) -> Result<T, PropertyError> {
self.require_with_options(name, true, &mut EnvironmentOption::new())
}
fn require_with_options<T: FromEnvironment>(
&self,
name: &str,
disable_placeholder: bool,
mut_option: &mut EnvironmentOption,
) -> Result<T, PropertyError>;
fn require_or<T: FromEnvironment>(&self, name: &str, default: T) -> Result<T, PropertyError> {
match self.require::<Option<T>>(name) {
Ok(Some(a)) => Ok(a),
Ok(None) => Ok(default),
Err(e) => Err(e),
}
}
fn get<T: FromEnvironment>(&self, name: &str) -> Option<T> {
self.require(name).ok()
}
fn get_or<T: FromEnvironment>(&self, name: &str, default: T) -> T {
self.get(name).unwrap_or(default)
}
}
pub trait FromEnvironment: Sized {
fn from_env(
prefix: &str,
p: Option<Property>,
env: &impl Environment,
disable_placeholder: bool,
mut_option: &mut EnvironmentOption,
) -> Result<Self, PropertyError>;
fn from_err(err: PropertyError) -> Result<Self, PropertyError> {
Err(err)
}
}
impl<P: FromProperty> FromEnvironment for P {
fn from_env(
n: &str,
property: Option<Property>,
_: &impl Environment,
_: bool,
_: &mut EnvironmentOption,
) -> Result<Self, PropertyError> {
if let Some(p) = property {
return P::from_property(p);
}
P::from_err(PropertyError::NotFound(n.to_owned()))
}
}
impl<P: FromEnvironment> FromEnvironment for Option<P> {
fn from_env(
n: &str,
property: Option<Property>,
env: &impl Environment,
disable_placeholder: bool,
mut_option: &mut EnvironmentOption,
) -> Result<Self, PropertyError> {
match P::from_env(n, property, env, disable_placeholder, mut_option) {
Ok(a) => Ok(Some(a)),
Err(err) => Self::from_err(err),
}
}
fn from_err(err: PropertyError) -> Result<Self, PropertyError> {
match err {
PropertyError::NotFound(_) => Ok(None),
_ => Err(err),
}
}
}
impl<P: FromEnvironment> FromEnvironment for Vec<P> {
fn from_env(
name: &str,
_: Option<Property>,
env: &impl Environment,
disable_placeholder: bool,
mut_option: &mut EnvironmentOption,
) -> Result<Self, PropertyError> {
let mut vs = vec![];
let mut i = 0;
let mut key = format!("{}{}", &name.to_prefix(), i);
while let Some(v) = <Option<P>>::from_env(
&key,
env.require::<Option<Property>>(&key)?,
env,
disable_placeholder.clone(),
mut_option,
)? {
vs.push(v);
i += 1;
key = format!("{}{}", &name.to_prefix(), i);
}
Ok(vs)
}
}
#[cfg(feature = "salak_derive")]
#[cfg(feature = "salak_toml")]
#[cfg(test)]
mod tests {
use crate::*;
#[derive(FromEnvironment, Debug)]
pub struct DatabaseConfigObj {
hello: String,
world: Option<String>,
}
#[derive(FromEnvironment, Debug)]
pub struct DatabaseConfigDetail {
#[salak(default = "str")]
option_str: String,
#[salak(default = 1)]
option_i64: i64,
option_arr: Vec<i64>,
option_obj: Vec<DatabaseConfigObj>,
}
#[derive(FromEnvironment, Debug)]
pub struct DatabaseConfig {
url: String,
#[salak(default = "salak")]
name: String,
#[salak(default = "{database.name}")]
username: String,
password: Option<String>,
#[salak(default = "{Hello}", disable_placeholder = true)]
description: String,
detail: DatabaseConfigDetail,
}
#[test]
fn integration_tests() {
let mut env = SalakBuilder::new().build();
let mut hey = MapPropertySource::empty("hey");
hey.insert("database.detail.option_arr.0".to_owned(), "10");
hey.insert("database.url".to_owned(), "localhost:5432");
env.register_source(Box::new(hey));
let ret = env.require::<DatabaseConfig>("database");
assert_eq!(true, ret.is_ok());
let ret = ret.unwrap();
assert_eq!("localhost:5432", ret.url);
assert_eq!("salak", ret.name);
assert_eq!("salak", ret.username);
assert_eq!(None, ret.password);
let ret = ret.detail;
assert_eq!("str", ret.option_str);
assert_eq!(1, ret.option_i64);
assert_eq!(5, ret.option_arr.len());
assert_eq!(2, ret.option_obj.len());
}
}