1use serde_json::{Map, Number, Value};
23
24use crate::cast_de;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum EnvDecode {
32 Json,
34 CommaSeparated,
36 CommaAndSpaceSeparated,
38 CommaThenSpaceSeparated,
40 JsonListOrCommaSeparated,
42 JsonListOrSpaceSeparated,
44 TracesSpan,
46 RawString,
48 Bool,
50 Integer,
52 Float,
54 StringList,
56 DurationString,
58 JsonValue,
60}
61
62pub fn decode(raw: &str, how: EnvDecode) -> Result<Value, String> {
70 match how {
71 EnvDecode::Json | EnvDecode::JsonValue => parse_json(raw),
72 EnvDecode::CommaSeparated => Ok(comma_separated(raw)),
73 EnvDecode::CommaAndSpaceSeparated => Ok(comma_and_space_separated(raw)),
74 EnvDecode::CommaThenSpaceSeparated => Ok(comma_then_space_separated(raw)),
75 EnvDecode::JsonListOrCommaSeparated => json_list_or_split(raw, ','),
76 EnvDecode::JsonListOrSpaceSeparated => json_list_or_split(raw, ' '),
77 EnvDecode::TracesSpan => traces_span(raw),
78 EnvDecode::RawString | EnvDecode::DurationString => Ok(Value::String(raw.to_string())),
79 EnvDecode::Bool => cast_de::parse_bool(raw).map(Value::Bool),
80 EnvDecode::Integer => cast_de::parse_i64(raw).map(|n| Value::Number(n.into())),
81 EnvDecode::Float => cast_de::parse_f64(raw)
82 .map(|parsed| Value::Number(Number::from_f64(parsed).expect("a finite float is a JSON number"))),
84 EnvDecode::StringList => Ok(whitespace_list(raw)),
85 }
86}
87
88fn parse_json(raw: &str) -> Result<Value, String> {
94 serde_json::from_str(raw).map_err(|e| format!("invalid JSON: {e}"))
95}
96
97fn comma_separated(raw: &str) -> Value {
102 if raw.is_empty() {
103 return Value::Array(Vec::new());
104 }
105 string_array(raw.split(','))
106}
107
108fn comma_and_space_separated(raw: &str) -> Value {
113 string_array(raw.split([',', ' ']).filter(|s| !s.is_empty()))
114}
115
116fn comma_then_space_separated(raw: &str) -> Value {
121 let sep = if raw.contains(',') { ',' } else { ' ' };
122 string_array(raw.split(sep).map(str::trim))
123}
124
125fn json_list_or_split(raw: &str, sep: char) -> Result<Value, String> {
131 let trimmed = raw.trim();
132 if trimmed.starts_with('[') && trimmed.ends_with(']') {
133 let list: Vec<String> = serde_json::from_str(trimmed).map_err(|e| format!("invalid JSON string list: {e}"))?;
134 return Ok(string_array(list));
135 }
136 Ok(string_array(trimmed.split(sep)))
137}
138
139fn traces_span(raw: &str) -> Result<Value, String> {
144 let mut map = Map::new();
145 if raw.is_empty() {
146 return Ok(Value::Object(map));
147 }
148 for token in raw.split(',') {
149 let (name, rate) = token
150 .split_once('=')
151 .ok_or_else(|| format!("bad traces_span token `{token}`: expected name=rate"))?;
152 let rate: f64 = rate.parse().map_err(|_| format!("bad traces_span rate in `{token}`"))?;
153 let rate = Number::from_f64(rate).ok_or_else(|| format!("non-finite traces_span rate in `{token}`"))?;
154 map.insert(name.to_string(), Value::Number(rate));
155 }
156 Ok(Value::Object(map))
157}
158
159fn whitespace_list(raw: &str) -> Value {
161 string_array(raw.split_whitespace())
162}
163
164fn string_array<S: AsRef<str>, I: IntoIterator<Item = S>>(items: I) -> Value {
166 Value::Array(
167 items
168 .into_iter()
169 .map(|s| Value::String(s.as_ref().to_string()))
170 .collect(),
171 )
172}
173
174#[cfg(test)]
175mod tests {
176 use serde_json::json;
177
178 use super::*;
179
180 fn arr(items: &[&str]) -> Value {
181 json!(items)
182 }
183
184 #[test]
185 fn json_parses_arbitrary_documents() {
186 assert_eq!(decode(r#"["a","b"]"#, EnvDecode::Json).unwrap(), arr(&["a", "b"]));
187 assert_eq!(
188 decode(r#"{"k":["v"]}"#, EnvDecode::JsonValue).unwrap(),
189 json!({"k": ["v"]})
190 );
191 assert!(decode("{bad", EnvDecode::Json).is_err());
192 }
193
194 #[test]
195 fn comma_separated_does_not_trim() {
196 assert_eq!(decode("a, b", EnvDecode::CommaSeparated).unwrap(), arr(&["a", " b"]));
197 assert_eq!(decode("", EnvDecode::CommaSeparated).unwrap(), Value::Array(vec![]));
198 }
199
200 #[test]
201 fn comma_and_space_drops_empties() {
202 assert_eq!(
203 decode("a, b c,,d", EnvDecode::CommaAndSpaceSeparated).unwrap(),
204 arr(&["a", "b", "c", "d"])
205 );
206 }
207
208 #[test]
209 fn comma_then_space_keeps_empties_and_trims() {
210 assert_eq!(
211 decode("a,,b", EnvDecode::CommaThenSpaceSeparated).unwrap(),
212 arr(&["a", "", "b"])
213 );
214 assert_eq!(
215 decode("a b c", EnvDecode::CommaThenSpaceSeparated).unwrap(),
216 arr(&["a", "b", "", "c"])
217 );
218 }
219
220 #[test]
221 fn json_list_or_comma_takes_either_branch() {
222 assert_eq!(
223 decode(r#" ["a","b"] "#, EnvDecode::JsonListOrCommaSeparated).unwrap(),
224 arr(&["a", "b"])
225 );
226 assert_eq!(
227 decode("a,b", EnvDecode::JsonListOrCommaSeparated).unwrap(),
228 arr(&["a", "b"])
229 );
230 assert!(decode("[bad", EnvDecode::JsonListOrCommaSeparated).is_ok()); assert!(decode(r#"["unterminated"#, EnvDecode::JsonListOrCommaSeparated).is_ok());
232 assert!(decode("[1,2]", EnvDecode::JsonListOrCommaSeparated).is_err()); }
234
235 #[test]
236 fn json_list_or_space_splits_on_space() {
237 assert_eq!(
238 decode("a b", EnvDecode::JsonListOrSpaceSeparated).unwrap(),
239 arr(&["a", "b"])
240 );
241 }
242
243 #[test]
244 fn traces_span_builds_a_rate_map() {
245 assert_eq!(
246 decode("svc|op=0.5,other|op2=1", EnvDecode::TracesSpan).unwrap(),
247 json!({ "svc|op": 0.5, "other|op2": 1.0 })
248 );
249 assert_eq!(decode("", EnvDecode::TracesSpan).unwrap(), json!({}));
250 assert!(decode("svc|op", EnvDecode::TracesSpan).is_err());
251 assert!(decode("svc|op=notnum", EnvDecode::TracesSpan).is_err());
252 }
253
254 #[test]
255 fn scalar_fallbacks() {
256 assert_eq!(decode("hello", EnvDecode::RawString).unwrap(), json!("hello"));
257 assert_eq!(decode("true", EnvDecode::Bool).unwrap(), json!(true));
258 assert_eq!(decode("T", EnvDecode::Bool).unwrap(), json!(true));
259 assert!(decode("yes", EnvDecode::Bool).is_err());
260 assert_eq!(decode("9125", EnvDecode::Integer).unwrap(), json!(9125));
261 assert_eq!(decode("9125.0", EnvDecode::Integer).unwrap(), json!(9125));
262 assert!(decode("9125.5", EnvDecode::Integer).is_err());
263 assert_eq!(decode("1.5", EnvDecode::Float).unwrap(), json!(1.5));
264 }
265
266 #[test]
267 fn string_list_splits_on_whitespace() {
268 assert_eq!(
269 decode("env:prod team:core", EnvDecode::StringList).unwrap(),
270 arr(&["env:prod", "team:core"])
271 );
272 assert_eq!(decode(" ", EnvDecode::StringList).unwrap(), Value::Array(vec![]));
273 }
274
275 #[test]
276 fn duration_is_carried_through_as_a_string() {
277 assert_eq!(decode("10s", EnvDecode::DurationString).unwrap(), json!("10s"));
279 assert_eq!(decode("30", EnvDecode::DurationString).unwrap(), json!("30"));
280 }
281}