diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e888a..72c5eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# 1.2.0 + +- Add support for environment variables in `Toml` arrays + # 1.1.0 - Add support for optional default in `Toml` files diff --git a/Cargo.toml b/Cargo.toml index 8b71519..1424456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "konfiguration" -version = "1.1.0" +version = "1.2.0" rust-version = "1.66.1" description = "Toml configuration loader with environment variables support." documentation = "https://docs.rs/konfiguration/" diff --git a/src/de.rs b/src/de.rs index 2ab8f37..b3295d2 100644 --- a/src/de.rs +++ b/src/de.rs @@ -137,9 +137,25 @@ impl TryFrom> for ConfigurationEntry { type Error = serde_untagged::de::Error; fn try_from(value: Seq) -> Result { - let array: TomlValue = value.deserialize()?; + let vec: Vec = value.deserialize()?; - Ok(ConfigurationEntry::Simple(array)) + let mut entries = Vec::new(); + + for toml in vec { + if let TomlValue::Table(t) = toml { + let str = toml::to_string(&t).map_err(|e| de::Error::custom(e.to_string()))?; + + let entry = ConfigurationEntry::Table( + toml::from_str(&str).map_err(|e| de::Error::custom(e.to_string()))?, + ); + + entries.push(entry); + } else { + entries.push(ConfigurationEntry::Simple(toml)); + } + } + + Ok(ConfigurationEntry::Vec(entries)) } } diff --git a/src/lib.rs b/src/lib.rs index 3b94cce..ec2d0ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ use std::fs; use serde::Deserialize; use toml::de::ValueDeserializer; -use crate::error::KonfigurationResult; +use crate::error::{KonfigurationError, KonfigurationResult}; use crate::value::{ConfigurationEntry, ConfigurationManifest, TomlMap, TomlValue}; mod de; @@ -55,26 +55,58 @@ impl Konfiguration { } } -/// Takes a configuration manifest and simplifies it into a TOML value. -fn simplify(manifest: ConfigurationManifest) -> KonfigurationResult { - let mut map = TomlMap::new(); +impl TryFrom for Option { + type Error = KonfigurationError; - for (key, config_entry) in manifest { - let value = match config_entry { - ConfigurationEntry::Simple(value) => value, - ConfigurationEntry::Env(env_value) => { - env_sanity_check(&env_value); - expand_with_retry(env_value)? + fn try_from(value: ConfigurationEntry) -> Result { + match value { + ConfigurationEntry::Simple(toml) => Ok(Some(toml)), + ConfigurationEntry::Env(env) => { + let toml = expand_with_retry(env)?; + Ok(Some(toml)) + } + ConfigurationEntry::UnsetEnv => Ok(None), + ConfigurationEntry::Vec(vec) => { + let mut entries = Vec::new(); + + for entry in vec { + match Self::try_from(entry)? { + Some(entry) => entries.push(entry), + None => continue, + } + } + + Ok(Some(TomlValue::Array(entries))) } - ConfigurationEntry::UnsetEnv => continue, ConfigurationEntry::Table(table) => { - let simplified = simplify(table)?; + let mut map = TomlMap::new(); + + for (key, entry) in table { + match Self::try_from(entry)? { + Some(entry) => { + map.insert(key, entry); + } + None => continue, + } + } - TomlValue::Table(simplified) + Ok(Some(TomlValue::Table(map))) } - }; + } + } +} + +/// Takes a configuration manifest and simplifies it into a TOML value. +fn simplify(manifest: ConfigurationManifest) -> KonfigurationResult { + let mut map = TomlMap::new(); - map.insert(key, value); + for (key, entry) in manifest { + match Option::::try_from(entry)? { + Some(entry) => { + map.insert(key, entry); + } + None => continue, + } } Ok(map) @@ -92,6 +124,8 @@ fn env_sanity_check(env: &str) { /// This is ugly because toml sometimes fails to deserialize a simple string /// I will be looking into this later. fn expand_with_retry(value: String) -> KonfigurationResult { + env_sanity_check(&value); + match TomlValue::deserialize(ValueDeserializer::new(&value)) { Ok(v) => Ok(v), Err(_) => Ok(TomlValue::String(value)), diff --git a/src/value.rs b/src/value.rs index 29f1721..f537b6a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -10,5 +10,6 @@ pub enum ConfigurationEntry { Simple(TomlValue), Env(String), UnsetEnv, + Vec(Vec), Table(HashMap), } diff --git a/test_files/config.toml b/test_files/config.toml index 203e8bb..0234208 100644 --- a/test_files/config.toml +++ b/test_files/config.toml @@ -34,6 +34,22 @@ hash_with_default = { env = "HASH_WITH_DEFAULT", default = { d = 4, e = 5, f = 6 hash_with_no_default_env_unset = { env = "HASH_WITH_NO_DEFAULT_ENV_UNSET" } hash_with_no_default_env_set = { env = "HASH_WITH_NO_DEFAULT_ENV_SET" } +[[list]] +s = "im a string" +i = 42 +f = 42.42 +b = true +array = [1, 2, 3] +hash = { a = 1, b = 2, c = 3 } + +[[list]] +s_with_no_default = { env = "S_WITH_NO_DEFAULT" } +i = 42 +f = 42.42 +b = true +array = [1, 2, 3] +hash = { a = 1, b = 2, c = 3 } + [nested] s = "im a nested string" i = 42 diff --git a/tests/parse_tests.rs b/tests/parse_tests.rs index 7f59e30..1bc0516 100644 --- a/tests/parse_tests.rs +++ b/tests/parse_tests.rs @@ -40,10 +40,23 @@ pub struct ConfigFileRepresentation { hash_with_no_default_env_unset: Option>, hash_with_no_default_env_set: Option>, + list: Vec, + nested: Nested, nested_do_not_exist: Option, } +#[derive(Debug, serde::Deserialize, PartialEq)] +pub struct List { + s: Option, + s_with_no_default: Option, + i: i32, + f: f32, + b: bool, + array: Vec, + hash: HashMap, +} + #[derive(Debug, serde::Deserialize, PartialEq)] pub struct Nested { s: String, @@ -168,6 +181,25 @@ fn can_parse_configs() { "array with no default env set failed" ); + assert_eq!(config.list[0].s, Some("im a string".to_string())); + assert_eq!(config.list[0].s_with_no_default, None); + assert_eq!(config.list[0].i, 42); + assert_eq!(config.list[0].f, 42.42); + assert_eq!(config.list[0].b, true); + assert_eq!(config.list[0].array, vec![1, 2, 3]); + assert_eq!(config.list[0].hash, config.hash); + + assert_eq!(config.list[1].s, None); + assert_eq!( + config.list[1].s_with_no_default, + Some("im a string".to_string()) + ); + assert_eq!(config.list[1].i, 42); + assert_eq!(config.list[1].f, 42.42); + assert_eq!(config.list[1].b, true); + assert_eq!(config.list[1].array, vec![1, 2, 3]); + assert_eq!(config.list[1].hash, config.hash); + let mut hash = HashMap::new(); hash.insert("a".to_string(), 1); hash.insert("b".to_string(), 2);