Skip to content

Commit f21f659

Browse files
committed
fixup: accept the float and zero-fraction forms cast accepts
`cast.ToBoolE` has a float branch, and `cast.ToInt64E` runs a string through `trimZeroDecimal` before parsing it, so the Agent reads `dogstatsd_non_local_traffic: 1.0` as true and `dogstatsd_port: "8125.0"` as 8125. Both were rejected here. Also collapse the Vale vocabulary entry to `deserializer(s?)` and cut the skill section down to the rule an agent needs: fix the leaf's schema type, never hand-write a per-key tolerant deserializer.
1 parent 65575d2 commit f21f659

4 files changed

Lines changed: 43 additions & 28 deletions

File tree

.claude/skills/config-system/SKILL.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,10 @@ section rather than renaming it onto one.
125125

126126
## Scalar leaf coercion
127127

128-
The Agent reads every setting by casting whatever is stored to the accessor's type (`GetBool`,
129-
`GetInt`, `GetString`, ...), so a leaf's permissiveness follows from its declared type, not from the
130-
key: `dogstatsd_port: "8125"`, or a `string`-typed leaf written as a YAML boolean, are configurations
131-
the Agent accepts. `datadog-agent/config/src/cast_de.rs` ports those casts, codegen attaches one to
132-
every scalar leaf by type (`permissivize`), and `env_decode` shares the same parsers, so every source
133-
accepts the same spellings. A value the cast cannot convert is a hard error rather than the Agent's
134-
silent zero value.
135-
136-
Do not hand-write a per-key tolerant deserializer for a schema-typed scalar; give the coercion to the
137-
type instead. A generated field whose type has no classification fails the build.
128+
Every scalar leaf already accepts what the Agent's cast accepts (`dogstatsd_port: "8125"`, a `string`
129+
leaf written as a YAML boolean), because codegen attaches a `cast_de.rs` coercion per schema type.
130+
When a value fails to deserialize, fix the leaf's schema type; never hand-write a per-key tolerant
131+
deserializer.
138132

139133
## Saluki-only values
140134

.vale/styles/config/vocabularies/technical/accept.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,7 @@ configurability
151151
cooldown
152152
crypto
153153
deserializable
154-
deserializer
155-
deserializers
154+
deserializer(s?)
156155
downcasted
157156
upcasted
158157
env

lib/datadog-agent/config/src/cast_de.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,20 @@ pub(crate) fn parse_bool(raw: &str) -> Result<bool, String> {
4141
///
4242
/// Returns a message naming the value when it is not a decimal integer.
4343
pub(crate) fn parse_i64(raw: &str) -> Result<i64, String> {
44-
raw.trim()
44+
trim_zero_decimal(raw.trim())
4545
.parse::<i64>()
4646
.map_err(|_| format!("invalid integer `{raw}`"))
4747
}
4848

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.
51+
fn 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+
4958
/// `cast.ToFloat64E` for a string.
5059
///
5160
/// # Errors
@@ -63,8 +72,8 @@ pub(crate) fn parse_f64(raw: &str) -> Result<f64, String> {
6372
///
6473
/// # Errors
6574
///
66-
/// Returns an error for a value the Agent cannot cast to a boolean: an unrecognized string, a
67-
/// floating-point number, or a compound value.
75+
/// Returns an error for a value the Agent cannot cast to a boolean: an unrecognized string or a
76+
/// compound value.
6877
pub(crate) fn deserialize_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
6978
where
7079
D: Deserializer<'de>,
@@ -128,7 +137,7 @@ impl Visitor<'_> for BoolVisitor {
128137
type Value = bool;
129138

130139
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131-
f.write_str("a boolean, a boolean string, or an integer")
140+
f.write_str("a boolean, a boolean string, or a number")
132141
}
133142

134143
fn visit_bool<E: de::Error>(self, value: bool) -> Result<bool, E> {
@@ -143,6 +152,10 @@ impl Visitor<'_> for BoolVisitor {
143152
Ok(value != 0)
144153
}
145154

155+
fn visit_f64<E: de::Error>(self, value: f64) -> Result<bool, E> {
156+
Ok(value != 0.0)
157+
}
158+
146159
fn visit_str<E: de::Error>(self, value: &str) -> Result<bool, E> {
147160
parse_bool(value).map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
148161
}
@@ -359,23 +372,18 @@ mod tests {
359372
assert_eq!(as_bool(falsy.clone()), Ok(false), "{falsy}");
360373
}
361374

362-
// A non-zero integer is truthy, and a null reads as the zero value.
375+
// Any non-zero number is truthy, and a null reads as the zero value.
363376
assert_eq!(as_bool(json!(2)), Ok(true));
364377
assert_eq!(as_bool(json!(-1)), Ok(true));
378+
assert_eq!(as_bool(json!(1.0)), Ok(true));
379+
assert_eq!(as_bool(json!(0.0)), Ok(false));
365380
assert_eq!(as_bool(json!(null)), Ok(false));
366381
}
367382

368383
#[test]
369384
fn bool_rejects_what_go_rejects() {
370-
// `strconv.ParseBool` accepts none of these, and `cast` has no float branch for a boolean.
371-
for rejected in [
372-
json!("yes"),
373-
json!("on"),
374-
json!(""),
375-
json!(1.0),
376-
json!([true]),
377-
json!({"a": true}),
378-
] {
385+
// `strconv.ParseBool` accepts none of these.
386+
for rejected in [json!("yes"), json!("on"), json!(""), json!([true]), json!({"a": true})] {
379387
assert!(as_bool(rejected.clone()).is_err(), "{rejected}");
380388
}
381389
}
@@ -388,14 +396,27 @@ mod tests {
388396
assert_eq!(as_int(json!(true)), Ok(1));
389397
assert_eq!(as_int(json!(null)), Ok(0));
390398

399+
// `cast` drops an all-zero fraction from a numeric string.
400+
assert_eq!(as_int(json!("8125.0")), Ok(8125));
401+
assert_eq!(as_int(json!("8125.000")), Ok(8125));
402+
assert_eq!(as_int(json!("-8125.0")), Ok(-8125));
403+
391404
// Go truncates toward zero rather than rounding.
392405
assert_eq!(as_int(json!(10.9)), Ok(10));
393406
assert_eq!(as_int(json!(-10.9)), Ok(-10));
394407
}
395408

396409
#[test]
397410
fn integer_rejects_unparseable_and_out_of_range_values() {
398-
for rejected in [json!("8125ms"), json!(""), json!("0x1f"), json!(1e300), json!(["8125"])] {
411+
for rejected in [
412+
json!("8125ms"),
413+
json!(""),
414+
json!("0x1f"),
415+
json!("8125.5"),
416+
json!("8125."),
417+
json!(1e300),
418+
json!(["8125"]),
419+
] {
399420
assert!(as_int(rejected.clone()).is_err(), "{rejected}");
400421
}
401422
}

lib/datadog-agent/config/src/env_decode.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ mod tests {
258258
assert_eq!(decode("T", EnvDecode::Bool).unwrap(), json!(true));
259259
assert!(decode("yes", EnvDecode::Bool).is_err());
260260
assert_eq!(decode("9125", EnvDecode::Integer).unwrap(), json!(9125));
261-
assert!(decode("9125.0", EnvDecode::Integer).is_err());
261+
assert_eq!(decode("9125.0", EnvDecode::Integer).unwrap(), json!(9125));
262+
assert!(decode("9125.5", EnvDecode::Integer).is_err());
262263
assert_eq!(decode("1.5", EnvDecode::Float).unwrap(), json!(1.5));
263264
}
264265

0 commit comments

Comments
 (0)