use std::{collections::HashMap, ffi::OsStr, fs::{self, DirEntry}};
use fosk::IdType;
use serde::{Deserialize, Serialize};
use toml::de::Error as DeserializeError;
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Config {
pub server: Option<ServerConfig>,
pub route: Option<RouteConfig>,
pub collection: Option<CollectionConfig>,
pub auth: Option<AuthConfig>,
pub upload: Option<UploadConfig>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ServerConfig {
pub port: Option<u16>,
pub folder: Option<String>,
pub enable_cors: Option<bool>,
pub allowed_origin: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RouteConfig {
pub delay: Option<u16>,
pub remap: Option<String>,
pub protect: Option<bool>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CollectionConfig {
pub name: Option<String>,
pub id_key: Option<String>,
pub id_type: Option<IdType>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AuthConfig {
pub username_field: Option<String>,
pub password_field: Option<String>,
pub roles_field: Option<String>,
pub cookie_name: Option<String>,
pub encrypt_password: Option<bool>,
pub jwt_secret: Option<String>,
pub token_collection: Option<CollectionConfig>,
pub user_collection: Option<CollectionConfig>,
pub login_endpoint: Option<String>,
pub logout_endpoint: Option<String>,
pub users_route: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UploadConfig {
pub upload_endpoint: Option<String>,
pub download_endpoint: Option<String>,
pub list_files_endpoint: Option<String>,
pub temporary: Option<bool>,
}
impl TryFrom<&str> for Config {
type Error = DeserializeError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
toml::from_str(value)
}
}
impl TryFrom<&DirEntry> for Config {
type Error = String;
fn try_from(value: &DirEntry) -> Result<Self, Self::Error> {
let content = fs::read_to_string(value.path())
.map_err(|e| e.to_string())?;
Config::try_from(content.as_str())
.map_err(|e| e.to_string())
}
}
#[derive(Debug, Default)]
pub struct ConfigStore {
map_configs: HashMap<String, Config>,
}
impl ConfigStore {
pub fn try_from_dir(dir_path: &str) -> Result<Self,std::io::Error> {
let mut store = Self::default();
fs::read_dir( dir_path)?
.filter_map(Result::ok)
.filter(|file| {
let path = file.path();
let extension = path.extension().and_then(OsStr::to_str).unwrap_or_default();
let result = extension.eq("toml");
result.to_string();
result
})
.for_each(|file| {
let key = file.path().as_path().file_stem().unwrap().to_string_lossy().to_ascii_lowercase();
match Config::try_from(&file) {
Ok(config) => { store.map_configs.insert(key, config); },
Err(err) =>
println!("Unable to load the config file {:?} due the error {}.", file.file_name(), err),
}
});
Ok(store)
}
pub fn get(&self, key: &str) -> Option<Config> {
self.map_configs.get(key.to_ascii_lowercase().as_str()).cloned()
}
}
pub trait Mergeable {
fn merge(self, parent: Self) -> Self;
}
impl Config {
pub fn merge(self, parent: Option<Self>) -> Self {
match parent {
Some(parent) => Self {
server: self.server.merge(parent.server),
route: self.route.merge(parent.route),
collection: self.collection, auth: self.auth, upload: self.upload, },
None => self,
}
}
pub fn merge_with_ref(self, parent: &Self) -> Self {
let parent = parent.clone();
Self {
server: self.server.merge(parent.server),
route: self.route.merge(parent.route),
collection: self.collection, auth: self.auth, upload: self.upload, }
}
pub fn with_protect(mut self, protect: bool) -> Self {
let mut route = self.route.unwrap_or_default();
route.protect = Some(protect);
self.route = Some(route);
self
}
pub fn with_collection_name(mut self, name: &str) -> Self {
let mut collection = self.collection.unwrap_or_default();
collection.name = Some(name.to_string());
self.collection = Some(collection);
self
}
pub fn with_id_key(mut self, id_key: &str) -> Self {
let mut collection = self.collection.unwrap_or_default();
collection.id_key = Some(id_key.to_string());
self.collection = Some(collection);
self
}
pub fn with_id_type(mut self, id_type: IdType) -> Self {
let mut collection = self.collection.unwrap_or_default();
collection.id_type = Some(id_type);
self.collection = Some(collection);
self
}
}
impl Mergeable for Config {
fn merge(self, parent: Self) -> Self {
Self {
server: self.server.merge(parent.server),
route: self.route.merge(parent.route),
collection: self.collection, auth: self.auth, upload: self.upload, }
}
}
impl Mergeable for Option<Config> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(p)) => Some(Config {
route: None.merge(p.route),
..Default::default()
}),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(Config {
server: child.server.merge(parent.server),
route: child.route.merge(parent.route),
collection: child.collection, auth: child.auth, upload: child.upload, }),
}
}
}
impl Mergeable for Option<ServerConfig> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(p)) => Some(p),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(ServerConfig {
port: child.port.merge(parent.port),
folder: child.folder.merge(parent.folder),
enable_cors: child.enable_cors.merge(parent.enable_cors),
allowed_origin: child.allowed_origin.merge(parent.allowed_origin),
}),
}
}
}
impl Mergeable for Option<RouteConfig> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(p)) => Some(RouteConfig {
delay: p.delay,
protect: p.protect,
..Default::default()
}),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(RouteConfig {
delay: child.delay.merge(parent.delay),
remap: child.remap, protect: child.protect.merge(parent.protect),
}),
}
}
}
impl Mergeable for Option<CollectionConfig> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(p)) => Some(p),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(CollectionConfig {
name: child.name.merge(parent.name),
id_key: child.id_key.merge(parent.id_key),
id_type: child.id_type.merge(parent.id_type),
}),
}
}
}
impl Mergeable for Option<AuthConfig> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(parent)) => Some(parent),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(AuthConfig {
username_field: child.username_field.merge(parent.username_field),
password_field: child.password_field.merge(parent.password_field),
roles_field: child.roles_field.merge(parent.roles_field),
cookie_name: child.cookie_name.merge(parent.cookie_name),
encrypt_password: child.encrypt_password.merge(parent.encrypt_password),
jwt_secret: child.jwt_secret.merge(parent.jwt_secret),
token_collection: child.token_collection.merge(parent.token_collection),
user_collection: child.user_collection.merge(parent.user_collection),
login_endpoint: child.login_endpoint.merge(parent.login_endpoint),
logout_endpoint: child.logout_endpoint.merge(parent.logout_endpoint),
users_route: child.users_route.merge(parent.users_route),
}),
}
}
}
impl Mergeable for Option<UploadConfig> {
fn merge(self, parent: Self) -> Self {
match (self, parent) {
(None, None) => None,
(None, Some(parent)) => Some(parent),
(Some(child), None) => Some(child),
(Some(child), Some(parent)) => Some(UploadConfig {
upload_endpoint: child.upload_endpoint.merge(parent.upload_endpoint),
download_endpoint: child.download_endpoint.merge(parent.download_endpoint),
list_files_endpoint: child.list_files_endpoint.merge(parent.list_files_endpoint),
temporary: child.temporary.merge(parent.temporary)
}),
}
}
}
impl Mergeable for Option<String> {
fn merge(self, parent: Self) -> Self {
if self.is_some() { self } else { parent }
}
}
impl Mergeable for Option<bool> {
fn merge(self, parent: Self) -> Self {
if self.is_some() { self } else { parent }
}
}
impl Mergeable for Option<u16> {
fn merge(self, parent: Self) -> Self {
if self.is_some() { self } else { parent }
}
}
impl Mergeable for Option<IdType> {
fn merge(self, parent: Self) -> Self {
if self.is_some() { self } else { parent }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_config_merge() {
let child = ServerConfig { port: Some(3000), folder: None, enable_cors: Some(false), allowed_origin: None };
let parent = ServerConfig { port: None, folder: Some("mocks".to_string()), enable_cors: Some(true), allowed_origin: Some("example.com".to_string()) };
let merged = Some(child.clone()).merge(Some(parent.clone())).unwrap();
assert_eq!(merged.port, Some(3000));
assert_eq!(merged.folder, Some("mocks".to_string()));
assert_eq!(merged.enable_cors, Some(false));
assert_eq!(merged.allowed_origin, Some("example.com".to_string()));
}
#[test]
fn test_route_config_merge() {
let child = RouteConfig { delay: None, remap: Some("/api".into()), protect: None };
let parent = RouteConfig { delay: Some(10), remap: None, protect: Some(true) };
let merged = Some(child.clone()).merge(Some(parent.clone())).unwrap();
assert_eq!(merged.delay, Some(10));
assert_eq!(merged.remap, Some("/api".to_string()));
assert_eq!(merged.protect, Some(true));
}
#[test]
fn test_collection_config_merge() {
let child = CollectionConfig { name: Some("child".into()), id_key: None, id_type: Some(IdType::Uuid) };
let parent = CollectionConfig { name: None, id_key: Some("id".into()), id_type: Some(IdType::Int) };
let merged = Some(child.clone()).merge(Some(parent.clone())).unwrap();
assert_eq!(merged.name, Some("child".to_string()));
assert_eq!(merged.id_key, Some("id".to_string()));
assert_eq!(merged.id_type, Some(IdType::Uuid));
}
#[test]
fn test_auth_config_merge() {
let child = AuthConfig {
username_field: Some("user".into()),
token_collection: Some(CollectionConfig { name: Some("tok".into()), id_key: Some("t".into()), id_type: Some(IdType::Uuid) }),
..Default::default()
};
let parent = AuthConfig {
username_field: Some("parent".into()),
password_field: Some("pass".into()),
token_collection: Some(CollectionConfig { name: Some("parent_tok".into()), id_key: None, id_type: Some(IdType::Int) }),
..Default::default()
};
let merged = Some(child.clone()).merge(Some(parent.clone())).unwrap();
assert_eq!(merged.username_field, Some("user".into()));
assert_eq!(merged.password_field, Some("pass".into()));
let token = merged.token_collection.unwrap();
assert_eq!(token.name, Some("tok".into()));
assert_eq!(token.id_key, Some("t".into()));
assert_eq!(token.id_type, Some(IdType::Uuid));
}
#[test]
fn test_upload_config_merge() {
let child = UploadConfig { upload_endpoint: None, download_endpoint: Some("/dl".into()), list_files_endpoint: None, temporary: Some(true) };
let parent = UploadConfig { upload_endpoint: Some("/up".into()), download_endpoint: None, list_files_endpoint: Some("/list".into()), temporary: Some(false) };
let merged = Some(child.clone()).merge(Some(parent.clone())).unwrap();
assert_eq!(merged.upload_endpoint, Some("/up".into()));
assert_eq!(merged.download_endpoint, Some("/dl".into()));
assert_eq!(merged.list_files_endpoint, Some("/list".into()));
assert_eq!(merged.temporary, Some(true));
}
#[test]
fn test_config_option_merge() {
let child = Config { server: Some(ServerConfig { port: Some(1), folder: None, enable_cors: None, allowed_origin: None }), route: None, collection: None, auth: None, upload: None };
let parent = Config { server: Some(ServerConfig { port: None, folder: Some("dir".into()), enable_cors: Some(true), allowed_origin: Some("o".into()) }), route: Some(RouteConfig { delay: Some(5), remap: None, protect: Some(false) }), collection: None, auth: None, upload: None };
let merged_opt = Some(child.clone()).merge(Some(parent.clone()));
let merged = merged_opt.unwrap();
let server = merged.server.unwrap();
assert_eq!(server.port, Some(1));
assert_eq!(server.folder, Some("dir".into()));
assert_eq!(server.enable_cors, Some(true));
assert_eq!(merged.route, Some(RouteConfig { delay: Some(5), remap: None, protect: Some(false) }));
}
#[test]
fn test_config_merge_trait() {
let child = Config { server: None, route: Some(RouteConfig { delay: Some(2), remap: None, protect: None }), collection: None, auth: None, upload: None };
let parent = Config { server: None, route: Some(RouteConfig { delay: None, remap: Some("/p".into()), protect: Some(true) }), collection: None, auth: None, upload: None };
let merged = child.merge(Some(parent));
let route = merged.route.unwrap();
assert_eq!(route.delay, Some(2));
assert!(route.remap.is_none());
assert_eq!(route.protect, Some(true));
}
}