use crate::secret::SecretString;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
pub type ConfigMap = BTreeMap<String, ConfigValue>;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConfigValue {
String(String),
Secret(SecretString),
Object(ConfigMap),
Json(JsonValue),
}
impl ConfigValue {
pub fn secret(value: impl Into<String>) -> Self {
Self::Secret(SecretString::new(value))
}
pub fn string(value: impl Into<String>) -> Self {
Self::String(value.into())
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
Self::Secret(s) => Some(s.expose()),
Self::Json(JsonValue::String(s)) => Some(s),
_ => None,
}
}
#[must_use]
pub fn into_json(self) -> JsonValue {
match self {
Self::String(s) => JsonValue::String(s),
Self::Secret(s) => JsonValue::String(s.expose().to_string()),
Self::Object(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map {
obj.insert(k, v.into_json());
}
JsonValue::Object(obj)
}
Self::Json(v) => v,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SecretRef {
pub key: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub provider: Option<String>,
}
#[must_use]
pub fn map_to_json(map: ConfigMap) -> JsonValue {
let mut obj = serde_json::Map::new();
for (k, v) in map {
obj.insert(k, v.into_json());
}
JsonValue::Object(obj)
}
pub fn merge_maps(base: &mut ConfigMap, overlay: ConfigMap) {
for (k, v) in overlay {
match (base.get_mut(&k), v) {
(Some(ConfigValue::Object(dst)), ConfigValue::Object(src)) => {
merge_maps(dst, src);
}
(_, other) => {
base.insert(k, other);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_nested_overlay_wins() {
let mut base = ConfigMap::new();
let mut inner = ConfigMap::new();
inner.insert("a".into(), ConfigValue::string("1"));
base.insert("obj".into(), ConfigValue::Object(inner));
let mut overlay_inner = ConfigMap::new();
overlay_inner.insert("a".into(), ConfigValue::string("2"));
overlay_inner.insert("b".into(), ConfigValue::string("3"));
let mut overlay = ConfigMap::new();
overlay.insert("obj".into(), ConfigValue::Object(overlay_inner));
merge_maps(&mut base, overlay);
match &base["obj"] {
ConfigValue::Object(o) => {
assert_eq!(o["a"].as_str(), Some("2"));
assert_eq!(o["b"].as_str(), Some("3"));
}
_ => panic!("expected object"),
}
}
#[test]
fn map_to_json_flattens_secrets() {
let mut map = ConfigMap::new();
map.insert("database_url".into(), ConfigValue::secret("s"));
let json = map_to_json(map);
assert_eq!(json["database_url"], "s");
}
}