use std::fmt;
use serde::de::{
self, DeserializeSeed, Deserializer, IntoDeserializer, SeqAccess, Visitor,
value::StrDeserializer,
};
use crate::error::DotCfgError;
pub(crate) fn from_env_str<T>(var: &str, raw: &str) -> Result<T, DotCfgError>
where
T: serde::de::DeserializeOwned,
{
T::deserialize(EnvStr { raw })
.map_err(|err| DotCfgError::EnvParse(var.to_string(), err.to_string()))
}
#[derive(Debug)]
pub(crate) struct EnvError(String);
impl fmt::Display for EnvError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for EnvError {}
impl de::Error for EnvError {
fn custom<T: fmt::Display>(msg: T) -> Self {
EnvError(msg.to_string())
}
}
struct EnvStr<'a> {
raw: &'a str,
}
impl EnvStr<'_> {
fn parse<T>(&self, what: &str) -> Result<T, EnvError>
where
T: std::str::FromStr,
{
self.raw
.trim()
.parse()
.map_err(|_| EnvError(format!("expected {}, got '{}'", what, self.raw)))
}
fn unsupported<T>(&self, what: &str) -> Result<T, EnvError> {
Err(EnvError(format!(
"cannot read {} from an environment variable; use nested keys instead",
what
)))
}
}
macro_rules! parsed {
($method:ident, $visit:ident, $ty:ty) => {
fn $method<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
let parsed: $ty = self.parse(stringify!($ty))?;
visitor.$visit(parsed)
}
};
}
impl<'de> Deserializer<'de> for EnvStr<'_> {
type Error = EnvError;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_str(self.raw)
}
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
match self.raw.trim().to_ascii_lowercase().as_str() {
"true" | "1" => visitor.visit_bool(true),
"false" | "0" => visitor.visit_bool(false),
_ => Err(EnvError(format!("expected bool, got '{}'", self.raw))),
}
}
parsed!(deserialize_i8, visit_i8, i8);
parsed!(deserialize_i16, visit_i16, i16);
parsed!(deserialize_i32, visit_i32, i32);
parsed!(deserialize_i64, visit_i64, i64);
parsed!(deserialize_i128, visit_i128, i128);
parsed!(deserialize_u8, visit_u8, u8);
parsed!(deserialize_u16, visit_u16, u16);
parsed!(deserialize_u32, visit_u32, u32);
parsed!(deserialize_u64, visit_u64, u64);
parsed!(deserialize_u128, visit_u128, u128);
parsed!(deserialize_f32, visit_f32, f32);
parsed!(deserialize_f64, visit_f64, f64);
parsed!(deserialize_char, visit_char, char);
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_str(self.raw)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_str(self.raw)
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_bytes(self.raw.as_bytes())
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_bytes(self.raw.as_bytes())
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_some(self)
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_newtype_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
let parts: Vec<&str> = if self.raw.trim().is_empty() {
Vec::new()
} else {
self.raw.split(',').map(str::trim).collect()
};
visitor.visit_seq(CommaSeparated {
iter: parts.into_iter(),
})
}
fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
self.deserialize_seq(visitor)
}
fn deserialize_tuple_struct<V>(
self,
_name: &'static str,
_len: usize,
visitor: V,
) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
self.deserialize_seq(visitor)
}
fn deserialize_map<V>(self, _visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
self.unsupported("a map")
}
fn deserialize_struct<V>(
self,
name: &'static str,
_fields: &'static [&'static str],
_visitor: V,
) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
self.unsupported(&format!("struct `{name}`"))
}
fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
let de: StrDeserializer<'_, EnvError> = self.raw.trim().into_deserializer();
visitor.visit_enum(de)
}
fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_str(self.raw)
}
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, EnvError>
where
V: Visitor<'de>,
{
visitor.visit_unit()
}
}
struct CommaSeparated<'a> {
iter: std::vec::IntoIter<&'a str>,
}
impl<'de> SeqAccess<'de> for CommaSeparated<'_> {
type Error = EnvError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, EnvError>
where
T: DeserializeSeed<'de>,
{
match self.iter.next() {
Some(part) => seed.deserialize(EnvStr { raw: part }).map(Some),
None => Ok(None),
}
}
fn size_hint(&self) -> Option<usize> {
Some(self.iter.len())
}
}
#[cfg(test)]
mod tests {
use super::from_env_str;
#[test]
fn primitives() {
assert_eq!(from_env_str::<String>("V", "hello").unwrap(), "hello");
assert_eq!(from_env_str::<u16>("V", "9000").unwrap(), 9000);
assert_eq!(from_env_str::<i32>("V", "-3").unwrap(), -3);
assert_eq!(from_env_str::<f64>("V", "0.75").unwrap(), 0.75);
assert_eq!(from_env_str::<u16>("V", " 9000 ").unwrap(), 9000);
}
#[test]
fn bools() {
for raw in ["true", "TRUE", "1"] {
assert!(from_env_str::<bool>("V", raw).unwrap(), "{raw}");
}
for raw in ["false", "False", "0"] {
assert!(!from_env_str::<bool>("V", raw).unwrap(), "{raw}");
}
assert!(from_env_str::<bool>("V", "yes").is_err());
}
#[test]
fn comma_separated_sequences() {
assert_eq!(
from_env_str::<Vec<String>>("V", "cli,fast").unwrap(),
vec!["cli".to_string(), "fast".to_string()]
);
assert_eq!(
from_env_str::<Vec<String>>("V", "cli, fast").unwrap(),
vec!["cli".to_string(), "fast".to_string()]
);
assert_eq!(
from_env_str::<Vec<i32>>("V", "1,2,3").unwrap(),
vec![1, 2, 3]
);
assert_eq!(
from_env_str::<Vec<String>>("V", "").unwrap(),
Vec::<String>::new()
);
assert!(from_env_str::<Vec<i32>>("V", "1,nope").is_err());
}
#[test]
fn options_and_errors() {
assert_eq!(
from_env_str::<Option<u16>>("V", "8080").unwrap(),
Some(8080)
);
let err = from_env_str::<u16>("MYAPP_PORT", "notanumber").unwrap_err();
assert!(err.to_string().contains("MYAPP_PORT"), "{err}");
assert!(err.to_string().contains("notanumber"), "{err}");
}
#[test]
fn structs_are_rejected_with_a_clear_error() {
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct Server {
host: String,
}
let err = from_env_str::<Server>("V", "host=x").unwrap_err();
assert!(err.to_string().contains("nested keys"), "{err}");
}
}