1use serde_json::{Map, Number, Value};
23
24use crate::cast_de;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum EnvDecode {
34 Json,
36 CommaSeparated,
38 CommaAndSpaceSeparated,
40 CommaThenSpaceSeparated,
42 JsonListOrCommaSeparated,
44 JsonListOrSpaceSeparated,
46 TracesSpan,
48 RawString,
50 Bool,
52 Integer,
55 Float,
57 StringList,
60 DurationString,
62 JsonValue,
64 DecimalInteger,
67 DecimalFloat,
69 WhitespaceList,
71}
72
73pub fn decode(raw: &str, how: EnvDecode) -> Result<Value, String> {
81 match how {
82 EnvDecode::Json | EnvDecode::JsonValue => parse_json(raw),
83 EnvDecode::CommaSeparated => Ok(comma_separated(raw)),
84 EnvDecode::CommaAndSpaceSeparated => Ok(comma_and_space_separated(raw)),
85 EnvDecode::CommaThenSpaceSeparated => Ok(comma_then_space_separated(raw)),
86 EnvDecode::JsonListOrCommaSeparated => json_list_or_split(raw, ','),
87 EnvDecode::JsonListOrSpaceSeparated => json_list_or_split(raw, ' '),
88 EnvDecode::TracesSpan => traces_span(raw),
89 EnvDecode::RawString | EnvDecode::DurationString => Ok(Value::String(raw.to_string())),
90 EnvDecode::Bool => cast_de::parse_bool(raw).map(Value::Bool),
91 EnvDecode::Integer => cast_de::parse_i64(raw).map(|n| Value::Number(n.into())),
92 EnvDecode::Float => cast_de::parse_f64(raw).map(finite_number),
93 EnvDecode::StringList => Ok(string_array(cast_de::parse_string_list(raw))),
94 EnvDecode::DecimalInteger => cast_de::parse_decimal_i64(raw).map(|n| Value::Number(n.into())),
95 EnvDecode::DecimalFloat => cast_de::parse_decimal_f64(raw).map(finite_number),
96 EnvDecode::WhitespaceList => Ok(string_array(raw.split_whitespace())),
97 }
98}
99
100fn parse_json(raw: &str) -> Result<Value, String> {
106 serde_json::from_str(raw).map_err(|e| format!("invalid JSON: {e}"))
107}
108
109fn comma_separated(raw: &str) -> Value {
114 if raw.is_empty() {
115 return Value::Array(Vec::new());
116 }
117 string_array(raw.split(','))
118}
119
120fn comma_and_space_separated(raw: &str) -> Value {
125 string_array(raw.split([',', ' ']).filter(|s| !s.is_empty()))
126}
127
128fn comma_then_space_separated(raw: &str) -> Value {
133 let sep = if raw.contains(',') { ',' } else { ' ' };
134 string_array(raw.split(sep).map(str::trim))
135}
136
137fn json_list_or_split(raw: &str, sep: char) -> Result<Value, String> {
144 let trimmed = raw.trim();
145 if trimmed.starts_with('[') && trimmed.ends_with(']') {
146 let list: Vec<Option<String>> =
147 serde_json::from_str(trimmed).map_err(|e| format!("invalid JSON string list: {e}"))?;
148 return Ok(string_array(list.into_iter().map(Option::unwrap_or_default)));
149 }
150 Ok(string_array(trimmed.split(sep)))
151}
152
153fn traces_span(raw: &str) -> Result<Value, String> {
158 let mut map = Map::new();
159 if raw.is_empty() {
160 return Ok(Value::Object(map));
161 }
162 for token in raw.split(',') {
163 let (name, rate) = token
164 .split_once('=')
165 .ok_or_else(|| format!("bad traces_span token `{token}`: expected name=rate"))?;
166 let rate = Some(rate)
168 .filter(|rate| rate.trim() == *rate)
169 .and_then(|rate| cast_de::parse_f64(rate).ok())
170 .ok_or_else(|| format!("bad traces_span rate in `{token}`"))?;
171 map.insert(name.to_string(), finite_number(rate));
172 }
173 Ok(Value::Object(map))
174}
175
176fn finite_number(parsed: f64) -> Value {
178 Value::Number(Number::from_f64(parsed).expect("a finite float is a JSON number"))
179}
180
181fn string_array<S: AsRef<str>, I: IntoIterator<Item = S>>(items: I) -> Value {
183 Value::Array(
184 items
185 .into_iter()
186 .map(|s| Value::String(s.as_ref().to_string()))
187 .collect(),
188 )
189}
190
191#[cfg(test)]
192mod tests {
193 use serde_json::json;
194
195 use super::*;
196
197 fn arr(items: &[&str]) -> Value {
198 json!(items)
199 }
200
201 #[test]
202 fn json_parses_arbitrary_documents() {
203 assert_eq!(decode(r#"["a","b"]"#, EnvDecode::Json).unwrap(), arr(&["a", "b"]));
204 assert_eq!(
205 decode(r#"{"k":["v"]}"#, EnvDecode::JsonValue).unwrap(),
206 json!({"k": ["v"]})
207 );
208 assert!(decode("{bad", EnvDecode::Json).is_err());
209 }
210
211 #[test]
212 fn comma_separated_does_not_trim() {
213 assert_eq!(decode("a, b", EnvDecode::CommaSeparated).unwrap(), arr(&["a", " b"]));
214 assert_eq!(decode("", EnvDecode::CommaSeparated).unwrap(), Value::Array(vec![]));
215 }
216
217 #[test]
218 fn comma_and_space_drops_empties() {
219 assert_eq!(
220 decode("a, b c,,d", EnvDecode::CommaAndSpaceSeparated).unwrap(),
221 arr(&["a", "b", "c", "d"])
222 );
223 }
224
225 #[test]
226 fn comma_then_space_keeps_empties_and_trims() {
227 assert_eq!(
228 decode("a,,b", EnvDecode::CommaThenSpaceSeparated).unwrap(),
229 arr(&["a", "", "b"])
230 );
231 assert_eq!(
232 decode("a b c", EnvDecode::CommaThenSpaceSeparated).unwrap(),
233 arr(&["a", "b", "", "c"])
234 );
235 }
236
237 #[test]
238 fn json_list_or_comma_takes_either_branch() {
239 assert_eq!(
240 decode(r#" ["a","b"] "#, EnvDecode::JsonListOrCommaSeparated).unwrap(),
241 arr(&["a", "b"])
242 );
243 assert_eq!(
244 decode("a,b", EnvDecode::JsonListOrCommaSeparated).unwrap(),
245 arr(&["a", "b"])
246 );
247 assert!(decode("[bad", EnvDecode::JsonListOrCommaSeparated).is_ok()); assert!(decode(r#"["unterminated"#, EnvDecode::JsonListOrCommaSeparated).is_ok());
249 assert!(decode("[1,2]", EnvDecode::JsonListOrCommaSeparated).is_err()); }
251
252 #[test]
253 fn json_list_reads_a_null_element_as_an_empty_string() {
254 assert_eq!(
255 decode(r#"["a",null]"#, EnvDecode::JsonListOrSpaceSeparated).unwrap(),
256 arr(&["a", ""])
257 );
258 }
259
260 #[test]
261 fn json_list_or_space_splits_on_space() {
262 assert_eq!(
263 decode("a b", EnvDecode::JsonListOrSpaceSeparated).unwrap(),
264 arr(&["a", "b"])
265 );
266 }
267
268 #[test]
269 fn traces_span_builds_a_rate_map() {
270 assert_eq!(
271 decode("svc|op=0.5,other|op2=1", EnvDecode::TracesSpan).unwrap(),
272 json!({ "svc|op": 0.5, "other|op2": 1.0 })
273 );
274 assert_eq!(
275 decode("svc|op=0x1p-1,other|op2=1_0", EnvDecode::TracesSpan).unwrap(),
276 json!({ "svc|op": 0.5, "other|op2": 10.0 })
277 );
278 assert_eq!(decode("", EnvDecode::TracesSpan).unwrap(), json!({}));
279 assert!(decode("svc|op", EnvDecode::TracesSpan).is_err());
280 assert!(decode("svc|op=notnum", EnvDecode::TracesSpan).is_err());
281 assert!(decode("svc|op= 0.5", EnvDecode::TracesSpan).is_err());
282 }
283
284 #[test]
285 fn scalar_fallbacks() {
286 assert_eq!(decode("hello", EnvDecode::RawString).unwrap(), json!("hello"));
287 assert_eq!(decode("true", EnvDecode::Bool).unwrap(), json!(true));
288 assert_eq!(decode("T", EnvDecode::Bool).unwrap(), json!(true));
289 assert!(decode("yes", EnvDecode::Bool).is_err());
290 assert_eq!(decode("9125", EnvDecode::Integer).unwrap(), json!(9125));
291 assert_eq!(decode("9125.0", EnvDecode::Integer).unwrap(), json!(9125));
292 assert!(decode("9125.5", EnvDecode::Integer).is_err());
293 assert_eq!(decode("1.5", EnvDecode::Float).unwrap(), json!(1.5));
294 }
295
296 #[test]
297 fn string_list_follows_the_go_slice_cast() {
298 for (raw, expected) in cast_de::STRING_LIST_CASES {
299 assert_eq!(decode(raw, EnvDecode::StringList).unwrap(), arr(expected), "{raw:?}");
300 }
301 }
302
303 #[test]
304 fn integer_reads_the_go_base_prefixes() {
305 for (raw, expected) in [
306 ("0x10", 16),
307 ("010", 8),
308 ("0b11", 3),
309 ("0o17", 15),
310 ("1_000", 1000),
311 ("-0x10", -16),
312 ] {
313 assert_eq!(decode(raw, EnvDecode::Integer).unwrap(), json!(expected), "{raw}");
314 }
315 assert_eq!(decode("08", EnvDecode::Integer).unwrap(), json!(0));
317 assert!(decode("08.", EnvDecode::Integer).is_err());
318 }
319
320 #[test]
321 fn float_reads_go_literals() {
322 assert_eq!(decode("0x1p4", EnvDecode::Float).unwrap(), json!(16.0));
323 assert_eq!(decode("1_000.5", EnvDecode::Float).unwrap(), json!(1000.5));
324 assert!(decode("0x10", EnvDecode::Float).is_err());
325 assert!(decode("inf", EnvDecode::Float).is_err());
326 }
327
328 #[test]
329 fn decimal_integer_keeps_a_leading_zero_decimal() {
330 assert_eq!(decode("010", EnvDecode::DecimalInteger).unwrap(), json!(10));
331 assert_eq!(decode("9125.0", EnvDecode::DecimalInteger).unwrap(), json!(9125));
332 for rejected in ["0x10", "0b11", "1_000", "9125.5"] {
333 assert!(decode(rejected, EnvDecode::DecimalInteger).is_err(), "{rejected}");
334 }
335 }
336
337 #[test]
338 fn decimal_float_has_no_go_only_spellings() {
339 assert_eq!(decode("1.5e1", EnvDecode::DecimalFloat).unwrap(), json!(15.0));
340 for rejected in ["0x1p4", "1_000.5", "inf"] {
341 assert!(decode(rejected, EnvDecode::DecimalFloat).is_err(), "{rejected}");
342 }
343 }
344
345 #[test]
346 fn whitespace_list_never_reads_json() {
347 assert_eq!(
348 decode("env:prod team:core", EnvDecode::WhitespaceList).unwrap(),
349 arr(&["env:prod", "team:core"])
350 );
351 assert_eq!(
352 decode(r#"["a","b"]"#, EnvDecode::WhitespaceList).unwrap(),
353 arr(&[r#"["a","b"]"#])
354 );
355 assert_eq!(decode(" ", EnvDecode::WhitespaceList).unwrap(), Value::Array(vec![]));
356 }
357
358 #[test]
359 fn duration_is_carried_through_as_a_string() {
360 assert_eq!(decode("10s", EnvDecode::DurationString).unwrap(), json!("10s"));
362 assert_eq!(decode("30", EnvDecode::DurationString).unwrap(), json!("30"));
363 }
364}