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, marker::PhantomData};
21
22use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, 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 map, or a string holding a JSON-encoded map (`cast.ToStringMap*E`).
123///
124/// The Agent stores a map-typed environment variable as its raw string and decodes the JSON when the
125/// key is read, so its configuration stream carries the string.
126///
127/// A null is an empty map: the Agent's configuration stream can carry `null` for an empty or
128/// cleared map, and its map accessors read it as empty.
129/// # Errors
130///
131/// Returns an error when the value is neither a map, a null, nor a string that decodes to a map, or
132/// when a map value does not deserialize as `V`.
133pub(crate) fn deserialize_map_or_json_string<'de, D, V>(deserializer: D) -> Result<HashMap<String, V>, D::Error>
134where
135    D: Deserializer<'de>,
136    V: DeserializeOwned,
137{
138    struct MapOrJsonString<V>(PhantomData<V>);
139
140    impl<'de, V: DeserializeOwned> Visitor<'de> for MapOrJsonString<V> {
141        type Value = HashMap<String, V>;
142
143        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144            f.write_str("a map or a JSON-encoded map string")
145        }
146
147        fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
148            serde_json::from_str(value).map_err(|e| E::custom(format_args!("invalid JSON-encoded map: {e}")))
149        }
150
151        fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
152            HashMap::deserialize(de::value::MapAccessDeserializer::new(map))
153        }
154
155        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
156            Ok(HashMap::new())
157        }
158
159        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
160            Ok(HashMap::new())
161        }
162    }
163
164    deserializer.deserialize_any(MapOrJsonString(PhantomData))
165}
166
167/// Deserializes a string map, coercing each value as the Agent does.
168///
169/// # Errors
170///
171/// Returns an error when the map is malformed (see [`deserialize_map_or_json_string`]) or a value is
172/// not scalar.
173pub(crate) fn deserialize_string_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
174where
175    D: Deserializer<'de>,
176{
177    let values = deserialize_map_or_json_string::<_, serde_json::Value>(deserializer)?;
178    values
179        .into_iter()
180        .map(|(key, value)| {
181            cast_to_string(&value)
182                .map(|value| (key, value))
183                .map_err(de::Error::custom)
184        })
185        .collect()
186}
187
188/// Renders a JSON value as a `string` leaf (`cast.ToStringE`).
189///
190/// A witness method that receives a leaf as raw JSON, rather than as a generated field, renders it
191/// through this so that `1.0` and a null read as the Agent reads them (`"1"` and `""`) instead of as
192/// their JSON spelling.
193///
194/// # Errors
195///
196/// Returns an error for a compound value, the one shape the Agent cannot render as a string.
197pub fn cast_to_string(value: &::serde_json::Value) -> Result<String, String> {
198    value.deserialize_any(StringVisitor).map_err(|e| e.to_string())
199}
200
201/// Deserializes an optional `string` leaf, where an absent or null value stays `None`.
202///
203/// # Errors
204///
205/// Same as [`deserialize_string`] for a present value.
206pub(crate) fn deserialize_optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
207where
208    D: Deserializer<'de>,
209{
210    deserializer.deserialize_option(OptionalStringVisitor)
211}
212
213/// Deserializes an optional `integer` leaf, where an absent or null value stays `None`.
214///
215/// # Errors
216///
217/// Same as [`deserialize_i64`] for a present value.
218pub(crate) fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
219where
220    D: Deserializer<'de>,
221{
222    deserializer.deserialize_option(OptionalI64Visitor)
223}
224
225struct BoolVisitor;
226
227impl Visitor<'_> for BoolVisitor {
228    type Value = bool;
229
230    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        f.write_str("a boolean, a boolean string, or a number")
232    }
233
234    fn visit_bool<E: de::Error>(self, value: bool) -> Result<bool, E> {
235        Ok(value)
236    }
237
238    fn visit_i64<E: de::Error>(self, value: i64) -> Result<bool, E> {
239        Ok(value != 0)
240    }
241
242    fn visit_u64<E: de::Error>(self, value: u64) -> Result<bool, E> {
243        Ok(value != 0)
244    }
245
246    fn visit_f64<E: de::Error>(self, value: f64) -> Result<bool, E> {
247        Ok(value != 0.0)
248    }
249
250    fn visit_str<E: de::Error>(self, value: &str) -> Result<bool, E> {
251        parse_bool(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
252    }
253
254    // `cast` maps a nil to the zero value, which for a configuration leaf means an explicit
255    // `key: null` reads as if the key were unset.
256    fn visit_unit<E: de::Error>(self) -> Result<bool, E> {
257        Ok(false)
258    }
259}
260
261struct I64Visitor;
262
263impl Visitor<'_> for I64Visitor {
264    type Value = i64;
265
266    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        f.write_str("an integer, a numeric string, or a boolean")
268    }
269
270    fn visit_bool<E: de::Error>(self, value: bool) -> Result<i64, E> {
271        Ok(i64::from(value))
272    }
273
274    fn visit_i64<E: de::Error>(self, value: i64) -> Result<i64, E> {
275        Ok(value)
276    }
277
278    fn visit_u64<E: de::Error>(self, value: u64) -> Result<i64, E> {
279        i64::try_from(value).map_err(|_| E::invalid_value(Unexpected::Unsigned(value), &self))
280    }
281
282    // Go's `int(float64)` truncation, which is what the Agent reads for a leaf written `10.5`.
283    fn visit_f64<E: de::Error>(self, value: f64) -> Result<i64, E> {
284        if !value.is_finite() || value < i64::MIN as f64 || value > i64::MAX as f64 {
285            return Err(E::invalid_value(Unexpected::Float(value), &self));
286        }
287        Ok(value.trunc() as i64)
288    }
289
290    fn visit_str<E: de::Error>(self, value: &str) -> Result<i64, E> {
291        parse_i64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
292    }
293
294    fn visit_unit<E: de::Error>(self) -> Result<i64, E> {
295        Ok(0)
296    }
297}
298
299struct F64Visitor;
300
301impl Visitor<'_> for F64Visitor {
302    type Value = f64;
303
304    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.write_str("a number, a numeric string, or a boolean")
306    }
307
308    fn visit_bool<E: de::Error>(self, value: bool) -> Result<f64, E> {
309        Ok(if value { 1.0 } else { 0.0 })
310    }
311
312    fn visit_i64<E: de::Error>(self, value: i64) -> Result<f64, E> {
313        Ok(value as f64)
314    }
315
316    fn visit_u64<E: de::Error>(self, value: u64) -> Result<f64, E> {
317        Ok(value as f64)
318    }
319
320    fn visit_f64<E: de::Error>(self, value: f64) -> Result<f64, E> {
321        Ok(value)
322    }
323
324    fn visit_str<E: de::Error>(self, value: &str) -> Result<f64, E> {
325        parse_f64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
326    }
327
328    fn visit_unit<E: de::Error>(self) -> Result<f64, E> {
329        Ok(0.0)
330    }
331}
332
333struct StringVisitor;
334
335impl Visitor<'_> for StringVisitor {
336    type Value = String;
337
338    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        f.write_str("a string, a boolean, or a number")
340    }
341
342    fn visit_bool<E: de::Error>(self, value: bool) -> Result<String, E> {
343        Ok(value.to_string())
344    }
345
346    fn visit_i64<E: de::Error>(self, value: i64) -> Result<String, E> {
347        Ok(value.to_string())
348    }
349
350    fn visit_u64<E: de::Error>(self, value: u64) -> Result<String, E> {
351        Ok(value.to_string())
352    }
353
354    // Rust's `Display` for `f64` matches Go's `FormatFloat(v, 'f', -1, 64)`: the shortest form that
355    // round-trips, never in exponent notation.
356    fn visit_f64<E: de::Error>(self, value: f64) -> Result<String, E> {
357        Ok(value.to_string())
358    }
359
360    fn visit_str<E: de::Error>(self, value: &str) -> Result<String, E> {
361        Ok(value.to_owned())
362    }
363
364    fn visit_string<E: de::Error>(self, value: String) -> Result<String, E> {
365        Ok(value)
366    }
367
368    fn visit_unit<E: de::Error>(self) -> Result<String, E> {
369        Ok(String::new())
370    }
371}
372
373struct OptionalStringVisitor;
374
375impl<'de> Visitor<'de> for OptionalStringVisitor {
376    type Value = Option<String>;
377
378    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        f.write_str("a string, a boolean, a number, or null")
380    }
381
382    fn visit_none<E: de::Error>(self) -> Result<Option<String>, E> {
383        Ok(None)
384    }
385
386    fn visit_unit<E: de::Error>(self) -> Result<Option<String>, E> {
387        Ok(None)
388    }
389
390    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<String>, D::Error> {
391        deserializer.deserialize_any(StringVisitor).map(Some)
392    }
393}
394
395struct OptionalI64Visitor;
396
397impl<'de> Visitor<'de> for OptionalI64Visitor {
398    type Value = Option<i64>;
399
400    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        f.write_str("an integer, a numeric string, a boolean, or null")
402    }
403
404    fn visit_none<E: de::Error>(self) -> Result<Option<i64>, E> {
405        Ok(None)
406    }
407
408    fn visit_unit<E: de::Error>(self) -> Result<Option<i64>, E> {
409        Ok(None)
410    }
411
412    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<i64>, D::Error> {
413        deserializer.deserialize_any(I64Visitor).map(Some)
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use serde::Deserialize;
420    use serde_json::{json, Value};
421
422    use super::*;
423
424    #[derive(Deserialize)]
425    struct Bool(#[serde(deserialize_with = "deserialize_bool")] bool);
426
427    #[derive(Deserialize)]
428    struct Int(#[serde(deserialize_with = "deserialize_i64")] i64);
429
430    #[derive(Deserialize)]
431    struct Float(#[serde(deserialize_with = "deserialize_f64")] f64);
432
433    #[derive(Deserialize)]
434    struct Str(#[serde(deserialize_with = "deserialize_string")] String);
435
436    #[derive(Deserialize)]
437    struct OptStr(#[serde(deserialize_with = "deserialize_optional_string")] Option<String>);
438
439    #[derive(Deserialize)]
440    struct StringMap(#[serde(deserialize_with = "deserialize_string_map")] HashMap<String, String>);
441
442    fn as_bool(value: Value) -> Result<bool, String> {
443        serde_json::from_value::<Bool>(value)
444            .map(|b| b.0)
445            .map_err(|e| e.to_string())
446    }
447
448    fn as_int(value: Value) -> Result<i64, String> {
449        serde_json::from_value::<Int>(value)
450            .map(|i| i.0)
451            .map_err(|e| e.to_string())
452    }
453
454    fn as_float(value: Value) -> Result<f64, String> {
455        serde_json::from_value::<Float>(value)
456            .map(|f| f.0)
457            .map_err(|e| e.to_string())
458    }
459
460    fn as_string(value: Value) -> Result<String, String> {
461        serde_json::from_value::<Str>(value)
462            .map(|s| s.0)
463            .map_err(|e| e.to_string())
464    }
465
466    #[test]
467    fn bool_accepts_every_spelling_go_accepts() {
468        for truthy in [
469            json!(true),
470            json!("true"),
471            json!("True"),
472            json!("TRUE"),
473            json!("t"),
474            json!("T"),
475            json!("1"),
476        ] {
477            assert_eq!(as_bool(truthy.clone()), Ok(true), "{truthy}");
478        }
479        for falsy in [
480            json!(false),
481            json!("false"),
482            json!("False"),
483            json!("FALSE"),
484            json!("f"),
485            json!("F"),
486            json!("0"),
487        ] {
488            assert_eq!(as_bool(falsy.clone()), Ok(false), "{falsy}");
489        }
490
491        // Any non-zero number is truthy, and a null reads as the zero value.
492        assert_eq!(as_bool(json!(2)), Ok(true));
493        assert_eq!(as_bool(json!(-1)), Ok(true));
494        assert_eq!(as_bool(json!(1.0)), Ok(true));
495        assert_eq!(as_bool(json!(0.0)), Ok(false));
496        assert_eq!(as_bool(json!(null)), Ok(false));
497    }
498
499    #[test]
500    fn bool_rejects_what_go_rejects() {
501        // `strconv.ParseBool` accepts none of these.
502        for rejected in [json!("yes"), json!("on"), json!(""), json!([true]), json!({"a": true})] {
503            assert!(as_bool(rejected.clone()).is_err(), "{rejected}");
504        }
505    }
506
507    #[test]
508    fn integer_accepts_numeric_strings_floats_and_booleans() {
509        assert_eq!(as_int(json!(8125)), Ok(8125));
510        assert_eq!(as_int(json!("8125")), Ok(8125));
511        assert_eq!(as_int(json!(" -7 ")), Ok(-7));
512        assert_eq!(as_int(json!(true)), Ok(1));
513        assert_eq!(as_int(json!(null)), Ok(0));
514
515        // `cast` drops an all-zero fraction from a numeric string.
516        assert_eq!(as_int(json!("8125.0")), Ok(8125));
517        assert_eq!(as_int(json!("8125.000")), Ok(8125));
518        assert_eq!(as_int(json!("-8125.0")), Ok(-8125));
519
520        // Go truncates toward zero rather than rounding.
521        assert_eq!(as_int(json!(10.9)), Ok(10));
522        assert_eq!(as_int(json!(-10.9)), Ok(-10));
523    }
524
525    #[test]
526    fn integer_rejects_unparseable_and_out_of_range_values() {
527        for rejected in [
528            json!("8125ms"),
529            json!(""),
530            json!("0x1f"),
531            json!("8125.5"),
532            json!("8125."),
533            json!(1e300),
534            json!(["8125"]),
535        ] {
536            assert!(as_int(rejected.clone()).is_err(), "{rejected}");
537        }
538    }
539
540    #[test]
541    fn number_accepts_numeric_strings_integers_and_booleans() {
542        assert_eq!(as_float(json!(1.5)), Ok(1.5));
543        assert_eq!(as_float(json!("1.5")), Ok(1.5));
544        assert_eq!(as_float(json!(2)), Ok(2.0));
545        assert_eq!(as_float(json!(true)), Ok(1.0));
546        assert_eq!(as_float(json!(null)), Ok(0.0));
547        assert!(as_float(json!("half")).is_err());
548    }
549
550    #[test]
551    fn string_accepts_every_scalar() {
552        assert_eq!(as_string(json!("datadog_only")), Ok("datadog_only".to_owned()));
553        assert_eq!(as_string(json!(true)), Ok("true".to_owned()));
554        assert_eq!(as_string(json!(false)), Ok("false".to_owned()));
555        assert_eq!(as_string(json!(10485760)), Ok("10485760".to_owned()));
556        assert_eq!(as_string(json!(-1)), Ok("-1".to_owned()));
557        assert_eq!(as_string(json!(10.5)), Ok("10.5".to_owned()));
558        assert_eq!(as_string(json!(null)), Ok(String::new()));
559
560        // A float with no fractional part renders without one, as Go's shortest form does.
561        assert_eq!(as_string(json!(1.0)), Ok("1".to_owned()));
562    }
563
564    #[test]
565    fn string_rejects_compound_values() {
566        for rejected in [json!(["a"]), json!({"a": "b"})] {
567            assert!(as_string(rejected.clone()).is_err(), "{rejected}");
568        }
569    }
570
571    #[test]
572    fn string_map_coerces_scalar_values() {
573        let values = serde_json::from_value::<StringMap>(json!({
574            "bool": true,
575            "integer": 3,
576            "null": null,
577            "string": "datadog_only"
578        }))
579        .expect("scalar values deserialize")
580        .0;
581
582        assert_eq!(values["bool"], "true");
583        assert_eq!(values["integer"], "3");
584        assert_eq!(values["null"], "");
585        assert_eq!(values["string"], "datadog_only");
586        assert!(serde_json::from_value::<StringMap>(json!({ "compound": [] })).is_err());
587    }
588
589    #[test]
590    fn string_map_accepts_a_json_encoded_string() {
591        let values = serde_json::from_value::<StringMap>(json!(r#"{"bool": true, "string": "datadog_only"}"#))
592            .expect("JSON-encoded map deserializes")
593            .0;
594
595        assert_eq!(values["bool"], "true");
596        assert_eq!(values["string"], "datadog_only");
597        for rejected in [json!("not json"), json!(r#"["a"]"#), json!(r#"{"compound": []}"#)] {
598            assert!(
599                serde_json::from_value::<StringMap>(rejected.clone()).is_err(),
600                "{rejected}"
601            );
602        }
603    }
604
605    #[test]
606    fn string_map_reads_null_as_empty() {
607        for empty in [json!(null), json!({})] {
608            let values = serde_json::from_value::<StringMap>(empty.clone())
609                .expect("empty map deserializes")
610                .0;
611            assert!(values.is_empty(), "{empty}");
612        }
613    }
614
615    #[test]
616    fn optional_string_distinguishes_null_from_a_coerced_scalar() {
617        let absent = serde_json::from_value::<OptStr>(json!(null))
618            .expect("null deserializes")
619            .0;
620        assert_eq!(absent, None);
621
622        let coerced = serde_json::from_value::<OptStr>(json!(3))
623            .expect("scalar deserializes")
624            .0;
625        assert_eq!(coerced, Some("3".to_owned()));
626    }
627}