datadog_agent_config_overlay_model/
schema_gen.rs1use std::path::Path;
10
11use indexmap::IndexMap;
12use serde_json::{Map, Value};
13
14pub enum FieldType {
19 String,
20 Bool,
21 Integer,
22 Float,
23 StringList,
24 Duration,
25 Unknown,
26}
27
28pub enum EnvParser {
34 Json,
35 CommaSeparated,
36 CsvCommaSeparated,
37 CommaAndSpaceSeparated,
38 CommaThenSpaceSeparated,
39 JsonListOrCommaSeparated,
40 JsonListOrSpaceSeparated,
41 TracesSpan,
42}
43
44impl EnvParser {
45 fn parse(name: &str) -> Self {
48 match name {
49 "json" => EnvParser::Json,
50 "comma_separated" => EnvParser::CommaSeparated,
51 "csv_comma_separated" => EnvParser::CsvCommaSeparated,
52 "comma_and_space_separated" => EnvParser::CommaAndSpaceSeparated,
53 "comma_then_space_separated" => EnvParser::CommaThenSpaceSeparated,
54 "json_list_or_comma_separated" => EnvParser::JsonListOrCommaSeparated,
55 "json_list_or_space_separated" => EnvParser::JsonListOrSpaceSeparated,
56 "traces_span" => EnvParser::TracesSpan,
57 other => panic!("unknown env_parser `{other}` in schema; add it to EnvParser and env_decode"),
58 }
59 }
60}
61
62pub enum EnvBinding {
70 None,
72 Standard,
74 Overridden(Vec<String>),
76}
77
78pub struct FieldInfo {
80 pub value_type: FieldType,
82 pub env: EnvBinding,
84 pub env_parser: Option<EnvParser>,
86 pub default: Option<String>,
88}
89
90pub fn load_schema(datadog_schema: &Path, otel_schema_dir: &Path) -> IndexMap<String, FieldInfo> {
95 let doc = crate::load_composed_schema(datadog_schema, otel_schema_dir)
96 .unwrap_or_else(|e| panic!("failed to load composed schema: {e}"));
97 load_schema_from_value(&doc)
98}
99
100pub fn load_schema_from_value(doc: &Value) -> IndexMap<String, FieldInfo> {
105 let properties = doc
106 .get("properties")
107 .and_then(|v| v.as_object())
108 .expect("schema root must have a 'properties' mapping");
109
110 let mut entries = Vec::new();
111 collect_entries(properties, &[], &mut entries);
112 entries.sort_by(|a, b| a.0.cmp(&b.0));
113
114 let mut map = IndexMap::new();
115 for (yaml_path, info) in entries {
116 map.insert(yaml_path, info);
117 }
118 map
119}
120
121fn collect_entries(mapping: &Map<String, Value>, path_parts: &[&str], out: &mut Vec<(String, FieldInfo)>) {
122 for (key, value) in mapping {
123 let mut parts = path_parts.to_vec();
124 parts.push(key);
125
126 let node_type = value.get("node_type").and_then(|v| v.as_str()).unwrap_or("");
129
130 match node_type {
131 "setting" => out.push(parse_setting(&parts, value)),
132 "section" => {
133 if let Some(props) = value.get("properties").and_then(|v| v.as_object()) {
134 collect_entries(props, &parts, out);
135 }
136 }
137 _ => {
138 if let Some(props) = value.get("properties").and_then(|v| v.as_object()) {
139 collect_entries(props, &parts, out);
140 }
141 }
142 }
143 }
144}
145
146fn parse_setting(path_parts: &[&str], value: &Value) -> (String, FieldInfo) {
147 let yaml_path = path_parts.join(".");
148
149 let has_no_env_tag = value
150 .get("tags")
151 .and_then(|v| v.as_array())
152 .map(|tags| tags.iter().any(|t| t.as_str() == Some("no-env")))
153 .unwrap_or(false);
154
155 let explicit_env_vars: Vec<String> = value
156 .get("env_vars")
157 .and_then(|v| v.as_array())
158 .map(|seq| seq.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
159 .unwrap_or_default();
160
161 let env = if has_no_env_tag {
163 EnvBinding::None
164 } else if explicit_env_vars.is_empty() {
165 EnvBinding::Standard
166 } else {
167 EnvBinding::Overridden(explicit_env_vars)
168 };
169
170 let value_type = parse_value_type(value);
171 let default = value.get("default").and_then(yaml_value_to_json_str);
172 let env_parser = value.get("env_parser").and_then(|v| v.as_str()).map(EnvParser::parse);
173
174 (
175 yaml_path,
176 FieldInfo {
177 value_type,
178 env,
179 env_parser,
180 default,
181 },
182 )
183}
184
185fn parse_value_type(value: &Value) -> FieldType {
186 let ty = value.get("type").and_then(|v| v.as_str());
187 let format = value.get("format").and_then(|v| v.as_str());
188
189 if format == Some("duration") {
193 return match ty {
194 Some("string") | Some("number") | Some("integer") => FieldType::Duration,
195 other => panic!(
196 "config schema field has `format: duration` with unsupported base type {:?}; \
197 expected string, number, or integer",
198 other
199 ),
200 };
201 }
202
203 match ty {
204 Some("string") => FieldType::String,
205 Some("boolean") => FieldType::Bool,
206 Some("integer") => FieldType::Integer,
207 Some("number") => FieldType::Float,
208 Some("array") => {
209 let item_type = value.get("items").and_then(|v| v.get("type")).and_then(|v| v.as_str());
210 if item_type == Some("string") {
211 FieldType::StringList
212 } else {
213 FieldType::Unknown
214 }
215 }
216 _ => FieldType::Unknown,
217 }
218}
219
220fn yaml_value_to_json_str(value: &Value) -> Option<String> {
221 match value {
222 Value::Null => None,
223 Value::Bool(b) => Some(b.to_string()),
224 Value::Number(n) => Some(n.to_string()),
225 Value::String(s) => {
226 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
227 Some(format!("\"{}\"", escaped))
228 }
229 Value::Array(seq) => {
230 let items: Option<Vec<String>> = seq.iter().map(yaml_value_to_json_str).collect();
231 items.map(|elems| format!("[{}]", elems.join(",")))
232 }
233 Value::Object(map) if map.is_empty() => Some("{}".to_string()),
234 _ => None,
235 }
236}
237
238pub fn field_type_as_rust(ft: &FieldType) -> &'static str {
240 match ft {
241 FieldType::String | FieldType::Unknown => "ValueType::String",
242 FieldType::Bool => "ValueType::Bool",
243 FieldType::Integer => "ValueType::Integer",
244 FieldType::Float => "ValueType::Float",
245 FieldType::StringList => "ValueType::StringList",
246 FieldType::Duration => "ValueType::Duration",
247 }
248}
249
250pub fn is_unknown(ft: &FieldType) -> bool {
252 matches!(ft, FieldType::Unknown)
253}
254
255pub fn escape_str(s: &str) -> String {
257 s.replace('\\', "\\\\").replace('"', "\\\"")
258}
259
260pub fn generate_schema_rs(schema_map: &IndexMap<String, FieldInfo>, dir: &Path) {
266 use std::fmt::Write as _;
267
268 let mut out = String::new();
269 writeln!(
270 out,
271 "// @generated by build.rs from core_schema.yaml + schema_overlay.yaml — DO NOT EDIT"
272 )
273 .unwrap();
274 writeln!(out, "#[allow(unused_imports)]").unwrap();
275 writeln!(out, "use super::*;").unwrap();
276 writeln!(out).unwrap();
277
278 let mut keys: Vec<&str> = schema_map.keys().map(|s| s.as_str()).collect();
279 keys.sort_unstable();
280
281 for yaml_path in &keys {
282 let info = &schema_map[*yaml_path];
283 let const_name = yaml_path_to_const(yaml_path);
284 let vt = field_type_as_rust(&info.value_type);
285
286 if is_unknown(&info.value_type) {
287 writeln!(
288 out,
289 "// TODO: unknown type for '{}' — set value_type_override in the annotation",
290 yaml_path
291 )
292 .unwrap();
293 }
294
295 let env_vars_lit = match &info.env {
298 EnvBinding::Overridden(vars) => {
299 let items: Vec<String> = vars.iter().map(|e| format!("\"{}\"", escape_str(e))).collect();
300 format!("&[{}]", items.join(", "))
301 }
302 EnvBinding::None | EnvBinding::Standard => "&[]".to_string(),
303 };
304
305 let default_lit = match &info.default {
306 Some(d) => format!("Some(\"{}\")", escape_str(d)),
307 None => "None".to_string(),
308 };
309
310 writeln!(out, "pub const {}: SchemaEntry = SchemaEntry {{", const_name).unwrap();
311 writeln!(out, " schema: Schema::Datadog,").unwrap();
312 writeln!(out, " yaml_path: \"{}\",", yaml_path).unwrap();
313 writeln!(out, " env_vars: {},", env_vars_lit).unwrap();
314 writeln!(out, " value_type: {},", vt).unwrap();
315 writeln!(out, " default: {},", default_lit).unwrap();
316 writeln!(out, "}};").unwrap();
317 writeln!(out).unwrap();
318 }
319
320 let path = dir.join("schema.rs");
322 if std::fs::read_to_string(&path).is_ok_and(|existing| existing == out) {
323 return;
324 }
325 std::fs::write(&path, out).unwrap_or_else(|e| panic!("cannot write {}: {}", path.display(), e));
326}
327
328pub fn yaml_path_to_const(yaml_path: &str) -> String {
331 yaml_path
332 .chars()
333 .map(|c| if c == '.' || c == '-' { '_' } else { c })
334 .collect::<String>()
335 .to_uppercase()
336}