2020//! Vec<String>>` may carry each map value as either one scalar string or a sequence. Handling those
2121//! shapes at the deserialization boundary keeps downstream types consistent; see `stringlistize`
2222//! and `crate::list_de`.
23+ //!
24+ //! Scalar leaves get the Agent's own type coercion for the same reason: the Agent reads every
25+ //! setting through a cast against its declared type, so each leaf must accept the spellings that
26+ //! cast accepts. See `permissivize` and `crate::cast_de`.
2327use std:: collections:: { BTreeMap , HashMap , HashSet } ;
2428use std:: path:: Path ;
2529
2630use datadog_agent_config_overlay_model:: schema_gen:: { FieldInfo , FieldType } ;
27- use datadog_agent_config_overlay_model:: { load_resolved_schema, InputShape , KnownEntry , SchemaOverlay } ;
31+ use datadog_agent_config_overlay_model:: { load_resolved_schema, KnownEntry , SchemaOverlay } ;
2832use indexmap:: IndexMap ;
2933use serde_json:: { Map , Value } ;
3034use syn:: visit_mut:: { self , VisitMut } ;
@@ -75,8 +79,7 @@ pub fn generate(
7579 let pruned_schema = Value :: Object ( root) ;
7680
7781 let aliases = field_aliases ( overlay) ;
78- let input_shapes = field_input_shapes ( overlay) ;
79- let body = render ( pruned_schema, & aliases, & durations, & input_shapes) ;
82+ let body = render ( pruned_schema, & aliases, & durations) ;
8083
8184 let mut out = String :: new ( ) ;
8285 out. push_str ( "// @generated by build.rs from core_schema.yaml + schema_overlay.yaml — DO NOT EDIT\n " ) ;
@@ -111,22 +114,6 @@ fn field_aliases(overlay: &SchemaOverlay) -> HashMap<String, Vec<String>> {
111114 map
112115}
113116
114- /// Collect `dotted key -> InputShape` for every supported entry that declares `input_shape`.
115- fn field_input_shapes ( overlay : & SchemaOverlay ) -> BTreeMap < String , InputShape > {
116- let mut map = BTreeMap :: new ( ) ;
117- for ( key, entry) in & overlay. inventory {
118- let shape = match entry {
119- KnownEntry :: Full ( f) => f. input_shape ,
120- KnownEntry :: Partial ( p) => p. input_shape ,
121- _ => None ,
122- } ;
123- if let Some ( shape) = shape {
124- map. insert ( key. clone ( ) , shape) ;
125- }
126- }
127- map
128- }
129-
130117/// Collect the dotted paths of every `support: full` / `support: partial` overlay entry.
131118fn supported_keys ( overlay : & SchemaOverlay ) -> HashSet < String > {
132119 overlay
@@ -223,10 +210,7 @@ fn duration_default_nanos(info: Option<&FieldInfo>, path: &str) -> u64 {
223210}
224211
225212/// Run typify over the pruned schema and pretty-print the generated module body.
226- fn render (
227- pruned_schema : Value , aliases : & HashMap < String , Vec < String > > , durations : & BTreeMap < String , u64 > ,
228- input_shapes : & BTreeMap < String , InputShape > ,
229- ) -> String {
213+ fn render ( pruned_schema : Value , aliases : & HashMap < String , Vec < String > > , durations : & BTreeMap < String , u64 > ) -> String {
230214 let root_schema: schemars:: schema:: RootSchema =
231215 serde_json:: from_value ( pruned_schema) . expect ( "pruned schema is not a valid JSON Schema document" ) ;
232216
@@ -258,7 +242,7 @@ fn render(
258242 stringlistize ( & mut file) ;
259243 strip_section_prefixes ( & mut file) ;
260244 durationize ( & mut file, durations) ;
261- inject_input_shapes ( & mut file, input_shapes ) ;
245+ permissivize ( & mut file) ;
262246
263247 let rendered = blank_lines_between_fields ( & prettyplease:: unparse ( & file) ) ;
264248 let rendered = blank_lines_between_items ( & rendered) ;
@@ -566,18 +550,20 @@ fn option_section_inner(ty: &syn::Type) -> Option<syn::Type> {
566550 }
567551}
568552
569- /// Attach the string-or-integer tolerant deserializer to every leaf whose overlay entry declares
570- /// `input_shape: string_or_integer`.
553+ /// Give every scalar leaf the coercion the Agent applies when it reads that leaf's declared type.
571554///
572- /// The target leaf is located by its full dotted path, navigating section structs from the root, so
573- /// two like-named leaves in different sections never collide (unlike a bare field-name match). The
574- /// leaf must be a plain `String` field; anything else is an overlay/schema mismatch and fails the
575- /// build. Runs after `strip_section_prefixes`, so section structs already carry their bare names.
576- fn inject_input_shapes ( file : & mut syn:: File , input_shapes : & BTreeMap < String , InputShape > ) {
577- if input_shapes. is_empty ( ) {
578- return ;
579- }
580-
555+ /// The Agent casts a stored value to the accessor's type, so a leaf's permissiveness follows from its
556+ /// schema type alone and needs no per-key metadata: `crate::cast_de` holds one coercion per type and
557+ /// this attaches it by the leaf's generated Rust type, which typify derived from that schema type.
558+ ///
559+ /// Every field is classified, and an unrecognized shape fails the build. A schema change that
560+ /// introduces a new leaf type must then decide how that type coerces instead of silently shipping a
561+ /// leaf that rejects input the Agent accepts. Runs after `durationize` and `stringlistize`, whose
562+ /// leaves carry their own shape-tolerant readers.
563+ // TODO: a leaf the Agent reads through an accessor of a different type than the schema declares
564+ // cannot be resolved from the schema type alone, and would need per-key overlay metadata. Add it back
565+ // if such a leaf turns up.
566+ fn permissivize ( file : & mut syn:: File ) {
581567 let struct_names: HashSet < String > = file
582568 . items
583569 . iter ( )
@@ -587,26 +573,114 @@ fn inject_input_shapes(file: &mut syn::File, input_shapes: &BTreeMap<String, Inp
587573 } )
588574 . collect ( ) ;
589575
590- for ( dotted, shape) in input_shapes {
591- let ( owner_struct, leaf_field) = resolve_owner_and_leaf ( file, & struct_names, dotted) ;
592- let field = find_field_mut ( file, & owner_struct, & leaf_field) . unwrap_or_else ( || {
593- panic ! ( "input_shape key `{dotted}` resolves to unknown field `{owner_struct}.{leaf_field}`" )
594- } ) ;
595- match shape {
596- InputShape :: StringOrInteger => {
597- assert ! (
598- is_plain_string( & field. ty) ,
599- "input_shape `string_or_integer` on `{dotted}`, but its generated field is not a plain \
600- `String`; this metadata only applies to schema-string leaves"
601- ) ;
602- field. attrs . push ( parse_quote ! (
603- #[ serde( deserialize_with = "crate::string_de::deserialize_string_or_integer" ) ]
604- ) ) ;
605- }
576+ for item in & mut file. items {
577+ let Item :: Struct ( s) = item else { continue } ;
578+ let syn:: Fields :: Named ( fields) = & mut s. fields else {
579+ continue ;
580+ } ;
581+ for field in & mut fields. named {
582+ let name = field. ident . as_ref ( ) . expect ( "a named field has an identifier" ) ;
583+ let deserializer = match leaf_kind ( & field. ty , & struct_names) {
584+ LeafKind :: Bool => "crate::cast_de::deserialize_bool" ,
585+ LeafKind :: Integer => "crate::cast_de::deserialize_i64" ,
586+ LeafKind :: Number => "crate::cast_de::deserialize_f64" ,
587+ LeafKind :: Text => "crate::cast_de::deserialize_string" ,
588+ LeafKind :: OptionalText => "crate::cast_de::deserialize_optional_string" ,
589+ LeafKind :: Exempt => continue ,
590+ LeafKind :: Unknown => panic ! (
591+ "field `{}.{name}` has no declared coercion; classify its type in `leaf_kind` and \
592+ give that type a coercion in `crate::cast_de`",
593+ s. ident
594+ ) ,
595+ } ;
596+ field
597+ . attrs
598+ . push ( parse_quote ! ( #[ serde( deserialize_with = #deserializer) ] ) ) ;
606599 }
607600 }
608601}
609602
603+ /// How one generated field accepts input.
604+ enum LeafKind {
605+ Bool ,
606+ Integer ,
607+ Number ,
608+ Text ,
609+ OptionalText ,
610+ /// A nested section, or a leaf whose shape another pass or its own consumer handles.
611+ Exempt ,
612+ Unknown ,
613+ }
614+
615+ /// Classify a generated field by the type typify derived from its schema type.
616+ fn leaf_kind ( ty : & syn:: Type , struct_names : & HashSet < String > ) -> LeafKind {
617+ if section_struct_name ( ty, struct_names) . is_some ( ) {
618+ return LeafKind :: Exempt ;
619+ }
620+ if is_plain_string ( ty) {
621+ return LeafKind :: Text ;
622+ }
623+ if option_inner ( ty) . is_some_and ( is_plain_string) {
624+ return LeafKind :: OptionalText ;
625+ }
626+ if is_vec_string ( ty) || is_string_map_vec_string ( ty) || is_json_container ( ty) || is_duration ( ty) {
627+ return LeafKind :: Exempt ;
628+ }
629+ match plain_ident ( ty) {
630+ Some ( ident) if ident == "bool" => LeafKind :: Bool ,
631+ Some ( ident) if ident == "i64" => LeafKind :: Integer ,
632+ Some ( ident) if ident == "f64" => LeafKind :: Number ,
633+ _ => LeafKind :: Unknown ,
634+ }
635+ }
636+
637+ /// The final path segment of a type carrying no generic arguments (`bool`, `i64`, `String`, ...).
638+ fn plain_ident ( ty : & syn:: Type ) -> Option < & syn:: Ident > {
639+ let syn:: Type :: Path ( tp) = ty else { return None } ;
640+ let seg = tp. path . segments . last ( ) ?;
641+ matches ! ( seg. arguments, syn:: PathArguments :: None ) . then_some ( & seg. ident )
642+ }
643+
644+ /// If `ty` is `Option<T>`, return `T`.
645+ fn option_inner ( ty : & syn:: Type ) -> Option < & syn:: Type > {
646+ let syn:: Type :: Path ( tp) = ty else { return None } ;
647+ let last = tp. path . segments . last ( ) ?;
648+ if last. ident != "Option" {
649+ return None ;
650+ }
651+ let syn:: PathArguments :: AngleBracketed ( args) = & last. arguments else {
652+ return None ;
653+ } ;
654+ match args. args . first ( ) ? {
655+ syn:: GenericArgument :: Type ( inner) => Some ( inner) ,
656+ _ => None ,
657+ }
658+ }
659+
660+ /// Returns whether `ty` is a raw JSON container, the shape an object-typed or heterogeneous-array
661+ /// leaf keeps so its own consumer can interpret it.
662+ fn is_json_container ( ty : & syn:: Type ) -> bool {
663+ let syn:: Type :: Path ( tp) = ty else { return false } ;
664+ if tp. path . segments . iter ( ) . any ( |seg| seg. ident == "serde_json" ) {
665+ return true ;
666+ }
667+ let Some ( last) = tp. path . segments . last ( ) else {
668+ return false ;
669+ } ;
670+ if last. ident != "Vec" {
671+ return false ;
672+ }
673+ let syn:: PathArguments :: AngleBracketed ( args) = & last. arguments else {
674+ return false ;
675+ } ;
676+ matches ! ( args. args. first( ) , Some ( syn:: GenericArgument :: Type ( inner) ) if is_json_container( inner) )
677+ }
678+
679+ /// Returns whether `ty` is the `std::time::Duration` that `durationize` installs.
680+ fn is_duration ( ty : & syn:: Type ) -> bool {
681+ plain_ident ( ty) . is_some_and ( |ident| ident == "Duration" )
682+ }
683+
610684/// Resolve a dotted key to the `(owner struct name, leaf field name)` in the generated tree.
611685///
612686/// Every non-final segment is a non-optional nested section struct (its type names one of the
@@ -620,9 +694,9 @@ fn resolve_owner_and_leaf(file: &syn::File, struct_names: &HashSet<String>, dott
620694 return ( current, ( * segment) . to_string ( ) ) ;
621695 }
622696 let field = find_field ( file, & current, segment)
623- . unwrap_or_else ( || panic ! ( "input_shape key `{dotted}`: field `{segment}` not found on struct `{current}`" ) ) ;
697+ . unwrap_or_else ( || panic ! ( "key `{dotted}`: field `{segment}` not found on struct `{current}`" ) ) ;
624698 current = section_struct_name ( & field. ty , struct_names)
625- . unwrap_or_else ( || panic ! ( "input_shape key `{dotted}`: segment `{segment}` is not a nested section" ) ) ;
699+ . unwrap_or_else ( || panic ! ( "key `{dotted}`: segment `{segment}` is not a nested section" ) ) ;
626700 }
627701
628702 unreachable ! ( "a dotted key always has a final segment" ) ;
0 commit comments