Skip to content

Commit

Permalink
Implement the library
Browse files Browse the repository at this point in the history
Signed-off-by: Marcel Müller <[email protected]>
  • Loading branch information
TheNeikos committed Feb 3, 2023
1 parent 203400c commit 83689af
Show file tree
Hide file tree
Showing 5 changed files with 640 additions and 7 deletions.
79 changes: 79 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Deserialize (potentially nested) environment variables into your custom structs"
resolver = "2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = "1.0.152"
thiserror = "1.0.38"

[dev-dependencies]
serde = { version = "1.0.152", features = ["derive"] }
18 changes: 18 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum EnvDeserializationError {
#[error("An error occured during deserialization: {}", .0)]
GenericDeserialization(String),
#[error("An unsupported variant was tried to be deserialized. Only structs and maps are currently supported.")]
UnsupportedValue,
#[error("Tried to nest values while a simple value was expected")]
InvalidNestedValues,
}

impl serde::de::Error for EnvDeserializationError {
fn custom<T>(msg: T) -> Self
where
T: std::fmt::Display,
{
EnvDeserializationError::GenericDeserialization(msg.to_string())
}
}
215 changes: 208 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,215 @@
pub fn add(left: usize, right: usize) -> usize {
left + right
use error::EnvDeserializationError;
use serde::de::{value::StringDeserializer, DeserializeOwned, IntoDeserializer};
use value::Value;

mod error;
mod value;

#[derive(Debug, PartialEq)]
struct Key {
original: String,
current: String,
}

impl AsRef<str> for Key {
fn as_ref(&self) -> &str {
&self.current
}
}

impl Key {
fn new(original: String) -> Self {
Self {
current: original.clone(),
original,
}
}
}

impl<'de> IntoDeserializer<'de, EnvDeserializationError> for Key {
type Deserializer = StringDeserializer<EnvDeserializationError>;
fn into_deserializer(self) -> Self::Deserializer {
self.current.into_deserializer()
}
}

pub enum Prefix<'a> {
None,
Some(&'a str),
}

pub fn from_env<T: DeserializeOwned>(
prefix: Prefix<'_>,
) -> Result<T, error::EnvDeserializationError> {
let env_values = std::env::vars();

from_primitive(env_values.flat_map(|(key, value)| {
if let Prefix::Some(prefix) = prefix {
let stripped_key = key.strip_prefix(prefix).map(|s| s.to_string())?;
Some((Key::new(stripped_key), value))
} else {
Some((Key::new(key), value))
}
}))
}

fn from_primitive<T: DeserializeOwned, I: Iterator<Item = (Key, String)>>(
values: I,
) -> Result<T, error::EnvDeserializationError> {
let deserializer =
Value::from_list(values.map(|(key, val)| (key, Value::Simple(val))).collect()).unwrap();
T::deserialize(deserializer)
}

#[cfg(test)]
mod tests {
use super::*;
mod test {
use serde::Deserialize;

use crate::{from_primitive, Key};

#[test]
fn check_simple_struct() {
#[derive(Debug, PartialEq, Deserialize)]
struct Simple {
allowed: bool,
}

let expected = Simple { allowed: true };

let actual: Simple =
from_primitive([(Key::new(String::from("allowed")), String::from("true"))].into_iter())
.unwrap();

assert_eq!(actual, expected);
}

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn check_double_nested_struct() {
#[derive(Debug, PartialEq, Deserialize)]
struct InnerExtraConfig {
allowed: bool,
}

#[derive(Debug, PartialEq, Deserialize)]
struct InnerConfig {
smoothness: f32,
extra: InnerExtraConfig,
}

#[derive(Debug, PartialEq, Deserialize)]
struct Nested {
temp: u64,
inner: InnerConfig,
}

let expected = Nested {
temp: 15,
inner: InnerConfig {
smoothness: 32.0,
extra: InnerExtraConfig { allowed: false },
},
};

let actual: Nested = from_primitive(
[
(Key::new(String::from("temp")), String::from("15")),
(
Key::new(String::from("inner__smoothness")),
String::from("32.0"),
),
(
Key::new(String::from("inner__extra__allowed")),
String::from("false"),
),
]
.into_iter(),
)
.unwrap();

assert_eq!(actual, expected);
}

#[test]
fn check_renamed_struct() {
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
struct Simple {
allowed_simply: bool,
}

let expected = Simple {
allowed_simply: true,
};

let actual: Simple = from_primitive(
[(
Key::new(String::from("ALLOWED-SIMPLY")),
String::from("true"),
)]
.into_iter(),
)
.unwrap();

assert_eq!(actual, expected);
}

#[test]
fn check_simple_enum() {
#[derive(Debug, PartialEq, Deserialize)]
enum Simple {
Yes,
No,
}

#[derive(Debug, PartialEq, Deserialize)]
struct SimpleEnum {
simple: Simple,
}

let expected = SimpleEnum { simple: Simple::No };

let actual: SimpleEnum =
from_primitive([(Key::new(String::from("simple")), String::from("No"))].into_iter())
.unwrap();

assert_eq!(actual, expected);
}

#[test]
fn check_complex_enum() {
#[derive(Debug, PartialEq, Deserialize)]
enum Complex {
Access { password: String, foo: f32 },
No,
}

#[derive(Debug, PartialEq, Deserialize)]
struct ComplexEnum {
complex: Complex,
}

let expected = ComplexEnum {
complex: Complex::Access {
password: String::from("hunter2"),
foo: 42.0,
},
};

let actual: ComplexEnum = from_primitive(
[
(
Key::new(String::from("complex__Access__password")),
String::from("hunter2"),
),
(
Key::new(String::from("complex__Access__foo")),
String::from("42.0"),
),
]
.into_iter(),
)
.unwrap();

assert_eq!(actual, expected);
}
}
Loading

0 comments on commit 83689af

Please sign in to comment.