use crate::{Config, ConfigError, ConfigResult};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::RwLock;
pub trait PropertiesConfig: Any + Send + Sync {
fn prefix() -> &'static str
where
Self: Sized;
fn load_from_config(config: &Config) -> ConfigResult<Self>
where
Self: Sized + serde::de::DeserializeOwned,
{
let prefix = Self::prefix();
let map = config.get_prefix(prefix);
if map.is_empty() {
return Err(ConfigError::MissingProperty(prefix.to_string()));
}
let json = serde_json::to_value(map)
.map_err(|e| ConfigError::Parse(format!("Failed to convert to JSON: {}", e)))?;
serde_json::from_value(json).map_err(|e| {
ConfigError::Deserialize(format!(
"Failed to deserialize {} with prefix '{}': {}",
std::any::type_name::<Self>(),
prefix,
e
))
})
}
fn load_or_default(config: &Config) -> Self
where
Self: Sized + serde::de::DeserializeOwned + Default,
{
Self::load_from_config(config).unwrap_or_default()
}
fn validate(&self) -> ConfigResult<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct PropertiesConfigRegistry {
configs: RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>,
}
impl PropertiesConfigRegistry {
pub fn new() -> Self {
Self {
configs: RwLock::new(HashMap::new()),
}
}
pub fn register<T>(&self, config: T)
where
T: PropertiesConfig + 'static,
{
let mut configs = self
.configs
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs.insert(TypeId::of::<T>(), Box::new(config));
}
pub fn register_from_config<T>(&self, config: &Config) -> ConfigResult<()>
where
T: PropertiesConfig + serde::de::DeserializeOwned + 'static,
{
let value = T::load_from_config(config)?;
value.validate()?;
self.register(value);
Ok(())
}
pub fn get<T>(&self) -> Option<T>
where
T: PropertiesConfig + Clone + 'static,
{
let configs = self
.configs
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs
.get(&TypeId::of::<T>())
.and_then(|v| v.downcast_ref::<T>())
.cloned()
}
pub fn get_or_load<T>(&self, config: &Config) -> ConfigResult<T>
where
T: PropertiesConfig + serde::de::DeserializeOwned + Clone + 'static,
{
if let Some(value) = self.get::<T>() {
return Ok(value);
}
let value = T::load_from_config(config)?;
value.validate()?;
self.register(value.clone());
Ok(value)
}
pub fn contains<T>(&self) -> bool
where
T: 'static,
{
let configs = self
.configs
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs.contains_key(&TypeId::of::<T>())
}
pub fn remove<T>(&self) -> bool
where
T: 'static,
{
let mut configs = self
.configs
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs.remove(&TypeId::of::<T>()).is_some()
}
pub fn clear(&self) {
let mut configs = self
.configs
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs.clear();
}
pub fn len(&self) -> usize {
let configs = self
.configs
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
configs.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for PropertiesConfigRegistry {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct NestedProperties;
impl NestedProperties {
pub(crate) fn flatten_key(key: &str) -> String {
key.replace('.', "_")
}
pub(crate) fn nest_key(key: &str) -> String {
key.replace('_', ".")
}
pub(crate) fn extract_prefix(key: &str) -> Option<String> {
key.rfind('.').map(|pos| key[..pos].to_string())
}
pub(crate) fn extract_suffix(key: &str) -> Option<String> {
if let Some(pos) = key.rfind('.') {
Some(key[pos + 1..].to_string())
} else {
Some(key.to_string())
}
}
}
pub(crate) struct PropertiesConfigBuilder<T> {
_phantom: std::marker::PhantomData<T>,
}
impl<T> PropertiesConfigBuilder<T>
where
T: PropertiesConfig + serde::de::DeserializeOwned,
{
pub(crate) fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub(crate) fn load(&self, config: &Config) -> ConfigResult<T> {
T::load_from_config(config)
}
pub(crate) fn load_or_default(&self, config: &Config) -> T
where
T: Default,
{
T::load_or_default(config)
}
pub(crate) fn load_and_validate(&self, config: &Config) -> ConfigResult<T> {
let value = T::load_from_config(config)?;
value.validate()?;
Ok(value)
}
}
impl<T> Default for PropertiesConfigBuilder<T>
where
T: PropertiesConfig + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! impl_properties_config {
($type:ty, $prefix:expr) => {
impl $crate::PropertiesConfig for $type {
fn prefix() -> &'static str {
$prefix
}
}
};
}
#[macro_export]
macro_rules! properties_config {
($(#[$meta:meta])* $name:ident { $($field:ident: $field_type:ty),* $(,)? }) => {
$(#[$meta])*
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct $name {
$(
pub $field: $field_type,
)*
}
impl $crate::PropertiesConfig for $name {
fn prefix() -> &'static str {
stringify!($name)
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, serde::Deserialize)]
struct TestConfig {
value: String,
}
impl PropertiesConfig for TestConfig {
fn prefix() -> &'static str {
"test"
}
}
#[test]
fn test_registry() {
let registry = PropertiesConfigRegistry::new();
assert!(registry.is_empty());
let config = TestConfig {
value: "test_config".to_string(),
};
registry.register(config);
assert!(registry.contains::<TestConfig>());
assert_eq!(registry.len(), 1);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn test_registry_get() {
let registry = PropertiesConfigRegistry::new();
let config = TestConfig {
value: "hello".to_string(),
};
registry.register(config);
let retrieved = registry.get::<TestConfig>();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().value, "hello");
}
#[test]
fn test_registry_get_unregistered() {
let registry = PropertiesConfigRegistry::new();
assert!(registry.get::<TestConfig>().is_none());
}
#[test]
fn test_registry_remove() {
let registry = PropertiesConfigRegistry::new();
registry.register(TestConfig {
value: "to_remove".to_string(),
});
assert!(registry.contains::<TestConfig>());
assert!(registry.remove::<TestConfig>());
assert!(!registry.contains::<TestConfig>());
}
#[test]
fn test_registry_remove_absent() {
let registry = PropertiesConfigRegistry::new();
assert!(!registry.remove::<TestConfig>());
}
#[test]
fn test_registry_len_and_empty() {
let registry = PropertiesConfigRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
registry.register(TestConfig {
value: "a".to_string(),
});
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
registry.register(TestConfig {
value: "b".to_string(),
});
assert_eq!(registry.len(), 1);
}
#[test]
fn test_registry_default() {
let registry = PropertiesConfigRegistry::default();
assert!(registry.is_empty());
}
#[test]
fn test_nested_properties() {
assert_eq!(NestedProperties::flatten_key("server.port"), "server_port");
assert_eq!(NestedProperties::nest_key("server_port"), "server.port");
assert_eq!(NestedProperties::extract_prefix("server.port"), Some("server".to_string()));
assert_eq!(NestedProperties::extract_suffix("server.port"), Some("port".to_string()));
}
#[test]
fn test_nested_properties_no_dot() {
assert_eq!(NestedProperties::extract_prefix("simple"), None);
assert_eq!(NestedProperties::extract_suffix("simple"), Some("simple".to_string()));
}
#[test]
fn test_nested_properties_deep() {
assert_eq!(NestedProperties::extract_prefix("a.b.c"), Some("a.b".to_string()));
assert_eq!(NestedProperties::extract_suffix("a.b.c"), Some("c".to_string()));
assert_eq!(NestedProperties::flatten_key("a.b.c"), "a_b_c".to_string());
}
#[test]
fn test_properties_config_builder_new() {
let builder: PropertiesConfigBuilder<TestConfig> = PropertiesConfigBuilder::new();
assert!(
format!("{:?}", std::any::type_name::<PropertiesConfigBuilder<TestConfig>>()).len() > 0
);
}
#[test]
fn test_properties_config_load_from_config_empty() {
let config = Config::new();
let result = TestConfig::load_from_config(&config);
assert!(result.is_err());
}
#[test]
fn test_properties_config_load_or_default_with_default() {
#[derive(Debug, Clone, Default, serde::Deserialize)]
struct DefaultableConfig {
name: String,
}
impl PropertiesConfig for DefaultableConfig {
fn prefix() -> &'static str {
"defaultable"
}
}
let config = Config::new();
let result = DefaultableConfig::load_or_default(&config);
assert_eq!(result.name, "");
}
#[test]
fn test_properties_config_validate() {
let tc = TestConfig {
value: "test".to_string(),
};
assert!(tc.validate().is_ok());
}
}