datadog_agent_config/
cast_de.rs

1//! Deserialize a schema scalar leaf the way the Datadog Agent reads one.
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 a leaf accepts every spelling the
9//! Agent accepts, while the generated field keeps the schema's type. Codegen attaches these to every
10//! scalar leaf, and [`crate::env_decode`] routes environment strings through the same parsers, so one
11//! accept-set serves every configuration source.
12//!
13//! Two deliberate divergences from `cast`:
14//!
15//! - A value `cast` cannot convert is a hard error here. `cast.To*` swallows its error and yields the
16//!   zero value, so the Agent silently reads a malformed setting as `false`/`0`/`""`;
17//!   [`crate::env_decode`] already rejects such a value rather than losing it.
18//! - A numeric string is accepted in decimal only, not in Go's base-prefixed or underscored integer
19//!   literal forms. YAML and JSON parse those spellings into numbers before ADP sees them.
20
21use std::fmt;
22
23use serde::de::{self, Deserializer, Unexpected, Visitor};
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/// Renders a JSON value as a `string` leaf (`cast.ToStringE`).
123///
124/// A witness method that receives a leaf as raw JSON, rather than as a generated field, renders it
125/// through this so that `1.0` and a null read as the Agent reads them (`"1"` and `""`) instead of as
126/// their JSON spelling.
127///
128/// # Errors
129///
130/// Returns an error for a compound value, the one shape the Agent cannot render as a string.
131pub fn cast_to_string(value: &::serde_json::Value) -> Result<String, String> {
132    value.deserialize_any(StringVisitor).map_err(|e| e.to_string())
133}
134
135/// Deserializes an optional `string` leaf, where an absent or null value stays `None`.
136///
137/// # Errors
138///
139/// Same as [`deserialize_string`] for a present value.
140pub(crate) fn deserialize_optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
141where
142    D: Deserializer<'de>,
143{
144    deserializer.deserialize_option(OptionalStringVisitor)
145}
146
147/// Deserializes an optional `integer` leaf, where an absent or null value stays `None`.
148///
149/// # Errors
150///
151/// Same as [`deserialize_i64`] for a present value.
152pub(crate) fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
153where
154    D: Deserializer<'de>,
155{
156    deserializer.deserialize_option(OptionalI64Visitor)
157}
158
159struct BoolVisitor;
160
161impl Visitor<'_> for BoolVisitor {
162    type Value = bool;
163
164    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.write_str("a boolean, a boolean string, or a number")
166    }
167
168    fn visit_bool<E: de::Error>(self, value: bool) -> Result<bool, E> {
169        Ok(value)
170    }
171
172    fn visit_i64<E: de::Error>(self, value: i64) -> Result<bool, E> {
173        Ok(value != 0)
174    }
175
176    fn visit_u64<E: de::Error>(self, value: u64) -> Result<bool, E> {
177        Ok(value != 0)
178    }
179
180    fn visit_f64<E: de::Error>(self, value: f64) -> Result<bool, E> {
181        Ok(value != 0.0)
182    }
183
184    fn visit_str<E: de::Error>(self, value: &str) -> Result<bool, E> {
185        parse_bool(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
186    }
187
188    // `cast` maps a nil to the zero value, which for a configuration leaf means an explicit
189    // `key: null` reads as if the key were unset.
190    fn visit_unit<E: de::Error>(self) -> Result<bool, E> {
191        Ok(false)
192    }
193}
194
195struct I64Visitor;
196
197impl Visitor<'_> for I64Visitor {
198    type Value = i64;
199
200    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str("an integer, a numeric string, or a boolean")
202    }
203
204    fn visit_bool<E: de::Error>(self, value: bool) -> Result<i64, E> {
205        Ok(i64::from(value))
206    }
207
208    fn visit_i64<E: de::Error>(self, value: i64) -> Result<i64, E> {
209        Ok(value)
210    }
211
212    fn visit_u64<E: de::Error>(self, value: u64) -> Result<i64, E> {
213        i64::try_from(value).map_err(|_| E::invalid_value(Unexpected::Unsigned(value), &self))
214    }
215
216    // Go's `int(float64)` truncation, which is what the Agent reads for a leaf written `10.5`.
217    fn visit_f64<E: de::Error>(self, value: f64) -> Result<i64, E> {
218        if !value.is_finite() || value < i64::MIN as f64 || value > i64::MAX as f64 {
219            return Err(E::invalid_value(Unexpected::Float(value), &self));
220        }
221        Ok(value.trunc() as i64)
222    }
223
224    fn visit_str<E: de::Error>(self, value: &str) -> Result<i64, E> {
225        parse_i64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
226    }
227
228    fn visit_unit<E: de::Error>(self) -> Result<i64, E> {
229        Ok(0)
230    }
231}
232
233struct F64Visitor;
234
235impl Visitor<'_> for F64Visitor {
236    type Value = f64;
237
238    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        f.write_str("a number, a numeric string, or a boolean")
240    }
241
242    fn visit_bool<E: de::Error>(self, value: bool) -> Result<f64, E> {
243        Ok(if value { 1.0 } else { 0.0 })
244    }
245
246    fn visit_i64<E: de::Error>(self, value: i64) -> Result<f64, E> {
247        Ok(value as f64)
248    }
249
250    fn visit_u64<E: de::Error>(self, value: u64) -> Result<f64, E> {
251        Ok(value as f64)
252    }
253
254    fn visit_f64<E: de::Error>(self, value: f64) -> Result<f64, E> {
255        Ok(value)
256    }
257
258    fn visit_str<E: de::Error>(self, value: &str) -> Result<f64, E> {
259        parse_f64(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
260    }
261
262    fn visit_unit<E: de::Error>(self) -> Result<f64, E> {
263        Ok(0.0)
264    }
265}
266
267struct StringVisitor;
268
269impl Visitor<'_> for StringVisitor {
270    type Value = String;
271
272    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        f.write_str("a string, a boolean, or a number")
274    }
275
276    fn visit_bool<E: de::Error>(self, value: bool) -> Result<String, E> {
277        Ok(value.to_string())
278    }
279
280    fn visit_i64<E: de::Error>(self, value: i64) -> Result<String, E> {
281        Ok(value.to_string())
282    }
283
284    fn visit_u64<E: de::Error>(self, value: u64) -> Result<String, E> {
285        Ok(value.to_string())
286    }
287
288    // Rust's `Display` for `f64` matches Go's `FormatFloat(v, 'f', -1, 64)`: the shortest form that
289    // round-trips, never in exponent notation.
290    fn visit_f64<E: de::Error>(self, value: f64) -> Result<String, E> {
291        Ok(value.to_string())
292    }
293
294    fn visit_str<E: de::Error>(self, value: &str) -> Result<String, E> {
295        Ok(value.to_owned())
296    }
297
298    fn visit_string<E: de::Error>(self, value: String) -> Result<String, E> {
299        Ok(value)
300    }
301
302    fn visit_unit<E: de::Error>(self) -> Result<String, E> {
303        Ok(String::new())
304    }
305}
306
307struct OptionalStringVisitor;
308
309impl<'de> Visitor<'de> for OptionalStringVisitor {
310    type Value = Option<String>;
311
312    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        f.write_str("a string, a boolean, a number, or null")
314    }
315
316    fn visit_none<E: de::Error>(self) -> Result<Option<String>, E> {
317        Ok(None)
318    }
319
320    fn visit_unit<E: de::Error>(self) -> Result<Option<String>, E> {
321        Ok(None)
322    }
323
324    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<String>, D::Error> {
325        deserializer.deserialize_any(StringVisitor).map(Some)
326    }
327}
328
329struct OptionalI64Visitor;
330
331impl<'de> Visitor<'de> for OptionalI64Visitor {
332    type Value = Option<i64>;
333
334    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        f.write_str("an integer, a numeric string, a boolean, or null")
336    }
337
338    fn visit_none<E: de::Error>(self) -> Result<Option<i64>, E> {
339        Ok(None)
340    }
341
342    fn visit_unit<E: de::Error>(self) -> Result<Option<i64>, E> {
343        Ok(None)
344    }
345
346    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<i64>, D::Error> {
347        deserializer.deserialize_any(I64Visitor).map(Some)
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use serde::Deserialize;
354    use serde_json::{json, Value};
355
356    use super::*;
357
358    #[derive(Deserialize)]
359    struct Bool(#[serde(deserialize_with = "deserialize_bool")] bool);
360
361    #[derive(Deserialize)]
362    struct Int(#[serde(deserialize_with = "deserialize_i64")] i64);
363
364    #[derive(Deserialize)]
365    struct Float(#[serde(deserialize_with = "deserialize_f64")] f64);
366
367    #[derive(Deserialize)]
368    struct Str(#[serde(deserialize_with = "deserialize_string")] String);
369
370    #[derive(Deserialize)]
371    struct OptStr(#[serde(deserialize_with = "deserialize_optional_string")] Option<String>);
372
373    fn as_bool(value: Value) -> Result<bool, String> {
374        serde_json::from_value::<Bool>(value)
375            .map(|b| b.0)
376            .map_err(|e| e.to_string())
377    }
378
379    fn as_int(value: Value) -> Result<i64, String> {
380        serde_json::from_value::<Int>(value)
381            .map(|i| i.0)
382            .map_err(|e| e.to_string())
383    }
384
385    fn as_float(value: Value) -> Result<f64, String> {
386        serde_json::from_value::<Float>(value)
387            .map(|f| f.0)
388            .map_err(|e| e.to_string())
389    }
390
391    fn as_string(value: Value) -> Result<String, String> {
392        serde_json::from_value::<Str>(value)
393            .map(|s| s.0)
394            .map_err(|e| e.to_string())
395    }
396
397    #[test]
398    fn bool_accepts_every_spelling_go_accepts() {
399        for truthy in [
400            json!(true),
401            json!("true"),
402            json!("True"),
403            json!("TRUE"),
404            json!("t"),
405            json!("T"),
406            json!("1"),
407        ] {
408            assert_eq!(as_bool(truthy.clone()), Ok(true), "{truthy}");
409        }
410        for falsy in [
411            json!(false),
412            json!("false"),
413            json!("False"),
414            json!("FALSE"),
415            json!("f"),
416            json!("F"),
417            json!("0"),
418        ] {
419            assert_eq!(as_bool(falsy.clone()), Ok(false), "{falsy}");
420        }
421
422        // Any non-zero number is truthy, and a null reads as the zero value.
423        assert_eq!(as_bool(json!(2)), Ok(true));
424        assert_eq!(as_bool(json!(-1)), Ok(true));
425        assert_eq!(as_bool(json!(1.0)), Ok(true));
426        assert_eq!(as_bool(json!(0.0)), Ok(false));
427        assert_eq!(as_bool(json!(null)), Ok(false));
428    }
429
430    #[test]
431    fn bool_rejects_what_go_rejects() {
432        // `strconv.ParseBool` accepts none of these.
433        for rejected in [json!("yes"), json!("on"), json!(""), json!([true]), json!({"a": true})] {
434            assert!(as_bool(rejected.clone()).is_err(), "{rejected}");
435        }
436    }
437
438    #[test]
439    fn integer_accepts_numeric_strings_floats_and_booleans() {
440        assert_eq!(as_int(json!(8125)), Ok(8125));
441        assert_eq!(as_int(json!("8125")), Ok(8125));
442        assert_eq!(as_int(json!(" -7 ")), Ok(-7));
443        assert_eq!(as_int(json!(true)), Ok(1));
444        assert_eq!(as_int(json!(null)), Ok(0));
445
446        // `cast` drops an all-zero fraction from a numeric string.
447        assert_eq!(as_int(json!("8125.0")), Ok(8125));
448        assert_eq!(as_int(json!("8125.000")), Ok(8125));
449        assert_eq!(as_int(json!("-8125.0")), Ok(-8125));
450
451        // Go truncates toward zero rather than rounding.
452        assert_eq!(as_int(json!(10.9)), Ok(10));
453        assert_eq!(as_int(json!(-10.9)), Ok(-10));
454    }
455
456    #[test]
457    fn integer_rejects_unparseable_and_out_of_range_values() {
458        for rejected in [
459            json!("8125ms"),
460            json!(""),
461            json!("0x1f"),
462            json!("8125.5"),
463            json!("8125."),
464            json!(1e300),
465            json!(["8125"]),
466        ] {
467            assert!(as_int(rejected.clone()).is_err(), "{rejected}");
468        }
469    }
470
471    #[test]
472    fn number_accepts_numeric_strings_integers_and_booleans() {
473        assert_eq!(as_float(json!(1.5)), Ok(1.5));
474        assert_eq!(as_float(json!("1.5")), Ok(1.5));
475        assert_eq!(as_float(json!(2)), Ok(2.0));
476        assert_eq!(as_float(json!(true)), Ok(1.0));
477        assert_eq!(as_float(json!(null)), Ok(0.0));
478        assert!(as_float(json!("half")).is_err());
479    }
480
481    #[test]
482    fn string_accepts_every_scalar() {
483        assert_eq!(as_string(json!("datadog_only")), Ok("datadog_only".to_owned()));
484        assert_eq!(as_string(json!(true)), Ok("true".to_owned()));
485        assert_eq!(as_string(json!(false)), Ok("false".to_owned()));
486        assert_eq!(as_string(json!(10485760)), Ok("10485760".to_owned()));
487        assert_eq!(as_string(json!(-1)), Ok("-1".to_owned()));
488        assert_eq!(as_string(json!(10.5)), Ok("10.5".to_owned()));
489        assert_eq!(as_string(json!(null)), Ok(String::new()));
490
491        // A float with no fractional part renders without one, as Go's shortest form does.
492        assert_eq!(as_string(json!(1.0)), Ok("1".to_owned()));
493    }
494
495    #[test]
496    fn string_rejects_compound_values() {
497        for rejected in [json!(["a"]), json!({"a": "b"})] {
498            assert!(as_string(rejected.clone()).is_err(), "{rejected}");
499        }
500    }
501
502    #[test]
503    fn optional_string_distinguishes_null_from_a_coerced_scalar() {
504        let absent = serde_json::from_value::<OptStr>(json!(null))
505            .expect("null deserializes")
506            .0;
507        assert_eq!(absent, None);
508
509        let coerced = serde_json::from_value::<OptStr>(json!(3))
510            .expect("scalar deserializes")
511            .0;
512        assert_eq!(coerced, Some("3".to_owned()));
513    }
514}