Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Cargo.lock

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

13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ description = """
keywords = ["serde", "env", "serialization", "deserialization"]

[dependencies]
cfg-if = "1.0"
dotenvy = "0.15"
envy = "0.4"
indexmap = { version = "2.8.0", optional = true, features = ["serde"] }
log = { version = "0.4", optional = true }
serde = { version = "1.0", features = ["derive"] }
envy = "0.4"
dotenvy = "0.15"

[dev-dependencies]
tempfile = "3.19"

[features]
debug = ["dep:log"]
debug = ["dep:log"]

# Make serde_envfile::Value use a representation which maintains insertion order.
# This allows data to be read into a Value and written back to a envfile string
# while preserving the order of map keys in the input.
preserve_order = ["dep:indexmap"]
102 changes: 76 additions & 26 deletions src/value.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use std::ops::{Deref, DerefMut};
use std::collections::HashMap;
//! The Value object, a loosely typed way of representing any valid envfile content.

use serde::{Deserialize, Serialize};
cfg_if::cfg_if! {
if #[cfg(feature = "preserve_order")] {
use indexmap::IndexMap as Map;
} else {
// std::collections::HashMap vs hashbrown::HashMap
// https://users.rust-lang.org/t/hashmap-and-hashbrown/114535/2
use std::collections::HashMap as Map;
}
}

/// Flexible representation of environment variables.
///
Expand All @@ -19,49 +26,92 @@ use serde::{Deserialize, Serialize};
/// Ok(())
/// }
/// ```
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
pub struct Value {
#[serde(flatten)]
inner: HashMap<String, String>,
}
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Value(Map<String, String>);

impl Value {
/// Create an empty [`Value`].
///
/// Internally, the [`Value`] object uses a map to store the key-value pairs.
pub fn new() -> Self {
Self {
inner: HashMap::new(),
}
Self(Default::default())
}
}

impl Default for Value {
fn default() -> Self {
Self::new()
}
}

impl Deref for Value {
type Target = HashMap<String, String>;
impl std::ops::Deref for Value {
type Target = Map<String, String>;

fn deref(&self) -> &Self::Target {
&self.inner
&self.0
}
}

impl DerefMut for Value {
impl std::ops::DerefMut for Value {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
&mut self.0
}
}

impl<K,V> FromIterator<(K,V)> for Value
where
K: Into<String>,
V: Into<String>,
{
/// Create a new [`Value`] from an iterator of key-value pairs.
///
/// # Example
///
/// ```rust
/// use serde_envfile::Value;
///
/// let env = Value::from_iter([("KEY1", "VALUE1"), ("KEY2", "VALUE2")]);
/// # assert_eq!(env.get("KEY1").unwrap(), "VALUE1");
/// # assert_eq!(env.get("KEY2").unwrap(), "VALUE2");
/// ```
///
fn from_iter<T: IntoIterator<Item = (K,V)>>(iter: T) -> Self {
let iter = iter.into_iter().map(|(k,v)| (k.into(), v.into()));
Self(FromIterator::from_iter(iter))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{from_str, to_string};
use super::Value;
use crate::{de::from_str, ser::to_string};

#[test]
fn to_env_test() {
let mut env = Value::new();
env.insert("serde_envfile".into(), "HELLO WORLD".into());
fn value_to_string() {
//* Given
let env = Value::from_iter([("KEY1", "VALUE1"), ("KEY2", "VALUE2")]);

//* When
let value_serialized = to_string(&env).expect("Failed to convert Value to String");
let value_deserialized = from_str::<Value>(&value_serialized).expect("Failed to deserialize Value");

let s = to_string(&env).unwrap();
let expected = "SERDE_ENVFILE=\"HELLO WORLD\"";
assert_eq!(expected, s);
//* Then
// Assert that both expected lines are present
// The order of keys in the serialized output is not guaranteed without the `preserve_order` feature
assert!(value_serialized.contains(r#"KEY1="VALUE1""#));
assert!(value_serialized.contains(r#"KEY2="VALUE2""#));

let d: Value = from_str(&s).unwrap();
// Assert the deserialize output follows the order of the original input
// when the `preserve_order` feature is enabled
#[cfg(feature = "preserve_order")]
{
let expected_serialized = "KEY1=\"VALUE1\"\nKEY2=\"VALUE2\"";
assert_eq!(value_serialized, expected_serialized);
}

assert_eq!(env, d);
// Create a new Value with lowercase keys to match the deserialization behavior
let expected_deserialized = Value::from_iter([("key1", "VALUE1"), ("key2", "VALUE2")]);
assert_eq!(value_deserialized, expected_deserialized);
}
}