use crate::Value;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropertySourceType {
CommandLine,
SystemEnvironment,
SystemProperties,
ApplicationProperties,
ApplicationYaml,
ApplicationToml,
External,
Custom,
}
impl PropertySourceType {
pub fn default_order(&self) -> u32 {
match self {
PropertySourceType::CommandLine => 100,
PropertySourceType::SystemEnvironment => 200,
PropertySourceType::SystemProperties => 300,
PropertySourceType::ApplicationProperties => 400,
PropertySourceType::ApplicationYaml => 500,
PropertySourceType::ApplicationToml => 600,
PropertySourceType::External => 700,
PropertySourceType::Custom => 800,
}
}
}
#[derive(Debug, Clone)]
pub struct PropertySource {
name: String,
properties: IndexMap<String, Value>,
source_type: PropertySourceType,
order: u32,
file_path: Option<PathBuf>,
}
impl PropertySource {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
let source_type = Self::infer_source_type(&name);
Self {
name,
properties: IndexMap::new(),
source_type,
order: source_type.default_order(),
file_path: None,
}
}
pub fn with_map(name: impl Into<String>, map: HashMap<String, Value>) -> Self {
let mut source = Self::new(name);
source.properties = map.into_iter().collect();
source
}
fn infer_source_type(name: &str) -> PropertySourceType {
let lower = name.to_lowercase();
if lower.contains("command") || lower.contains("argv") {
PropertySourceType::CommandLine
} else if lower.contains("env") {
PropertySourceType::SystemEnvironment
} else if lower.contains("yaml") || lower.contains("yml") {
PropertySourceType::ApplicationYaml
} else if lower.contains("toml") {
PropertySourceType::ApplicationToml
} else if lower.contains("properties") || lower.contains("props") {
PropertySourceType::ApplicationProperties
} else if lower.contains("external") {
PropertySourceType::External
} else {
PropertySourceType::Custom
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn properties(&self) -> &IndexMap<String, Value> {
&self.properties
}
pub fn source_type(&self) -> PropertySourceType {
self.source_type
}
pub fn order(&self) -> u32 {
self.order
}
pub fn file_path(&self) -> Option<&PathBuf> {
self.file_path.as_ref()
}
pub fn set_file_path(&mut self, path: PathBuf) {
self.file_path = Some(path);
}
pub fn set_order(&mut self, order: u32) {
self.order = order;
}
pub fn put(&mut self, key: impl Into<String>, value: impl Into<Value>) {
self.properties.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<Value> {
self.properties.get(key).cloned()
}
pub fn contains_key(&self, key: &str) -> bool {
self.properties.contains_key(key)
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.properties.shift_remove(key)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.properties.keys()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
self.properties.iter()
}
pub fn len(&self) -> usize {
self.properties.len()
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn merge(&mut self, other: PropertySource) {
for (key, value) in other.properties {
self.properties.insert(key, value);
}
}
}
pub struct PropertySourceBuilder {
source: PropertySource,
}
impl PropertySourceBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
source: PropertySource::new(name),
}
}
pub fn source_type(mut self, source_type: PropertySourceType) -> Self {
self.source.source_type = source_type;
self
}
pub fn order(mut self, order: u32) -> Self {
self.source.order = order;
self
}
pub fn file_path(mut self, path: PathBuf) -> Self {
self.source.file_path = Some(path);
self
}
pub fn put(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
self.source.put(key, value);
self
}
pub fn put_all(&mut self, map: HashMap<String, Value>) -> &mut Self {
for (key, value) in map {
self.source.put(key, value);
}
self
}
pub fn build(self) -> PropertySource {
self.source
}
}
impl Default for PropertySource {
fn default() -> Self {
Self::new("default")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_type_default_order() {
assert_eq!(PropertySourceType::CommandLine.default_order(), 100);
assert_eq!(PropertySourceType::SystemEnvironment.default_order(), 200);
assert_eq!(PropertySourceType::SystemProperties.default_order(), 300);
assert_eq!(PropertySourceType::ApplicationProperties.default_order(), 400);
assert_eq!(PropertySourceType::ApplicationYaml.default_order(), 500);
assert_eq!(PropertySourceType::ApplicationToml.default_order(), 600);
assert_eq!(PropertySourceType::External.default_order(), 700);
assert_eq!(PropertySourceType::Custom.default_order(), 800);
}
#[test]
fn test_source_type_priority_ordering() {
assert!(
PropertySourceType::CommandLine.default_order()
< PropertySourceType::SystemEnvironment.default_order()
);
assert!(
PropertySourceType::SystemEnvironment.default_order()
< PropertySourceType::ApplicationProperties.default_order()
);
assert!(
PropertySourceType::ApplicationProperties.default_order()
< PropertySourceType::Custom.default_order()
);
}
#[test]
fn test_new_property_source() {
let source = PropertySource::new("test-source");
assert_eq!(source.name(), "test-source");
assert_eq!(source.source_type(), PropertySourceType::Custom);
assert!(source.is_empty());
assert_eq!(source.len(), 0);
assert!(source.file_path().is_none());
}
#[test]
fn test_infer_source_type_command_line() {
let source = PropertySource::new("commandArgs");
assert_eq!(source.source_type(), PropertySourceType::CommandLine);
}
#[test]
fn test_infer_source_type_environment() {
let source = PropertySource::new("envVars");
assert_eq!(source.source_type(), PropertySourceType::SystemEnvironment);
}
#[test]
fn test_infer_source_type_yaml() {
let source = PropertySource::new("application-yaml");
assert_eq!(source.source_type(), PropertySourceType::ApplicationYaml);
}
#[test]
fn test_infer_source_type_toml() {
let source = PropertySource::new("config.toml");
assert_eq!(source.source_type(), PropertySourceType::ApplicationToml);
}
#[test]
fn test_infer_source_type_properties() {
let source = PropertySource::new("app-properties");
assert_eq!(source.source_type(), PropertySourceType::ApplicationProperties);
}
#[test]
fn test_infer_source_type_external() {
let source = PropertySource::new("external-config");
assert_eq!(source.source_type(), PropertySourceType::External);
}
#[test]
fn test_with_map() {
let mut map = HashMap::new();
map.insert("key1".to_string(), Value::string("value1"));
map.insert("key2".to_string(), Value::integer(42));
let source = PropertySource::with_map("test-map", map);
assert_eq!(source.len(), 2);
assert!(source.contains_key("key1"));
assert!(source.contains_key("key2"));
assert_eq!(source.get("key1").unwrap().as_str(), Some("value1"));
assert_eq!(source.get("key2").unwrap().as_i64(), Some(42));
}
#[test]
fn test_put_get_remove() {
let mut source = PropertySource::new("test");
assert!(!source.contains_key("name"));
source.put("name", "hiver");
assert!(source.contains_key("name"));
assert_eq!(source.get("name").unwrap().as_str(), Some("hiver"));
source.put("name", "updated");
assert_eq!(source.get("name").unwrap().as_str(), Some("updated"));
let removed = source.remove("name");
assert_eq!(removed.unwrap().as_str(), Some("updated"));
assert!(!source.contains_key("name"));
}
#[test]
fn test_keys_and_iter() {
let mut source = PropertySource::new("test");
source.put("a", 1);
source.put("b", 2);
source.put("c", 3);
let keys: Vec<&String> = source.keys().collect();
assert_eq!(keys.len(), 3);
let entries: Vec<_> = source.iter().collect();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_order_and_file_path() {
let mut source = PropertySource::new("test");
assert_eq!(source.order(), PropertySourceType::Custom.default_order());
source.set_order(50);
assert_eq!(source.order(), 50);
source.set_file_path(PathBuf::from("/etc/hiver/app.yaml"));
assert_eq!(source.file_path(), Some(&PathBuf::from("/etc/hiver/app.yaml")));
}
#[test]
fn test_merge() {
let mut source1 = PropertySource::new("source1");
source1.put("shared", "from_source1");
source1.put("only_in_1", "value1");
let mut source2 = PropertySource::new("source2");
source2.put("shared", "from_source2");
source2.put("only_in_2", "value2");
source1.merge(source2);
assert_eq!(source1.len(), 3);
assert_eq!(source1.get("shared").unwrap().as_str(), Some("from_source2"));
assert_eq!(source1.get("only_in_1").unwrap().as_str(), Some("value1"));
assert_eq!(source1.get("only_in_2").unwrap().as_str(), Some("value2"));
}
#[test]
fn test_default() {
let source = PropertySource::default();
assert_eq!(source.name(), "default");
assert!(source.is_empty());
}
#[test]
fn test_builder_basic() {
let source = PropertySourceBuilder::new("built-source")
.source_type(PropertySourceType::CommandLine)
.order(50)
.file_path(PathBuf::from("/tmp/config"))
.build();
assert_eq!(source.name(), "built-source");
assert_eq!(source.source_type(), PropertySourceType::CommandLine);
assert_eq!(source.order(), 50);
assert_eq!(source.file_path(), Some(&PathBuf::from("/tmp/config")));
}
#[test]
fn test_builder_put_properties() {
let mut builder = PropertySourceBuilder::new("props");
builder.put("key1", "value1");
builder.put("key2", 42);
let mut extra = HashMap::new();
extra.insert("key3".to_string(), Value::bool(true));
builder.put_all(extra);
let source = builder.build();
assert_eq!(source.len(), 3);
assert_eq!(source.get("key1").unwrap().as_str(), Some("value1"));
assert_eq!(source.get("key2").unwrap().as_i64(), Some(42));
assert_eq!(source.get("key3").unwrap().as_bool(), Some(true));
}
}