saluki_config/
space_separated.rs

1//! Serde deserializer for space-separated string lists.
2//!
3//! The Datadog Agent passes some list-typed configuration values as space-separated strings when
4//! using environment variables (for example, `DD_PROXY_NO_PROXY="host1 host2"`), while the same keys
5//! appear as YAML sequences in config files. This module provides a deserializer that accepts
6//! both representations.
7
8use std::fmt;
9
10use serde::de::{self, Deserializer, SeqAccess, Visitor};
11
12/// Deserializes a `Vec<String>` from either a sequence or a space-separated string.
13pub fn deserialize_space_separated_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
14where
15    D: Deserializer<'de>,
16{
17    struct SpaceSeparatedOrSeq;
18
19    impl<'de> Visitor<'de> for SpaceSeparatedOrSeq {
20        type Value = Vec<String>;
21
22        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23            formatter.write_str("a sequence or a space-separated string")
24        }
25
26        fn visit_str<E: de::Error>(self, v: &str) -> Result<Vec<String>, E> {
27            Ok(v.split_whitespace().map(str::to_owned).collect())
28        }
29
30        fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<String>, A::Error> {
31            let mut values = Vec::new();
32            while let Some(v) = seq.next_element()? {
33                values.push(v);
34            }
35            Ok(values)
36        }
37    }
38
39    deserializer.deserialize_any(SpaceSeparatedOrSeq)
40}
41
42/// Deserializes an `Option<Vec<String>>` from either a sequence or a space-separated string.
43///
44/// Pair with `#[serde(default)]` so that absent fields deserialize to `None`.
45pub fn deserialize_opt_space_separated_or_seq<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
46where
47    D: Deserializer<'de>,
48{
49    deserialize_space_separated_or_seq(deserializer).map(Some)
50}
51
52#[cfg(test)]
53mod tests {
54    use serde::Deserialize;
55    use serde_json::json;
56
57    use super::*;
58
59    #[derive(Deserialize)]
60    struct Holder {
61        #[serde(deserialize_with = "deserialize_space_separated_or_seq")]
62        values: Vec<String>,
63    }
64
65    #[test]
66    fn deserializes_space_separated_strings_and_sequences() {
67        // The `DD_PROXY_NO_PROXY="host1 host2"` env-var form arrives as a single space-separated string, while the same
68        // key in a YAML/JSON config file arrives as a sequence; both must produce the same `Vec<String>`.
69        let cases: &[(&str, serde_json::Value, Vec<&str>)] = &[
70            (
71                "space-separated string",
72                json!("host1 host2 host3"),
73                vec!["host1", "host2", "host3"],
74            ),
75            (
76                "surrounding and repeated whitespace collapses",
77                json!("  host1   host2  "),
78                vec!["host1", "host2"],
79            ),
80            ("empty string yields an empty list", json!(""), vec![]),
81            (
82                "sequence form is preserved verbatim",
83                json!(["host1", "host2"]),
84                vec!["host1", "host2"],
85            ),
86        ];
87
88        for (description, input, expected) in cases {
89            let holder: Holder =
90                serde_json::from_value(json!({ "values": input })).unwrap_or_else(|e| panic!("{description}: {e}"));
91            assert_eq!(holder.values, *expected, "case: {description}");
92        }
93    }
94}