Skip to main content

securitydept_utils/
ser.rs

1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4use serde_with::{DeserializeAs, SerializeAs};
5
6/// Deserializes a string into Vec<T> by splitting on comma and/or whitespace.
7/// Used with PickFirst to accept either a delimited string or a sequence
8/// (array).
9pub struct CommaOrSpaceSeparated<T>(std::marker::PhantomData<T>);
10
11impl<'de, T> DeserializeAs<'de, Vec<T>> for CommaOrSpaceSeparated<T>
12where
13    T: serde::de::DeserializeOwned,
14{
15    fn deserialize_as<D>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
16    where
17        D: serde::Deserializer<'de>,
18    {
19        let s = String::deserialize(deserializer)?;
20        s.split(|c: char| c == ',' || c.is_whitespace())
21            .map(str::trim)
22            .filter(|s| !s.is_empty())
23            .map(try_parse_maybe_json_string::<T, D>)
24            .collect()
25    }
26}
27
28impl<T> SerializeAs<Vec<T>> for CommaOrSpaceSeparated<T>
29where
30    T: Serialize,
31{
32    fn serialize_as<S>(source: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
33    where
34        S: serde::Serializer,
35    {
36        serialize_delimited_vec(source, " ", serializer)
37    }
38}
39
40pub struct SpaceSeparated<T>(std::marker::PhantomData<T>);
41
42impl<'de, T> DeserializeAs<'de, Vec<T>> for SpaceSeparated<T>
43where
44    T: serde::de::DeserializeOwned,
45{
46    fn deserialize_as<D>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
47    where
48        D: serde::Deserializer<'de>,
49    {
50        let s = String::deserialize(deserializer)?;
51        s.split_whitespace()
52            .map(str::trim)
53            .filter(|s| !s.is_empty())
54            .map(try_parse_maybe_json_string::<T, D>)
55            .collect()
56    }
57}
58
59impl<T> SerializeAs<Vec<T>> for SpaceSeparated<T>
60where
61    T: Serialize,
62{
63    fn serialize_as<S>(source: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
64    where
65        S: serde::Serializer,
66    {
67        serialize_delimited_vec(source, " ", serializer)
68    }
69}
70
71#[cfg(feature = "config-schema")]
72impl<T> serde_with::schemars_1::JsonSchemaAs<Vec<T>> for CommaOrSpaceSeparated<T> {
73    fn schema_name() -> Cow<'static, str> {
74        Cow::Borrowed("String")
75    }
76
77    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
78        <String as schemars::JsonSchema>::json_schema(generator)
79    }
80}
81
82#[cfg(feature = "config-schema")]
83impl<T> serde_with::schemars_1::JsonSchemaAs<Vec<T>> for SpaceSeparated<T> {
84    fn schema_name() -> Cow<'static, str> {
85        Cow::Borrowed("String")
86    }
87
88    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
89        <String as schemars::JsonSchema>::json_schema(generator)
90    }
91}
92
93fn serialize_delimited_vec<T, S>(
94    source: &[T],
95    delimiter: &str,
96    serializer: S,
97) -> Result<S::Ok, S::Error>
98where
99    T: Serialize,
100    S: serde::Serializer,
101{
102    let mut serialized_parts = Vec::with_capacity(source.len());
103
104    for item in source {
105        serialized_parts
106            .push(serde_json::to_string(item).map_err(<S::Error as serde::ser::Error>::custom)?);
107    }
108
109    serializer.serialize_str(&serialized_parts.join(delimiter))
110}
111
112fn try_parse_maybe_json_string<'de, T, D>(s: &str) -> std::result::Result<T, D::Error>
113where
114    T: serde::de::DeserializeOwned,
115    D: serde::Deserializer<'de>,
116{
117    let quoted = if s.starts_with('"') && s.ends_with('"') {
118        Cow::Borrowed(s)
119    } else {
120        Cow::Owned(serde_json::to_string(s).map_err(<D::Error as serde::de::Error>::custom)?)
121    };
122    serde_json::from_str::<T>(quoted.as_ref()).map_err(<D::Error as serde::de::Error>::custom)
123}