Skip to main content

sonic/util/
serde.rs

1// Sonic
2//
3// Fast, lightweight and schema-less search backend
4// Copyright: 2019, Valerian Saliou <valerian@valeriansaliou.name>
5// Copyright: 2026, Rémi Bardon <remi@remibardon.name>
6// License: Mozilla Public License v2.0 (MPL v2.0)
7
8pub(crate) mod none_string_as_none {
9    use serde::{Deserialize, Deserializer};
10
11    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
12    where
13        D: Deserializer<'de>,
14        T: Deserialize<'de>,
15    {
16        // First deserialize into a generic JSON-like value... but to keep this
17        // format-agnostic, easiest is to go through an enum of "string or T".
18        #[derive(Deserialize)]
19        #[serde(untagged)]
20        enum StringOrT<T> {
21            String(String),
22            T(T),
23        }
24
25        match Option::<StringOrT<T>>::deserialize(deserializer)? {
26            None => Ok(None),
27            Some(StringOrT::String(s)) if s.eq_ignore_ascii_case("none") => Ok(None),
28            Some(StringOrT::String(s)) => {
29                // Try to parse the string itself as T (in case T is String-like)
30                // For simple cases you may just want to error here instead.
31                Err(serde::de::Error::custom(format!(
32                    "unexpected string value: {s}"
33                )))
34            }
35            Some(StringOrT::T(v)) => Ok(Some(v)),
36        }
37    }
38}
39
40pub mod env_var {
41    use regex::Regex;
42    use serde::{Deserialize, Deserializer};
43    use std::net::SocketAddr;
44    use std::path::PathBuf;
45
46    #[derive(Deserialize, PartialEq)]
47    struct WrappedString(String);
48
49    pub fn str<'de, D>(deserializer: D) -> Result<String, D::Error>
50    where
51        D: Deserializer<'de>,
52    {
53        let value = String::deserialize(deserializer)?;
54
55        match is_env_var(&value) {
56            true => Ok(get_env_var(&value)),
57            false => Ok(value),
58        }
59    }
60
61    pub fn opt_str<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
62    where
63        D: Deserializer<'de>,
64    {
65        Option::<WrappedString>::deserialize(deserializer).map(|option: Option<WrappedString>| {
66            option.map(|wrapped: WrappedString| {
67                let value = wrapped.0;
68
69                match is_env_var(&value) {
70                    true => get_env_var(&value),
71                    false => value,
72                }
73            })
74        })
75    }
76
77    pub fn socket_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        let value = String::deserialize(deserializer)?;
82
83        match is_env_var(&value) {
84            true => Ok(get_env_var(&value).parse().unwrap()),
85            false => Ok(value.parse().unwrap()),
86        }
87    }
88
89    pub fn path_buf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
90    where
91        D: Deserializer<'de>,
92    {
93        let value = String::deserialize(deserializer)?;
94
95        match is_env_var(&value) {
96            true => Ok(PathBuf::from(get_env_var(&value))),
97            false => Ok(PathBuf::from(value)),
98        }
99    }
100
101    fn is_env_var(value: &str) -> bool {
102        Regex::new(r"^\$\{env\.\w+\}$")
103            .expect("env_var: regex is invalid")
104            .is_match(value)
105    }
106
107    fn get_env_var(wrapped_key: &str) -> String {
108        let key: String = String::from(wrapped_key)
109            .drain(6..(wrapped_key.len() - 1))
110            .collect();
111
112        // NOTE: While we could deprecate the `${env.*}` syntax now that Sonic has
113        //   first-class support for environment variables, it would force people
114        //   to use the Sonic naming convention and potentially duplicate some
115        //   variables. For better UX, let’s keep it that way. It doesn’t require
116        //   dependencies nor bloat the code so it’s acceptable.
117
118        std::env::var(&key).unwrap_or_else(|_| panic!("env_var: variable '{key}' is not set"))
119    }
120
121    #[cfg(test)]
122    mod tests {
123        use super::*;
124
125        #[test]
126        fn it_checks_environment_variable_patterns() {
127            assert!(is_env_var("${env.XXX}"));
128            assert!(!is_env_var("${env.XXX"));
129            assert!(!is_env_var("${env.XXX}a"));
130            assert!(!is_env_var("a${env.XXX}"));
131            assert!(!is_env_var("{env.XXX}"));
132            assert!(!is_env_var("$env.XXX}"));
133            assert!(!is_env_var("${envXXX}"));
134            assert!(!is_env_var("${.XXX}"));
135            assert!(!is_env_var("${XXX}"));
136        }
137
138        #[test]
139        fn it_gets_environment_variable() {
140            unsafe { std::env::set_var("TEST", "test") };
141
142            assert_eq!(get_env_var("${env.TEST}"), "test");
143
144            unsafe { std::env::remove_var("TEST") };
145        }
146    }
147}