use crate::{ConfigError, ConfigResult, PropertySource, Value};
use indexmap::IndexMap;
use std::fmt;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Profile(String);
impl Profile {
pub fn new(name: impl Into<String>) -> Self {
Profile(name.into())
}
pub fn dev() -> Self {
Profile("dev".to_string())
}
pub fn test() -> Self {
Profile("test".to_string())
}
pub fn staging() -> Self {
Profile("staging".to_string())
}
pub fn prod() -> Self {
Profile("prod".to_string())
}
pub fn name(&self) -> &str {
&self.0
}
pub fn is_default(&self) -> bool {
self.0 == "default"
}
}
impl fmt::Display for Profile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for Profile {
fn from(s: String) -> Self {
Profile(s)
}
}
impl From<&str> for Profile {
fn from(s: &str) -> Self {
Profile(s.to_string())
}
}
#[derive(Debug, Clone)]
pub struct ActiveProfiles {
profiles: Vec<Profile>,
default_profiles: Vec<Profile>,
}
impl ActiveProfiles {
pub fn new() -> Self {
Self {
profiles: vec![Profile::dev()],
default_profiles: vec![Profile("default".to_string())],
}
}
pub fn set_active(&mut self, profiles: Vec<Profile>) {
self.profiles = profiles;
}
pub fn add_active(&mut self, profile: Profile) {
if !self.profiles.contains(&profile) {
self.profiles.push(profile);
}
}
pub fn active(&self) -> &[Profile] {
&self.profiles
}
pub fn is_active(&self, profile: &Profile) -> bool {
self.profiles.contains(profile) || self.default_profiles.contains(profile)
}
pub fn set_defaults(&mut self, profiles: Vec<Profile>) {
self.default_profiles = profiles;
}
pub fn defaults(&self) -> &[Profile] {
&self.default_profiles
}
}
impl Default for ActiveProfiles {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Environment {
property_sources: Arc<RwLock<Vec<PropertySource>>>,
active_profiles: Arc<RwLock<ActiveProfiles>>,
system_env: IndexMap<String, String>,
}
impl Environment {
pub fn new() -> Self {
Self {
property_sources: Arc::new(RwLock::new(Vec::new())),
active_profiles: Arc::new(RwLock::new(ActiveProfiles::new())),
system_env: std::env::vars().collect(),
}
}
pub fn add_property_source(&self, source: PropertySource) {
let mut sources = self
.property_sources
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sources.push(source);
}
pub fn add_property_source_first(&self, source: PropertySource) {
let mut sources = self
.property_sources
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sources.insert(0, source);
}
pub fn get_property(&self, key: &str) -> Option<Value> {
let sources = self
.property_sources
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for source in sources.iter() {
if let Some(value) = source.get(key) {
return Some(value);
}
}
None
}
pub fn get_property_as<T>(&self, key: &str) -> ConfigResult<T>
where
T: serde::de::DeserializeOwned,
{
let value = self
.get_property(key)
.ok_or_else(|| ConfigError::MissingProperty(key.to_string()))?;
value.into::<T>()
}
pub fn get_required_property(&self, key: &str) -> ConfigResult<Value> {
self.get_property(key)
.ok_or_else(|| ConfigError::MissingProperty(key.to_string()))
}
pub fn get_required_property_as<T>(&self, key: &str) -> ConfigResult<T>
where
T: serde::de::DeserializeOwned,
{
let value = self.get_required_property(key)?;
value.into::<T>()
}
pub fn contains_property(&self, key: &str) -> bool {
self.get_property(key).is_some()
}
pub fn resolve_placeholders(&self, input: &str) -> String {
let mut result = input.to_string();
let mut start = 0;
while let Some(pos) = result[start..].find("${") {
let absolute_pos = start + pos;
if let Some(end) = result[absolute_pos..].find('}') {
let key = &result[absolute_pos + 2..absolute_pos + end];
if let Some(value) = self.get_property(key) {
let value_str = value.as_str().unwrap_or_default();
result.replace_range(absolute_pos..=(absolute_pos + end), value_str);
}
start = absolute_pos + 1;
} else {
break;
}
}
result
}
pub fn get_active_profiles(&self) -> Vec<Profile> {
let profiles = self
.active_profiles
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
profiles.active().to_vec()
}
pub fn set_active_profiles(&self, profiles: Vec<Profile>) {
let mut active = self
.active_profiles
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
active.set_active(profiles);
}
pub fn add_active_profile(&self, profile: Profile) {
let mut active = self
.active_profiles
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
active.add_active(profile);
}
pub fn accepts_profiles(&self, profiles: &[Profile]) -> bool {
let active = self
.active_profiles
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
profiles.iter().any(|p| active.is_active(p))
}
pub fn get_property_sources(&self) -> Vec<PropertySource> {
let sources = self
.property_sources
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
sources.clone()
}
pub fn get_env(&self, key: &str) -> Option<String> {
self.system_env.get(key).cloned()
}
}
impl Default for Environment {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_new() {
let p = Profile::new("custom");
assert_eq!(p.name(), "custom");
assert!(!p.is_default());
}
#[test]
fn test_profile_presets() {
assert_eq!(Profile::dev().name(), "dev");
assert_eq!(Profile::test().name(), "test");
assert_eq!(Profile::staging().name(), "staging");
assert_eq!(Profile::prod().name(), "prod");
}
#[test]
fn test_profile_is_default() {
assert!(Profile::new("default").is_default());
assert!(!Profile::dev().is_default());
}
#[test]
fn test_profile_display() {
assert_eq!(format!("{}", Profile::dev()), "dev");
assert_eq!(format!("{}", Profile::new("staging")), "staging");
}
#[test]
fn test_profile_from() {
let p1: Profile = "test".into();
let p2: Profile = String::from("prod").into();
assert_eq!(p1.name(), "test");
assert_eq!(p2.name(), "prod");
}
#[test]
fn test_profile_eq_and_ord() {
assert_eq!(Profile::dev(), Profile::new("dev"));
assert_ne!(Profile::dev(), Profile::prod());
assert!(Profile::dev() < Profile::prod());
}
#[test]
fn test_active_profiles_default() {
let ap = ActiveProfiles::new();
assert_eq!(ap.active().len(), 1);
assert_eq!(ap.active()[0], Profile::dev());
}
#[test]
fn test_active_profiles_set_active() {
let mut ap = ActiveProfiles::new();
ap.set_active(vec![Profile::prod()]);
assert_eq!(ap.active().len(), 1);
assert_eq!(ap.active()[0], Profile::prod());
}
#[test]
fn test_active_profiles_add_no_duplicate() {
let mut ap = ActiveProfiles::new();
ap.add_active(Profile::dev());
assert_eq!(ap.active().len(), 1); }
#[test]
fn test_active_profiles_add_new() {
let mut ap = ActiveProfiles::new();
ap.add_active(Profile::prod());
assert_eq!(ap.active().len(), 2);
}
#[test]
fn test_active_profiles_is_active() {
let ap = ActiveProfiles::new();
assert!(ap.is_active(&Profile::dev()));
assert!(ap.is_active(&Profile::new("default"))); assert!(!ap.is_active(&Profile::prod()));
}
#[test]
fn test_active_profiles_defaults() {
let mut ap = ActiveProfiles::new();
assert_eq!(ap.defaults().len(), 1);
assert_eq!(ap.defaults()[0], Profile::new("default"));
ap.set_defaults(vec![Profile::new("base")]);
assert_eq!(ap.defaults().len(), 1);
assert_eq!(ap.defaults()[0], Profile::new("base"));
}
#[test]
fn test_environment_new() {
let env = Environment::new();
assert!(env.get_active_profiles().len() >= 1); assert!(env.get_property_sources().is_empty());
}
#[test]
fn test_environment_add_and_get() {
let env = Environment::new();
let mut source = PropertySource::new("test");
source.put("server.port", Value::integer(8080));
source.put("server.host", Value::string("localhost"));
env.add_property_source(source);
assert_eq!(env.get_property("server.port").unwrap().as_i64(), Some(8080));
assert_eq!(env.get_property("server.host").unwrap().as_str(), Some("localhost"));
assert!(env.get_property("nonexistent").is_none());
}
#[test]
fn test_environment_add_first_priority() {
let env = Environment::new();
let mut source1 = PropertySource::new("source1");
source1.put("key", Value::string("from_source1"));
env.add_property_source(source1);
let mut source2 = PropertySource::new("source2");
source2.put("key", Value::string("from_source2"));
env.add_property_source_first(source2);
assert_eq!(env.get_property("key").unwrap().as_str(), Some("from_source2"));
}
#[test]
fn test_environment_get_property_as() {
let env = Environment::new();
let mut source = PropertySource::new("test");
source.put("count", Value::integer(42));
env.add_property_source(source);
let result: i64 = env.get_property_as("count").unwrap();
assert_eq!(result, 42);
}
#[test]
fn test_environment_get_property_as_missing() {
let env = Environment::new();
let result: Result<i64, _> = env.get_property_as("missing");
assert!(result.is_err());
}
#[test]
fn test_environment_required_property() {
let env = Environment::new();
let mut source = PropertySource::new("test");
source.put("present", Value::string("here"));
env.add_property_source(source);
assert!(env.get_required_property("present").is_ok());
assert!(env.get_required_property("absent").is_err());
}
#[test]
fn test_environment_contains_property() {
let env = Environment::new();
assert!(!env.contains_property("key"));
let mut source = PropertySource::new("test");
source.put("key", Value::string("value"));
env.add_property_source(source);
assert!(env.contains_property("key"));
}
#[test]
fn test_environment_resolve_placeholders() {
let env = Environment::new();
let mut source = PropertySource::new("test");
source.put("host", Value::string("localhost"));
source.put("port", Value::string("8080"));
env.add_property_source(source);
let result = env.resolve_placeholders("server at ${host}:${port}");
assert_eq!(result, "server at localhost:8080");
}
#[test]
fn test_environment_resolve_placeholders_unresolved() {
let env = Environment::new();
let result = env.resolve_placeholders("missing ${no.key} stays");
assert_eq!(result, "missing ${no.key} stays");
}
#[test]
fn test_environment_profiles() {
let env = Environment::new();
env.set_active_profiles(vec![Profile::prod(), Profile::staging()]);
let profiles = env.get_active_profiles();
assert_eq!(profiles.len(), 2);
assert!(profiles.contains(&Profile::prod()));
assert!(profiles.contains(&Profile::staging()));
}
#[test]
fn test_environment_add_profile() {
let env = Environment::new();
env.add_active_profile(Profile::test());
let profiles = env.get_active_profiles();
assert!(profiles.contains(&Profile::test()));
}
#[test]
fn test_environment_accepts_profiles() {
let env = Environment::new();
assert!(env.accepts_profiles(&[Profile::dev()]));
assert!(!env.accepts_profiles(&[Profile::prod()]));
}
#[test]
fn test_environment_get_property_sources() {
let env = Environment::new();
let source1 = PropertySource::new("s1");
let source2 = PropertySource::new("s2");
env.add_property_source(source1);
env.add_property_source(source2);
let sources = env.get_property_sources();
assert_eq!(sources.len(), 2);
}
#[test]
fn test_environment_get_env() {
let env = Environment::new();
assert!(env.get_env("PATH").is_some());
assert!(env.get_env("HIVER_TEST_NONEXISTENT_VAR_12345").is_none());
}
#[test]
fn test_environment_get_required_property_as() {
let env = Environment::new();
let mut source = PropertySource::new("test");
source.put("ratio", Value::float(2.5));
env.add_property_source(source);
let result: f64 = env.get_required_property_as("ratio").unwrap();
assert!((result - 2.5).abs() < f64::EPSILON);
}
}