use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigValue<T>
where
T: Serialize + DeserializeOwned + Clone,
{
Secret { name: String },
EnvironmentVariable {
name: String,
default: Option<String>,
},
Static(T),
}
pub type ConfigValueString = ConfigValue<String>;
pub type ConfigValueU16 = ConfigValue<u16>;
pub type ConfigValueU32 = ConfigValue<u32>;
pub type ConfigValueU64 = ConfigValue<u64>;
pub type ConfigValueUsize = ConfigValue<usize>;
pub type ConfigValueBool = ConfigValue<bool>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueString)]
pub struct ConfigValueStringSchema(pub ConfigValueString);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueU16)]
pub struct ConfigValueU16Schema(pub ConfigValueU16);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueU32)]
pub struct ConfigValueU32Schema(pub ConfigValueU32);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueU64)]
pub struct ConfigValueU64Schema(pub ConfigValueU64);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueUsize)]
pub struct ConfigValueUsizeSchema(pub ConfigValueUsize);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[schema(as = ConfigValueBool)]
pub struct ConfigValueBoolSchema(pub ConfigValueBool);
impl<T> Serialize for ConfigValue<T>
where
T: Serialize + DeserializeOwned + Clone,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
match self {
ConfigValue::Secret { name } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("kind", "Secret")?;
map.serialize_entry("name", name)?;
map.end()
}
ConfigValue::EnvironmentVariable { name, default } => {
let size = if default.is_some() { 3 } else { 2 };
let mut map = serializer.serialize_map(Some(size))?;
map.serialize_entry("kind", "EnvironmentVariable")?;
map.serialize_entry("name", name)?;
if let Some(d) = default {
map.serialize_entry("default", d)?;
}
map.end()
}
ConfigValue::Static(value) => value.serialize(serializer),
}
}
}
impl<'de, T> Deserialize<'de> for ConfigValue<T>
where
T: Serialize + DeserializeOwned + Clone + 'static,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
use serde_json::Value;
let value = Value::deserialize(deserializer)?;
if let Value::Object(ref map) = value {
if let Some(Value::String(kind)) = map.get("kind") {
match kind.as_str() {
"Secret" => {
let name = map
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| D::Error::missing_field("name"))?
.to_string();
return Ok(ConfigValue::Secret { name });
}
"EnvironmentVariable" => {
let name = map
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| D::Error::missing_field("name"))?
.to_string();
let default = map
.get("default")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
return Ok(ConfigValue::EnvironmentVariable { name, default });
}
_ => {
return Err(D::Error::custom(format!("Unknown kind: {kind}")));
}
}
}
}
if let Value::String(s) = &value {
if let Some(env_ref) = parse_posix_env_var(s) {
return Ok(env_ref);
}
}
let static_value: T = serde_json::from_value(value)
.map_err(|e| D::Error::custom(format!("Failed to deserialize as static value: {e}")))?;
Ok(ConfigValue::Static(static_value))
}
}
fn parse_posix_env_var<T>(s: &str) -> Option<ConfigValue<T>>
where
T: Clone + Serialize + DeserializeOwned,
{
if !s.starts_with("${") || !s.ends_with('}') {
return None;
}
let inner = &s[2..s.len() - 1];
if let Some(colon_pos) = inner.find(":-") {
let name = inner[..colon_pos].to_string();
let default = Some(inner[colon_pos + 2..].to_string());
Some(ConfigValue::EnvironmentVariable { name, default })
} else {
let name = inner.to_string();
Some(ConfigValue::EnvironmentVariable {
name,
default: None,
})
}
}
impl<T> Default for ConfigValue<T>
where
T: Serialize + DeserializeOwned + Clone + Default,
{
fn default() -> Self {
ConfigValue::Static(T::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_static_string() {
let json = r#""hello""#;
let value: ConfigValue<String> = serde_json::from_str(json).expect("deserialize");
assert_eq!(value, ConfigValue::Static("hello".to_string()));
}
#[test]
fn test_deserialize_static_number() {
let json = r#"5432"#;
let value: ConfigValue<u16> = serde_json::from_str(json).expect("deserialize");
assert_eq!(value, ConfigValue::Static(5432));
}
#[test]
fn test_deserialize_posix_with_default() {
let json = r#""${DB_PORT:-5432}""#;
let value: ConfigValue<String> = serde_json::from_str(json).expect("deserialize");
match value {
ConfigValue::EnvironmentVariable { name, default } => {
assert_eq!(name, "DB_PORT");
assert_eq!(default, Some("5432".to_string()));
}
_ => panic!("Expected EnvironmentVariable"),
}
}
#[test]
fn test_deserialize_posix_without_default() {
let json = r#""${DB_HOST}""#;
let value: ConfigValue<String> = serde_json::from_str(json).expect("deserialize");
match value {
ConfigValue::EnvironmentVariable { name, default } => {
assert_eq!(name, "DB_HOST");
assert_eq!(default, None);
}
_ => panic!("Expected EnvironmentVariable"),
}
}
#[test]
fn test_deserialize_structured_secret() {
let json = r#"{"kind": "Secret", "name": "db-password"}"#;
let value: ConfigValue<String> = serde_json::from_str(json).expect("deserialize");
match value {
ConfigValue::Secret { name } => assert_eq!(name, "db-password"),
_ => panic!("Expected Secret"),
}
}
#[test]
fn test_deserialize_structured_env() {
let json = r#"{"kind": "EnvironmentVariable", "name": "DB_HOST", "default": "localhost"}"#;
let value: ConfigValue<String> = serde_json::from_str(json).expect("deserialize");
match value {
ConfigValue::EnvironmentVariable { name, default } => {
assert_eq!(name, "DB_HOST");
assert_eq!(default, Some("localhost".to_string()));
}
_ => panic!("Expected EnvironmentVariable"),
}
}
#[test]
fn test_serialize_static() {
let value = ConfigValue::Static("hello".to_string());
let json = serde_json::to_string(&value).expect("serialize");
assert_eq!(json, r#""hello""#);
}
#[test]
fn test_serialize_env_var() {
let value: ConfigValue<String> = ConfigValue::EnvironmentVariable {
name: "DB_HOST".to_string(),
default: Some("localhost".to_string()),
};
let json = serde_json::to_string(&value).expect("serialize");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse json");
assert_eq!(parsed["kind"], "EnvironmentVariable");
assert_eq!(parsed["name"], "DB_HOST");
assert_eq!(parsed["default"], "localhost");
}
#[test]
fn test_serialize_secret() {
let value: ConfigValue<String> = ConfigValue::Secret {
name: "my-secret".to_string(),
};
let json = serde_json::to_string(&value).expect("serialize");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse json");
assert_eq!(parsed["kind"], "Secret");
assert_eq!(parsed["name"], "my-secret");
}
#[test]
fn test_roundtrip_static() {
let original = ConfigValue::Static(42u16);
let json = serde_json::to_string(&original).expect("serialize");
let deserialized: ConfigValue<u16> = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, deserialized);
}
#[test]
fn test_default() {
let value: ConfigValue<String> = ConfigValue::default();
assert_eq!(value, ConfigValue::Static(String::default()));
}
}