datadog_agent_config/
cast_de.rs

1//! Deserialize schema values the way the Datadog Agent reads them.
2//!
3//! The Agent never reads a setting as the type its YAML happens to hold: `GetBool`, `GetInt`,
4//! `GetFloat64`, and `GetString` each coerce whatever is stored through `spf13/cast`. Permissiveness
5//! is therefore a property of the leaf's declared type, not of the key, and `dogstatsd_port: "8125"`
6//! or `use_v3_api.series.enabled: true` are configurations the Agent accepts.
7//!
8//! This module ports `cast.To{Bool,Int64,Float64,String}E` so scalar leaves and string-map values
9//! accept every spelling the Agent accepts while keeping the schema's type. [`crate::env_decode`]
10//! uses the same parsers for environment strings.
11//!
12//! Two deliberate divergences from `cast`:
13//!
14//! - A value `cast` cannot convert is a hard error here. `cast.To*` swallows its error and yields the
15//!   zero value, so the Agent silently reads a malformed setting as `false`/`0`/`""`;
16//!   [`crate::env_decode`] already rejects such a value rather than losing it.
17//! - A numeric string is accepted in decimal only, not in Go's base-prefixed or underscored integer
18//!   literal forms. YAML and JSON parse those spellings into numbers before ADP sees them.
19
20use std::{collections::HashMap, fmt};
21
22use serde::de::{self, Deserializer, Unexpected, Visitor};
23use serde::Deserialize;
24
25/// `cast.ToBoolE` for a string: Go's `strconv.ParseBool` grammar, exactly.
26///
27/// # Errors
28///
29/// Returns a message naming the value when it is not one of the accepted spellings.
30pub(crate) fn parse_bool(raw: &str) -> Result<bool, String> {
31    match raw {
32        "1" | "t" | "T" | "TRUE" | "true" | "True" => Ok(true),
33        "0" | "f" | "F" | "FALSE" | "false" | "False" => Ok(false),
34        other => Err(format!("invalid boolean `{other}`")),
35    }
36}
37
38/// `cast.ToInt64E` for a string.
39///
40/// # Errors
41///
42/// Returns a message naming the value when it is not a decimal integer.
43pub(crate) fn parse_i64(raw: &str) -> Result<i64, String> {
44    trim_zero_decimal(raw.trim())
45        .parse::<i64>()
46        .map_err(|_| format!("invalid integer `{raw}`"))
47}
48
49/// `cast`'s `trimZeroDecimal`, which drops an all-zero fraction before integer parsing, so `"8125.0"`
50/// is an integer setting while `"8125.5"` is not.
51fn trim_zero_decimal(raw: &str) -> &str {
52    match raw.split_once('.') {
53        Some((integer, fraction)) if !fraction.is_empty() && fraction.bytes().all(|byte| byte == b'0') => integer,
54        _ => raw,
55    }
56}
57
58/// `cast.ToFloat64E` for a string.
59///
60/// # Errors
61///
62/// Returns a message naming the value when it is not a finite number.
63pub(crate) fn parse_f64(raw: &str) -> Result<f64, String> {
64    let parsed: f64 = raw.trim().parse().map_err(|_| format!("invalid number `{raw}`"))?;
65    if !parsed.is_finite() {
66        return Err(format!("non-finite number `{raw}`"));
67    }
68    Ok(parsed)
69}
70
71/// Deserializes a `boolean` leaf (`cast.ToBoolE`).
72///
73/// # Errors
74///
75/// Returns an error for a value the Agent cannot cast to a boolean: an unrecognized string or a
76/// compound value.
77pub(crate) fn deserialize_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
78where
79    D: Deserializer<'de>,
80{
81    deserializer.deserialize_any(BoolVisitor)
82}
83
84/// Deserializes an `integer` leaf (`cast.ToInt64E`).
85///
86/// # Errors
87///
88/// Returns an error for a value the Agent cannot cast to an integer: a non-numeric string, an
89/// out-of-range number, or a compound value.
90pub(crate) fn deserialize_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
91where
92    D: Deserializer<'de>,
93{
94    deserializer.deserialize_any(I64Visitor)
95}
96
97/// Deserializes a `number` leaf (`cast.ToFloat64E`).
98///
99/// # Errors
100///
101/// Returns an error for a value the Agent cannot cast to a number: a non-numeric string or a
102/// compound value.
103pub(crate) fn deserialize_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
104where
105    D: Deserializer<'de>,
106{
107    deserializer.deserialize_any(F64Visitor)
108}
109
110/// Deserializes a `string` leaf (`cast.ToStringE`).
111///
112/// # Errors
113///
114/// Returns an error for a compound value, the one shape the Agent cannot render as a string.
115pub(crate) fn deserialize_string<'de, D>(deserializer: D) -> Result<String, D::Error>
116where
117    D: Deserializer<'de>,
118{
119    deserializer.deserialize_any(StringVisitor)
120}
121
122/// Deserializes a string map, coercing each value as the Agent does.
123///
124/// # Errors
125///
126/// Returns an error when a value is not scalar.
127pub(crate) fn deserialize_string_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
128where
129    D: Deserializer<'de>,
130{
131    let values = HashMap::<String, serde_json::Value>::deserialize(deserializer)?;
132    values
133        .into_iter()
134        .map(|(key, value)| {
135            cast_to_string(&value)
136                .map(|value| (key, value))
137                .map_err(de::Error::custom)
138        })
139        .collect()
140}
141
142/// Renders a JSON value as a `string` leaf (`cast.ToStringE`).
143///
144/// A witness method that receives a leaf as raw JSON, rather than as a generated field, renders it
145/// through this so that `1.0` and a null read as the Agent reads them (`"1"` and `""`) instead of as
146/// their JSON spelling.
147///
148/// # Errors
149///
150/// Returns an error for a compound value, the one shape the Agent cannot render as a string.
151pub fn cast_to_string(value: &::serde_json::Value) -> Result<String, String> {
152    value.deserialize_any(StringVisitor).map_err(|e| e.to_string())
153}
154
155/// Deserializes an optional `string` leaf, where an absent or null value stays `None`.
156///
157/// # Errors
158///
159/// Same as [`deserialize_string`] for a present value.
160pub(crate) fn deserialize_optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
161where
162    D: Deserializer<'de>,
163{
164    deserializer.deserialize_option(OptionalStringVisitor)
165}
166
167/// Deserializes an optional `integer` leaf, where an absent or null value stays `None`.
168///
169/// # Errors
170///
171/// Same as [`deserialize_i64`] for a present value.
172pub(crate) fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
173where
174    D: Deserializer<'de>,
175{
176    deserializer.deserialize_option(OptionalI64Visitor)
177}
178
179struct BoolVisitor;
180
181impl Visitor<'_> for BoolVisitor {
182    type Value = bool;
183
184    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str("a boolean, a boolean string, or a number")
186    }
187
188    fn visit_bool<E: de::Error>(self, value: bool) -> Result<bool, E> {
189        Ok(value)
190    }
191
192    fn visit_i64<E: de::Error>(self, value: i64) -> Result<bool, E> {
193        Ok(value != 0)
194    }
195
196    fn visit_u64<E: de::Error>(self, value: u64) -> Result<bool, E> {
197        Ok(value != 0)
198    }
199
200    fn visit_f64<E: de::Error>(self, value: f64) -> Result<bool, E> {
201        Ok(value != 0.0)
202    }
203
204    fn visit_str<E: de::Error>(self, value: &str) -> Result<bool, E> {
205        parse_bool(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
206    }
207
208    // `cast` maps a nil to the zero value, which for a configuration leaf means an explicit
209    // `key: null` reads as if the key were unset.
210    fn visit_unit<E: de::Error>(self) -> Result<bool, E> {
211        Ok(false)
212    }
213}
214
215struct I64Visitor;
216
217impl Visitor<'_> for I64Visitor {
218    type Value = i64;
219
220    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        f.write_str("an integer, a numeric string, or a boolean")
222    }
223
224    fn visit_bool<E: de::Error>(self, value: bool) -> Result<i64, E> {
225        Ok(i64::from(value))
226    }
227
228    fn visit_i64<E: de::Error>(self, value: i64) -> Result<i64, E> {
229        Ok(value)
230    }
231
232    fn visit_u64<E: de::Error>(self, value: u64) -> Result<i64, E> {
233        i64::try_from(value).map_err(|_| E::invalid_value(Unexpected::Unsigned(value), &self))
234    }
235
236    // Go's `int(float64)` truncation, which is what the Agent reads for a leaf written `10.5`.
237    fn visit_f64<E: de::Error>(self, value: f64) -> Result<i64, E> {
238        if !value.is_finite() || value < i64::MIN as f64 || value > i64::MAX as f64 {
239            return Err(E::invalid_value(Unexpected::Float(value), &self));
240        }
241        Ok(value.trunc() as i64)
242    }
243
244    fn visit_str<E: de::Error>(self, value: &str) -> Result<i64, E> {
245        parse_i64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
246    }
247
248    fn visit_unit<E: de::Error>(self) -> Result<i64, E> {
249        Ok(0)
250    }
251}
252
253struct F64Visitor;
254
255impl Visitor<'_> for F64Visitor {
256    type Value = f64;
257
258    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        f.write_str("a number, a numeric string, or a boolean")
260    }
261
262    fn visit_bool<E: de::Error>(self, value: bool) -> Result<f64, E> {
263        Ok(if value { 1.0 } else { 0.0 })
264    }
265
266    fn visit_i64<E: de::Error>(self, value: i64) -> Result<f64, E> {
267        Ok(value as f64)
268    }
269
270    fn visit_u64<E: de::Error>(self, value: u64) -> Result<f64, E> {
271        Ok(value as f64)
272    }
273
274    fn visit_f64<E: de::Error>(self, value: f64) -> Result<f64, E> {
275        Ok(value)
276    }
277
278    fn visit_str<E: de::Error>(self, value: &str) -> Result<f64, E> {
279        parse_f64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
280    }
281
282    fn visit_unit<E: de::Error>(self) -> Result<f64, E> {
283        Ok(0.0)
284    }
285}
286
287struct StringVisitor;
288
289impl Visitor<'_> for StringVisitor {
290    type Value = String;
291
292    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        f.write_str("a string, a boolean, or a number")
294    }
295
296    fn visit_bool<E: de::Error>(self, value: bool) -> Result<String, E> {
297        Ok(value.to_string())
298    }
299
300    fn visit_i64<E: de::Error>(self, value: i64) -> Result<String, E> {
301        Ok(value.to_string())
302    }
303
304    fn visit_u64<E: de::Error>(self, value: u64) -> Result<String, E> {
305        Ok(value.to_string())
306    }
307
308    // Rust's `Display` for `f64` matches Go's `FormatFloat(v, 'f', -1, 64)`: the shortest form that
309    // round-trips, never in exponent notation.
310    fn visit_f64<E: de::Error>(self, value: f64) -> Result<String, E> {
311        Ok(value.to_string())
312    }
313
314    fn visit_str<E: de::Error>(self, value: &str) -> Result<String, E> {
315        Ok(value.to_owned())
316    }
317
318    fn visit_string<E: de::Error>(self, value: String) -> Result<String, E> {
319        Ok(value)
320    }
321
322    fn visit_unit<E: de::Error>(self) -> Result<String, E> {
323        Ok(String::new())
324    }
325}
326
327struct OptionalStringVisitor;
328
329impl<'de> Visitor<'de> for OptionalStringVisitor {
330    type Value = Option<String>;
331
332    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        f.write_str("a string, a boolean, a number, or null")
334    }
335
336    fn visit_none<E: de::Error>(self) -> Result<Option<String>, E> {
337        Ok(None)
338    }
339
340    fn visit_unit<E: de::Error>(self) -> Result<Option<String>, E> {
341        Ok(None)
342    }
343
344    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<String>, D::Error> {
345        deserializer.deserialize_any(StringVisitor).map(Some)
346    }
347}
348
349struct OptionalI64Visitor;
350
351impl<'de> Visitor<'de> for OptionalI64Visitor {
352    type Value = Option<i64>;
353
354    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        f.write_str("an integer, a numeric string, a boolean, or null")
356    }
357
358    fn visit_none<E: de::Error>(self) -> Result<Option<i64>, E> {
359        Ok(None)
360    }
361
362    fn visit_unit<E: de::Error>(self) -> Result<Option<i64>, E> {
363        Ok(None)
364    }
365
366    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<i64>, D::Error> {
367        deserializer.deserialize_any(I64Visitor).map(Some)
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use serde::Deserialize;
374    use serde_json::{json, Value};
375
376    use super::*;
377
378    #[derive(Deserialize)]
379    struct Bool(#[serde(deserialize_with = "deserialize_bool")] bool);
380
381    #[derive(Deserialize)]
382    struct Int(#[serde(deserialize_with = "deserialize_i64")] i64);
383
384    #[derive(Deserialize)]
385    struct Float(#[serde(deserialize_with = "deserialize_f64")] f64);
386
387    #[derive(Deserialize)]
388    struct Str(#[serde(deserialize_with = "deserialize_string")] String);
389
390    #[derive(Deserialize)]
391    struct OptStr(#[serde(deserialize_with = "deserialize_optional_string")] Option<String>);
392
393    #[derive(Deserialize)]
394    struct StringMap(#[serde(deserialize_with = "deserialize_string_map")] HashMap<String, String>);
395
396    fn as_bool(value: Value) -> Result<bool, String> {
397        serde_json::from_value::<Bool>(value)
398            .map(|b| b.0)
399            .map_err(|e| e.to_string())
400    }
401
402    fn as_int(value: Value) -> Result<i64, String> {
403        serde_json::from_value::<Int>(value)
404            .map(|i| i.0)
405            .map_err(|e| e.to_string())
406    }
407
408    fn as_float(value: Value) -> Result<f64, String> {
409        serde_json::from_value::<Float>(value)
410            .map(|f| f.0)
411            .map_err(|e| e.to_string())
412    }
413
414    fn as_string(value: Value) -> Result<String, String> {
415        serde_json::from_value::<Str>(value)
416            .map(|s| s.0)
417            .map_err(|e| e.to_string())
418    }
419
420    #[test]
421    fn bool_accepts_every_spelling_go_accepts() {
422        for truthy in [
423            json!(true),
424            json!("true"),
425            json!("True"),
426            json!("TRUE"),
427            json!("t"),
428            json!("T"),
429            json!("1"),
430        ] {
431            assert_eq!(as_bool(truthy.clone()), Ok(true), "{truthy}");
432        }
433        for falsy in [
434            json!(false),
435            json!("false"),
436            json!("False"),
437            json!("FALSE"),
438            json!("f"),
439            json!("F"),
440            json!("0"),
441        ] {
442            assert_eq!(as_bool(falsy.clone()), Ok(false), "{falsy}");
443        }
444
445        // Any non-zero number is truthy, and a null reads as the zero value.
446        assert_eq!(as_bool(json!(2)), Ok(true));
447        assert_eq!(as_bool(json!(-1)), Ok(true));
448        assert_eq!(as_bool(json!(1.0)), Ok(true));
449        assert_eq!(as_bool(json!(0.0)), Ok(false));
450        assert_eq!(as_bool(json!(null)), Ok(false));
451    }
452
453    #[test]
454    fn bool_rejects_what_go_rejects() {
455        // `strconv.ParseBool` accepts none of these.
456        for rejected in [json!("yes"), json!("on"), json!(""), json!([true]), json!({"a": true})] {
457            assert!(as_bool(rejected.clone()).is_err(), "{rejected}");
458        }
459    }
460
461    #[test]
462    fn integer_accepts_numeric_strings_floats_and_booleans() {
463        assert_eq!(as_int(json!(8125)), Ok(8125));
464        assert_eq!(as_int(json!("8125")), Ok(8125));
465        assert_eq!(as_int(json!(" -7 ")), Ok(-7));
466        assert_eq!(as_int(json!(true)), Ok(1));
467        assert_eq!(as_int(json!(null)), Ok(0));
468
469        // `cast` drops an all-zero fraction from a numeric string.
470        assert_eq!(as_int(json!("8125.0")), Ok(8125));
471        assert_eq!(as_int(json!("8125.000")), Ok(8125));
472        assert_eq!(as_int(json!("-8125.0")), Ok(-8125));
473
474        // Go truncates toward zero rather than rounding.
475        assert_eq!(as_int(json!(10.9)), Ok(10));
476        assert_eq!(as_int(json!(-10.9)), Ok(-10));
477    }
478
479    #[test]
480    fn integer_rejects_unparseable_and_out_of_range_values() {
481        for rejected in [
482            json!("8125ms"),
483            json!(""),
484            json!("0x1f"),
485            json!("8125.5"),
486            json!("8125."),
487            json!(1e300),
488            json!(["8125"]),
489        ] {
490            assert!(as_int(rejected.clone()).is_err(), "{rejected}");
491        }
492    }
493
494    #[test]
495    fn number_accepts_numeric_strings_integers_and_booleans() {
496        assert_eq!(as_float(json!(1.5)), Ok(1.5));
497        assert_eq!(as_float(json!("1.5")), Ok(1.5));
498        assert_eq!(as_float(json!(2)), Ok(2.0));
499        assert_eq!(as_float(json!(true)), Ok(1.0));
500        assert_eq!(as_float(json!(null)), Ok(0.0));
501        assert!(as_float(json!("half")).is_err());
502    }
503
504    #[test]
505    fn string_accepts_every_scalar() {
506        assert_eq!(as_string(json!("datadog_only")), Ok("datadog_only".to_owned()));
507        assert_eq!(as_string(json!(true)), Ok("true".to_owned()));
508        assert_eq!(as_string(json!(false)), Ok("false".to_owned()));
509        assert_eq!(as_string(json!(10485760)), Ok("10485760".to_owned()));
510        assert_eq!(as_string(json!(-1)), Ok("-1".to_owned()));
511        assert_eq!(as_string(json!(10.5)), Ok("10.5".to_owned()));
512        assert_eq!(as_string(json!(null)), Ok(String::new()));
513
514        // A float with no fractional part renders without one, as Go's shortest form does.
515        assert_eq!(as_string(json!(1.0)), Ok("1".to_owned()));
516    }
517
518    #[test]
519    fn string_rejects_compound_values() {
520        for rejected in [json!(["a"]), json!({"a": "b"})] {
521            assert!(as_string(rejected.clone()).is_err(), "{rejected}");
522        }
523    }
524
525    #[test]
526    fn string_map_coerces_scalar_values() {
527        let values = serde_json::from_value::<StringMap>(json!({
528            "bool": true,
529            "integer": 3,
530            "null": null,
531            "string": "datadog_only"
532        }))
533        .expect("scalar values deserialize")
534        .0;
535
536        assert_eq!(values["bool"], "true");
537        assert_eq!(values["integer"], "3");
538        assert_eq!(values["null"], "");
539        assert_eq!(values["string"], "datadog_only");
540        assert!(serde_json::from_value::<StringMap>(json!({ "compound": [] })).is_err());
541    }
542
543    #[test]
544    fn optional_string_distinguishes_null_from_a_coerced_scalar() {
545        let absent = serde_json::from_value::<OptStr>(json!(null))
546            .expect("null deserializes")
547            .0;
548        assert_eq!(absent, None);
549
550        let coerced = serde_json::from_value::<OptStr>(json!(3))
551            .expect("scalar deserializes")
552            .0;
553        assert_eq!(coerced, Some("3".to_owned()));
554    }
555}