use core::fmt;
use std::collections::BTreeMap;
use serde::{Deserialize, Deserializer};
pub(super) enum EnvValue {
Str(String),
Bool(bool),
Int(i128),
}
impl fmt::Debug for EnvValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Str(_) => f.write_str("<str>"),
Self::Bool(_) => f.write_str("<bool>"),
Self::Int(_) => f.write_str("<int>"),
}
}
}
impl EnvValue {
#[must_use]
fn into_string(self) -> String {
match self {
Self::Str(s) => s,
Self::Bool(b) => b.to_string(),
Self::Int(n) => n.to_string(),
}
}
}
pub(super) fn deserialize_env<'de, D: Deserializer<'de>>(
de: D,
) -> Result<BTreeMap<String, String>, D::Error> {
Ok(BTreeMap::<String, EnvValue>::deserialize(de)?
.into_iter()
.map(|(k, v)| (k, v.into_string()))
.collect())
}
impl<'de> serde::de::Deserialize<'de> for EnvValue {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct EnvValueVisitor;
impl serde::de::Visitor<'_> for EnvValueVisitor {
type Value = EnvValue;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("a string, boolean, or whole number")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<EnvValue, E> {
Ok(EnvValue::Str(v.to_string()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<EnvValue, E> {
Ok(EnvValue::Str(v))
}
fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<EnvValue, E> {
Ok(EnvValue::Bool(v))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<EnvValue, E> {
Ok(EnvValue::Int(i128::from(v)))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<EnvValue, E> {
Ok(EnvValue::Int(i128::from(v)))
}
fn visit_f64<E: serde::de::Error>(self, _v: f64) -> Result<EnvValue, E> {
Err(E::custom(
"a float env value loses its written form, quote it",
))
}
}
de.deserialize_any(EnvValueVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::testing::*;
#[test]
fn env_value_debug_never_prints_the_value() {
let cases = [
(EnvValue::Str("postgres://secret".to_string()), "<str>"),
(EnvValue::Bool(true), "<bool>"),
(EnvValue::Int(9_223_372_036_854_775_807), "<int>"),
];
for (value, expected) in cases {
assert_eq!(format!("{value:?}"), expected);
}
}
#[test]
fn the_clauses_enforced_outside_normalize_are_the_ones_named() {
let claims = refusal_claims();
let outside: Vec<(&str, &str, &str)> = claims
.iter()
.filter_map(|claim| match claim.proof {
Proof::Elsewhere(place) => Some((claim.field, claim.refusal, place)),
Proof::Refused { .. } => None,
})
.collect();
assert_eq!(
outside,
vec![
(
"env",
"a float, since 1.10 would arrive as 1.1",
"EnvValue's Deserialize, before normalize sees the table",
),
(
"dogs",
"a value that is not a table",
"DogTable's Deserialize, before normalize sees the table",
),
(
"group",
"a name with no group entry",
"shep-daemon's privilege::resolve, at spawn",
),
(
"group",
"another group, unless the shepherd runs as root",
"shep-daemon's privilege::resolve, at spawn",
),
(
"user",
"a name with no passwd entry",
"shep-daemon's privilege::resolve, at spawn",
),
(
"user",
"another user, unless the shepherd runs as root",
"shep-daemon's privilege::resolve, at spawn",
),
]
);
}
}