From 8813604cbdd2ff43b0a15c91557ff11733ca7b58 Mon Sep 17 00:00:00 2001 From: Lorenzo Delgado Date: Fri, 4 Apr 2025 01:45:30 +0200 Subject: [PATCH] feat: add preserve order feature to value object - Add `preserve_order` crate feature to value object. - Add `Value::from_iter` method to create a `Value` object from an iterator of key-value pairs. - Enhance value module documentation and unit tests. Signed-off-by: Lorenzo Delgado --- Cargo.lock | 25 +++++++++++++ Cargo.toml | 13 +++++-- src/value.rs | 102 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 789be23..dcbeeb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.10" @@ -57,6 +63,23 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + [[package]] name = "libc" version = "0.2.171" @@ -131,8 +154,10 @@ dependencies = [ name = "serde-envfile" version = "0.2.0" dependencies = [ + "cfg-if", "dotenvy", "envy", + "indexmap", "log", "serde", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 045a9ab..61637bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] \ No newline at end of file +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"] diff --git a/src/value.rs b/src/value.rs index c012f30..3845e8f 100644 --- a/src/value.rs +++ b/src/value.rs @@ -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. /// @@ -19,49 +26,92 @@ use serde::{Deserialize, Serialize}; /// Ok(()) /// } /// ``` -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)] -pub struct Value { - #[serde(flatten)] - inner: HashMap, -} +#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct Value(Map); 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; +impl std::ops::Deref for Value { + type Target = Map; + 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 FromIterator<(K,V)> for Value +where + K: Into, + V: Into, +{ + /// 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>(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_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); } }