use super::secret::{RawSecret, Secret};
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
trait DeserializeExternal: for<'de> Deserialize<'de> {
fn load_from_env(var_name: &str) -> Result<Self, std::env::VarError>;
fn load_from_file(path: &str) -> std::io::Result<Self>;
fn deserialize_from_env<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let var_name = String::deserialize(deserializer)?;
Self::load_from_env(&var_name).map_err(|e| {
D::Error::custom(format!(
"failed to read external data from ${var_name}: {e}"
))
})
}
fn deserialize_from_file<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let path = String::deserialize(deserializer)?;
Self::load_from_file(&path).map_err(|e| {
D::Error::custom(format!("failed to read external data from `{path}`: {e}"))
})
}
}
impl DeserializeExternal for String {
#[inline]
fn load_from_env(var_name: &str) -> Result<Self, std::env::VarError> {
std::env::var(var_name)
}
#[inline]
fn load_from_file(path: &str) -> std::io::Result<Self> {
std::fs::read_to_string(path)
}
}
impl DeserializeExternal for Vec<u8> {
#[inline]
fn load_from_env(var_name: &str) -> Result<Self, std::env::VarError> {
std::env::var(var_name).map(|v| v.into_bytes())
}
#[inline]
fn load_from_file(path: &str) -> std::io::Result<Self> {
std::fs::read(path)
}
}
impl DeserializeExternal for Secret {
#[inline]
fn load_from_env(var_name: &str) -> Result<Self, std::env::VarError> {
std::env::var(var_name).map(Self)
}
#[inline]
fn load_from_file(path: &str) -> std::io::Result<Self> {
std::fs::read_to_string(path).map(Self)
}
}
impl DeserializeExternal for RawSecret {
#[inline]
fn load_from_env(var_name: &str) -> Result<Self, std::env::VarError> {
std::env::var(var_name).map(|v| Self(v.into_bytes()))
}
#[inline]
fn load_from_file(path: &str) -> std::io::Result<Self> {
std::fs::read(path).map(Self)
}
}
fn serialize_as_none<S: Serializer, T>(_: &T, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_none()
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(
rename_all = "snake_case",
bound(deserialize = "T: DeserializeExternal")
)]
#[cfg_attr(
feature = "settings_deny_unknown_fields_by_default",
serde(deny_unknown_fields)
)]
pub enum MaybeExternal<T> {
Data(T),
#[serde(
serialize_with = "serialize_as_none",
deserialize_with = "DeserializeExternal::deserialize_from_env"
)]
Env(T),
#[serde(
serialize_with = "serialize_as_none",
deserialize_with = "DeserializeExternal::deserialize_from_file"
)]
File(T),
}
impl<T: Default> Default for MaybeExternal<T> {
#[inline]
fn default() -> Self {
Self::Data(T::default())
}
}
impl<T> AsRef<T> for MaybeExternal<T> {
#[inline]
fn as_ref(&self) -> &T {
let (Self::Data(v) | Self::Env(v) | Self::File(v)) = self;
v
}
}
impl<T> AsMut<T> for MaybeExternal<T> {
#[inline]
fn as_mut(&mut self) -> &mut T {
let (Self::Data(v) | Self::Env(v) | Self::File(v)) = self;
v
}
}
impl<T: fmt::Display> fmt::Display for MaybeExternal<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self.as_ref(), f)
}
}
impl<T: DeserializeExternal + super::Settings> super::Settings for MaybeExternal<T> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn external_deserializes_from_env() {
if std::env::var("NEXTEST").as_deref() != Ok("1") {
return;
}
unsafe {
std::env::set_var("MY_CUSTOM_ENV_VAR", "my custom value");
}
let yaml = "env: MY_CUSTOM_ENV_VAR\n";
let data: MaybeExternal<String> = crate::settings::from_yaml_str(yaml).unwrap();
assert_eq!(data.as_ref(), "my custom value");
}
#[test]
fn external_deserializes_from_file() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut tmp, b"hello\n").unwrap();
let yaml = format!("file: {}\n", tmp.path().display());
let data: MaybeExternal<String> = crate::settings::from_yaml_str(&yaml).unwrap();
assert_eq!(data.as_ref(), "hello\n");
}
}