1use serde_json::{Map, Number, Value};
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum EnvDecode {
27 Json,
29 CommaSeparated,
31 CommaAndSpaceSeparated,
33 CommaThenSpaceSeparated,
35 JsonListOrCommaSeparated,
37 JsonListOrSpaceSeparated,
39 TracesSpan,
41 RawString,
43 Bool,
45 Integer,
47 Float,
49 StringList,
51 DurationString,
53 JsonValue,
55}
56
57pub fn decode(raw: &str, how: EnvDecode) -> Result<Value, String> {
65 match how {
66 EnvDecode::Json | EnvDecode::JsonValue => parse_json(raw),
67 EnvDecode::CommaSeparated => Ok(comma_separated(raw)),
68 EnvDecode::CommaAndSpaceSeparated => Ok(comma_and_space_separated(raw)),
69 EnvDecode::CommaThenSpaceSeparated => Ok(comma_then_space_separated(raw)),
70 EnvDecode::JsonListOrCommaSeparated => json_list_or_split(raw, ','),
71 EnvDecode::JsonListOrSpaceSeparated => json_list_or_split(raw, ' '),
72 EnvDecode::TracesSpan => traces_span(raw),
73 EnvDecode::RawString | EnvDecode::DurationString => Ok(Value::String(raw.to_string())),
74 EnvDecode::Bool => parse_bool(raw).map(Value::Bool),
75 EnvDecode::Integer => parse_integer(raw),
76 EnvDecode::Float => parse_float(raw),
77 EnvDecode::StringList => Ok(whitespace_list(raw)),
78 }
79}
80
81fn parse_json(raw: &str) -> Result<Value, String> {
87 serde_json::from_str(raw).map_err(|e| format!("invalid JSON: {e}"))
88}
89
90fn comma_separated(raw: &str) -> Value {
95 if raw.is_empty() {
96 return Value::Array(Vec::new());
97 }
98 string_array(raw.split(','))
99}
100
101fn comma_and_space_separated(raw: &str) -> Value {
106 string_array(raw.split([',', ' ']).filter(|s| !s.is_empty()))
107}
108
109fn comma_then_space_separated(raw: &str) -> Value {
114 let sep = if raw.contains(',') { ',' } else { ' ' };
115 string_array(raw.split(sep).map(str::trim))
116}
117
118fn json_list_or_split(raw: &str, sep: char) -> Result<Value, String> {
124 let trimmed = raw.trim();
125 if trimmed.starts_with('[') && trimmed.ends_with(']') {
126 let list: Vec<String> = serde_json::from_str(trimmed).map_err(|e| format!("invalid JSON string list: {e}"))?;
127 return Ok(string_array(list));
128 }
129 Ok(string_array(trimmed.split(sep)))
130}
131
132fn traces_span(raw: &str) -> Result<Value, String> {
137 let mut map = Map::new();
138 if raw.is_empty() {
139 return Ok(Value::Object(map));
140 }
141 for token in raw.split(',') {
142 let (name, rate) = token
143 .split_once('=')
144 .ok_or_else(|| format!("bad traces_span token `{token}`: expected name=rate"))?;
145 let rate: f64 = rate.parse().map_err(|_| format!("bad traces_span rate in `{token}`"))?;
146 let rate = Number::from_f64(rate).ok_or_else(|| format!("non-finite traces_span rate in `{token}`"))?;
147 map.insert(name.to_string(), Value::Number(rate));
148 }
149 Ok(Value::Object(map))
150}
151
152fn whitespace_list(raw: &str) -> Value {
154 string_array(raw.split_whitespace())
155}
156
157fn parse_bool(raw: &str) -> Result<bool, String> {
161 match raw {
162 "1" | "t" | "T" | "TRUE" | "true" | "True" => Ok(true),
163 "0" | "f" | "F" | "FALSE" | "false" | "False" => Ok(false),
164 other => Err(format!("invalid boolean `{other}`")),
165 }
166}
167
168fn parse_integer(raw: &str) -> Result<Value, String> {
170 raw.trim()
171 .parse::<i64>()
172 .map(|n| Value::Number(n.into()))
173 .map_err(|_| format!("invalid integer `{raw}`"))
174}
175
176fn parse_float(raw: &str) -> Result<Value, String> {
178 let n: f64 = raw.trim().parse().map_err(|_| format!("invalid number `{raw}`"))?;
179 Number::from_f64(n)
180 .map(Value::Number)
181 .ok_or_else(|| format!("non-finite number `{raw}`"))
182}
183
184fn string_array<S: AsRef<str>, I: IntoIterator<Item = S>>(items: I) -> Value {
186 Value::Array(
187 items
188 .into_iter()
189 .map(|s| Value::String(s.as_ref().to_string()))
190 .collect(),
191 )
192}
193
194#[cfg(test)]
195mod tests {
196 use serde_json::json;
197
198 use super::*;
199
200 fn arr(items: &[&str]) -> Value {
201 json!(items)
202 }
203
204 #[test]
205 fn json_parses_arbitrary_documents() {
206 assert_eq!(decode(r#"["a","b"]"#, EnvDecode::Json).unwrap(), arr(&["a", "b"]));
207 assert_eq!(
208 decode(r#"{"k":["v"]}"#, EnvDecode::JsonValue).unwrap(),
209 json!({"k": ["v"]})
210 );
211 assert!(decode("{bad", EnvDecode::Json).is_err());
212 }
213
214 #[test]
215 fn comma_separated_does_not_trim() {
216 assert_eq!(decode("a, b", EnvDecode::CommaSeparated).unwrap(), arr(&["a", " b"]));
217 assert_eq!(decode("", EnvDecode::CommaSeparated).unwrap(), Value::Array(vec![]));
218 }
219
220 #[test]
221 fn comma_and_space_drops_empties() {
222 assert_eq!(
223 decode("a, b c,,d", EnvDecode::CommaAndSpaceSeparated).unwrap(),
224 arr(&["a", "b", "c", "d"])
225 );
226 }
227
228 #[test]
229 fn comma_then_space_keeps_empties_and_trims() {
230 assert_eq!(
231 decode("a,,b", EnvDecode::CommaThenSpaceSeparated).unwrap(),
232 arr(&["a", "", "b"])
233 );
234 assert_eq!(
235 decode("a b c", EnvDecode::CommaThenSpaceSeparated).unwrap(),
236 arr(&["a", "b", "", "c"])
237 );
238 }
239
240 #[test]
241 fn json_list_or_comma_takes_either_branch() {
242 assert_eq!(
243 decode(r#" ["a","b"] "#, EnvDecode::JsonListOrCommaSeparated).unwrap(),
244 arr(&["a", "b"])
245 );
246 assert_eq!(
247 decode("a,b", EnvDecode::JsonListOrCommaSeparated).unwrap(),
248 arr(&["a", "b"])
249 );
250 assert!(decode("[bad", EnvDecode::JsonListOrCommaSeparated).is_ok()); assert!(decode(r#"["unterminated"#, EnvDecode::JsonListOrCommaSeparated).is_ok());
252 assert!(decode("[1,2]", EnvDecode::JsonListOrCommaSeparated).is_err()); }
254
255 #[test]
256 fn json_list_or_space_splits_on_space() {
257 assert_eq!(
258 decode("a b", EnvDecode::JsonListOrSpaceSeparated).unwrap(),
259 arr(&["a", "b"])
260 );
261 }
262
263 #[test]
264 fn traces_span_builds_a_rate_map() {
265 assert_eq!(
266 decode("svc|op=0.5,other|op2=1", EnvDecode::TracesSpan).unwrap(),
267 json!({ "svc|op": 0.5, "other|op2": 1.0 })
268 );
269 assert_eq!(decode("", EnvDecode::TracesSpan).unwrap(), json!({}));
270 assert!(decode("svc|op", EnvDecode::TracesSpan).is_err());
271 assert!(decode("svc|op=notnum", EnvDecode::TracesSpan).is_err());
272 }
273
274 #[test]
275 fn scalar_fallbacks() {
276 assert_eq!(decode("hello", EnvDecode::RawString).unwrap(), json!("hello"));
277 assert_eq!(decode("true", EnvDecode::Bool).unwrap(), json!(true));
278 assert_eq!(decode("T", EnvDecode::Bool).unwrap(), json!(true));
279 assert!(decode("yes", EnvDecode::Bool).is_err());
280 assert_eq!(decode("9125", EnvDecode::Integer).unwrap(), json!(9125));
281 assert!(decode("9125.0", EnvDecode::Integer).is_err());
282 assert_eq!(decode("1.5", EnvDecode::Float).unwrap(), json!(1.5));
283 }
284
285 #[test]
286 fn string_list_splits_on_whitespace() {
287 assert_eq!(
288 decode("env:prod team:core", EnvDecode::StringList).unwrap(),
289 arr(&["env:prod", "team:core"])
290 );
291 assert_eq!(decode(" ", EnvDecode::StringList).unwrap(), Value::Array(vec![]));
292 }
293
294 #[test]
295 fn duration_is_carried_through_as_a_string() {
296 assert_eq!(decode("10s", EnvDecode::DurationString).unwrap(), json!("10s"));
298 assert_eq!(decode("30", EnvDecode::DurationString).unwrap(), json!("30"));
299 }
300}